Skip to content

Commit 06334ee

Browse files
authored
Merge pull request #14554 from maikypedia/maikypedia/insecure-randomness
Ruby: Add Insecure Randomness Query
2 parents e7676a0 + c2c4d9e commit 06334ee

File tree

10 files changed

+222
-0
lines changed

10 files changed

+222
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* Provides default sources, sinks, and sanitizers for reasoning about random values that
3+
* are not cryptographically secure, as well as extension points for adding your own.
4+
*/
5+
6+
private import codeql.ruby.CFG
7+
private import codeql.ruby.AST
8+
private import codeql.ruby.DataFlow
9+
private import codeql.ruby.security.SensitiveActions
10+
private import codeql.ruby.Concepts
11+
private import codeql.ruby.ApiGraphs
12+
import codeql.ruby.frameworks.core.Kernel
13+
14+
/**
15+
* Provides default sources, sinks, and sanitizers for reasoning about random values that
16+
* are not cryptographically secure, as well as extension points for adding your own.
17+
*/
18+
module InsecureRandomness {
19+
/**
20+
* A data flow source for random values that are not cryptographically secure.
21+
*/
22+
abstract class Source extends DataFlow::Node { }
23+
24+
/**
25+
* A data flow sink for random values that are not cryptographically secure.
26+
*/
27+
abstract class Sink extends DataFlow::Node { }
28+
29+
/**
30+
* A sanitizer for random values that are not cryptographically secure.
31+
*/
32+
abstract class Sanitizer extends DataFlow::Node { }
33+
34+
/**
35+
* A simple random number generator that is not cryptographically secure.
36+
*/
37+
class DefaultSource extends Source, DataFlow::CallNode {
38+
DefaultSource() {
39+
this.asExpr().getExpr() instanceof UnknownMethodCall and
40+
(
41+
this.getReceiver().asExpr().getExpr() instanceof SelfVariableAccess and
42+
super.getMethodName() = "rand"
43+
)
44+
or
45+
this.(Kernel::KernelMethodCall).getMethodName() = "rand"
46+
}
47+
}
48+
49+
/**
50+
* A sensitive write, considered as a sink for random values that are not cryptographically
51+
* secure.
52+
*/
53+
class SensitiveWriteSink extends Sink instanceof SensitiveWrite { }
54+
55+
/**
56+
* A cryptographic key, considered as a sink for random values that are not cryptographically
57+
* secure.
58+
*/
59+
class CryptoKeySink extends Sink {
60+
CryptoKeySink() {
61+
exists(Cryptography::CryptographicOperation operation | this = operation.getAnInput())
62+
}
63+
}
64+
65+
/**
66+
* A index call, considered as a sink for random values that are not cryptographiocally
67+
* secure
68+
*/
69+
class CharacterIndexing extends Sink {
70+
CharacterIndexing() {
71+
exists(DataFlow::CallNode c | this = c.getAMethodCall("[]").getArgument(0))
72+
}
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Provides default sources, sinks and sanitizers for detecting Insecure Randomness
3+
* vulnerabilities, as well as extension points for adding your own.
4+
*/
5+
6+
private import codeql.ruby.DataFlow
7+
private import codeql.ruby.TaintTracking
8+
import InsecureRandomnessCustomizations::InsecureRandomness
9+
10+
private module InsecureRandomnessConfig implements DataFlow::ConfigSig {
11+
predicate isSource(DataFlow::Node source) { source instanceof Source }
12+
13+
predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
14+
15+
predicate isBarrier(DataFlow::Node node) { node instanceof Sanitizer }
16+
}
17+
18+
/**
19+
* Taint-tracking for detecting Insecure Randomness vulnerabilities.
20+
*/
21+
module InsecureRandomnessFlow = TaintTracking::Global<InsecureRandomnessConfig>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: newQuery
3+
---
4+
* Added a new experimental query, `rb/insecure-randomness`, to detect when application uses random values that are not cryptographically secure.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>
8+
Using a cryptographically weak pseudo-random number generator to generate a security-sensitive value,
9+
such as a password, makes it easier for an attacker to predict the value.
10+
11+
Pseudo-random number generators generate a sequence of numbers that only approximates the
12+
properties of random numbers. The sequence is not truly random because it is completely
13+
determined by a relatively small set of initial values, the seed. If the random number generator is
14+
cryptographically weak, then this sequence may be easily predictable through outside observations.
15+
</p>
16+
</overview>
17+
18+
<recommendation>
19+
<p>
20+
When generating values for use in security-sensitive contexts, it's essential to utilize a
21+
cryptographically secure pseudo-random number generator. As a general guideline, a value
22+
should be deemed "security-sensitive" if its predictability would empower an attacker to
23+
perform actions that would otherwise be beyond their reach. For instance, if an attacker could
24+
predict a newly generated user's random password, they would gain unauthorized access to that user's
25+
account.
26+
27+
For Ruby, <code>SecureRandom</code> provides a cryptographically secure pseudo-random number generator.
28+
<code>rand</code> is not cryptographically secure, and should be avoided in security contexts.
29+
For contexts which are not security sensitive, <code>Random</code> may be preferable as it has a more convenient
30+
interface.
31+
32+
</p>
33+
</recommendation>
34+
35+
<example>
36+
<p>
37+
The following examples show different ways of generating a password.
38+
</p>
39+
40+
<p>The first example uses <code>Random.rand()</code> which is not for security purposes</p>
41+
42+
<sample src="examples/InsecureRandomnessBad.rb" />
43+
44+
<p>In the second example, the password is generated using <code>SecureRandom.random_bytes()</code> which is a
45+
cryptographically secure method.</p>
46+
47+
<sample src="examples/InsecureRandomnessGood.rb" />
48+
</example>
49+
50+
<references>
51+
<li>Wikipedia: <a href="http://en.wikipedia.org/wiki/Pseudorandom_number_generator">Pseudo-random number generator</a>.</li>
52+
<li>Common Weakness Enumeration: <a href="https://cwe.mitre.org/data/definitions/338.html">CWE-338</a>.</li>
53+
<li>Ruby-doc: <a href="https://ruby-doc.org/core-3.1.2/Random.html">Random</a>.</li>
54+
</references>
55+
</qhelp>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* @name Insecure randomness
3+
* @description Using a cryptographically weak pseudo-random number generator to generate a
4+
* security-sensitive value may allow an attacker to predict what value will
5+
* be generated.
6+
* @kind path-problem
7+
* @problem.severity warning
8+
* @security-severity 7.8
9+
* @precision high
10+
* @id rb/insecure-randomness
11+
* @tags security
12+
* external/cwe/cwe-338
13+
*/
14+
15+
import codeql.ruby.DataFlow
16+
import codeql.ruby.security.InsecureRandomnessQuery
17+
import InsecureRandomnessFlow::PathGraph
18+
19+
from InsecureRandomnessFlow::PathNode source, InsecureRandomnessFlow::PathNode sink
20+
where InsecureRandomnessFlow::flowPath(source, sink)
21+
select sink.getNode(), source, sink,
22+
"This uses a cryptographically insecure random number generated at $@ in a security context.",
23+
source.getNode(), source.getNode().toString()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
def generate_password()
2+
chars = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a + ['!', '@', '#', '$', '%']
3+
# BAD: rand is not cryptographically secure
4+
password = (1..10).collect { chars[rand(chars.size)] }.join
5+
end
6+
7+
password = generate_password
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
require 'securerandom'
2+
3+
def generate_password()
4+
chars = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a + ['!', '@', '#', '$', '%']
5+
6+
# GOOD: SecureRandom is cryptographically secure
7+
password = SecureRandom.random_bytes(10).each_byte.map do |byte|
8+
chars[byte % chars.length]
9+
end.join
10+
end
11+
12+
password = generate_password()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
edges
2+
nodes
3+
| InsecureRandomness.rb:6:42:6:57 | call to rand | semmle.label | call to rand |
4+
subpaths
5+
#select
6+
| InsecureRandomness.rb:6:42:6:57 | call to rand | InsecureRandomness.rb:6:42:6:57 | call to rand | InsecureRandomness.rb:6:42:6:57 | call to rand | This uses a cryptographically insecure random number generated at $@ in a security context. | InsecureRandomness.rb:6:42:6:57 | call to rand | call to rand |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
experimental/insecure-randomness/InsecureRandomness.ql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
require 'securerandom'
2+
3+
def generate_password_1(length)
4+
chars = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a + ['!', '@', '#', '$', '%']
5+
# BAD: rand is not cryptographically secure
6+
password = (1..length).collect { chars[rand(chars.size)] }.join
7+
end
8+
9+
def generate_password_2(length)
10+
chars = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a + ['!', '@', '#', '$', '%']
11+
12+
# GOOD: SecureRandom is cryptographically secure
13+
password = SecureRandom.random_bytes(length).each_byte.map do |byte|
14+
chars[byte % chars.length]
15+
end.join
16+
end
17+
18+
password = generate_password_1(10)
19+
password = generate_password_2(10)

0 commit comments

Comments
 (0)