Skip to content

Commit 45c9428

Browse files
authored
Merge pull request github#5337 from smowton/smowton/feature/commons-lang-random-sources
Java: Add support for Commons-Lang's RandomUtils
2 parents 9268050 + 92d6135 commit 45c9428

30 files changed

+470
-152
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lgtm,codescanning
2+
* Added models for the Apache Commons Lang `RandomUtils` class. This may lead to extra results from queries that check for proper use of random-number generators or those which check the range of possible random values that could be returned, including `java/improper-validation-of-array-index-code-specified` and `java/uncontrolled-arithmetic`.

java/ql/src/Likely Bugs/Arithmetic/BadAbsOfRandom.ql

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@
1111
*/
1212

1313
import java
14+
import semmle.code.java.security.Random
1415

15-
from MethodAccess ma, Method abs, Method nextIntOrLong, MethodAccess nma
16+
from MethodAccess ma, Method abs, Method nextIntOrLong, RandomDataSource nma
1617
where
1718
ma.getMethod() = abs and
1819
abs.hasName("abs") and
1920
abs.getDeclaringType().hasQualifiedName("java.lang", "Math") and
2021
ma.getAnArgument() = nma and
2122
nma.getMethod() = nextIntOrLong and
22-
(nextIntOrLong.hasName("nextInt") or nextIntOrLong.hasName("nextLong")) and
23-
nextIntOrLong.getDeclaringType().hasQualifiedName("java.util", "Random") and
24-
nextIntOrLong.hasNoParameters()
23+
nextIntOrLong.hasName(["nextInt", "nextLong"]) and
24+
not nma.resultMayBeBounded()
2525
select ma, "Incorrect computation of abs of signed integral random value."

java/ql/src/Likely Bugs/Arithmetic/RandomUsedOnce.ql

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,8 @@
1212
*/
1313

1414
import java
15+
import semmle.code.java.security.Random
1516

16-
from MethodAccess ma, Method random
17-
where
18-
random.getDeclaringType().hasQualifiedName("java.util", "Random") and
19-
ma.getMethod() = random and
20-
ma.getQualifier() instanceof ClassInstanceExpr
17+
from RandomDataSource ma
18+
where ma.getQualifier() instanceof ClassInstanceExpr
2119
select ma, "Random object created and used only once."

java/ql/src/Security/CWE/CWE-129/ArraySizing.qll

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import java
22
import semmle.code.java.dataflow.DataFlow
33
import semmle.code.java.dataflow.DefUse
4+
import semmle.code.java.security.Random
45
private import BoundingChecks
56

67
/**
@@ -124,33 +125,16 @@ abstract class BoundedFlowSource extends DataFlow::Node {
124125
}
125126

126127
/**
127-
* Input that is constructed using a `Random` value.
128+
* Input that is constructed using a random value.
128129
*/
129130
class RandomValueFlowSource extends BoundedFlowSource {
130-
RandomValueFlowSource() {
131-
exists(RefType random, MethodAccess nextAccess |
132-
random.hasQualifiedName("java.util", "Random")
133-
|
134-
nextAccess.getCallee().getDeclaringType().getAnAncestor() = random and
135-
nextAccess.getCallee().getName().matches("next%") and
136-
nextAccess = this.asExpr()
137-
)
138-
}
131+
RandomDataSource nextAccess;
139132

140-
override int lowerBound() {
141-
// If this call is to `nextInt()`, the lower bound is zero.
142-
this.asExpr().(MethodAccess).getCallee().hasName("nextInt") and
143-
this.asExpr().(MethodAccess).getNumArgument() = 1 and
144-
result = 0
145-
}
133+
RandomValueFlowSource() { this.asExpr() = nextAccess }
146134

147-
override int upperBound() {
148-
// If this call specified an argument to `nextInt()`, and that argument is a compile time constant,
149-
// it forms the upper bound.
150-
this.asExpr().(MethodAccess).getCallee().hasName("nextInt") and
151-
this.asExpr().(MethodAccess).getNumArgument() = 1 and
152-
result = this.asExpr().(MethodAccess).getArgument(0).(CompileTimeConstantExpr).getIntValue()
153-
}
135+
override int lowerBound() { result = nextAccess.getLowerBound() }
136+
137+
override int upperBound() { result = nextAccess.getUpperBound() }
154138

155139
override string getDescription() { result = "Random value" }
156140
}

java/ql/src/Security/CWE/CWE-190/ArithmeticUncontrolled.ql

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,34 +13,14 @@
1313

1414
import java
1515
import semmle.code.java.dataflow.TaintTracking
16+
import semmle.code.java.security.Random
1617
import semmle.code.java.security.SecurityTests
1718
import ArithmeticCommon
1819
import DataFlow::PathGraph
1920

2021
class TaintSource extends DataFlow::ExprNode {
2122
TaintSource() {
22-
// Either this is an access to a random number generating method of the right kind, ...
23-
exists(Method def |
24-
def = this.getExpr().(MethodAccess).getMethod() and
25-
(
26-
// Some random-number methods are omitted:
27-
// `nextDouble` and `nextFloat` are between 0 and 1,
28-
// `nextGaussian` is extremely unlikely to hit max values.
29-
def.getName() = "nextInt" or
30-
def.getName() = "nextLong"
31-
) and
32-
def.getNumberOfParameters() = 0 and
33-
def.getDeclaringType().hasQualifiedName("java.util", "Random")
34-
)
35-
or
36-
// ... or this is the array parameter of `nextBytes`, which is filled with random bytes.
37-
exists(MethodAccess m, Method def |
38-
m.getAnArgument() = this.getExpr() and
39-
m.getMethod() = def and
40-
def.getName() = "nextBytes" and
41-
def.getNumberOfParameters() = 1 and
42-
def.getDeclaringType().hasQualifiedName("java.util", "Random")
43-
)
23+
exists(RandomDataSource m | not m.resultMayBeBounded() | m.getOutput() = this.getExpr())
4424
}
4525
}
4626

java/ql/src/semmle/code/java/dataflow/RangeAnalysis.qll

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ private import SSA
6868
private import RangeUtils
6969
private import semmle.code.java.dataflow.internal.rangeanalysis.SsaReadPositionCommon
7070
private import semmle.code.java.controlflow.internal.GuardsLogic
71+
private import semmle.code.java.security.Random
7172
private import SignAnalysis
7273
private import ModulusAnalysis
7374
private import semmle.code.java.Reflection
@@ -486,14 +487,17 @@ private predicate boundFlowStep(Expr e2, Expr e1, int delta, boolean upper) {
486487
or
487488
e2.(AssignOrExpr).getSource() = e1 and positive(e2) and delta = 0 and upper = false
488489
or
489-
exists(MethodAccess ma, Method m |
490-
e2 = ma and
491-
ma.getMethod() = m and
492-
m.hasName("nextInt") and
493-
m.getDeclaringType().hasQualifiedName("java.util", "Random") and
494-
e1 = ma.getAnArgument() and
495-
delta = -1 and
496-
upper = true
490+
exists(RandomDataSource rds |
491+
e2 = rds.getOutput() and
492+
(
493+
e1 = rds.getUpperBoundExpr() and
494+
delta = -1 and
495+
upper = true
496+
or
497+
e1 = rds.getLowerBoundExpr() and
498+
delta = 0 and
499+
upper = false
500+
)
497501
)
498502
or
499503
exists(MethodAccess ma, Method m |

java/ql/src/semmle/code/java/security/Random.qll

Lines changed: 148 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,159 @@ import java
22
import semmle.code.java.dataflow.DefUse
33
import semmle.code.java.dataflow.DataFlow
44

5+
/**
6+
* The `java.security.SecureRandom` class.
7+
*/
58
class SecureRandomNumberGenerator extends RefType {
69
SecureRandomNumberGenerator() { this.hasQualifiedName("java.security", "SecureRandom") }
710
}
811

9-
class GetRandomData extends MethodAccess {
10-
GetRandomData() {
11-
this.getMethod().getName().matches("next%") and
12-
this.getQualifier().getType() instanceof SecureRandomNumberGenerator
12+
/**
13+
* A method access that returns random data or writes random data to an argument.
14+
*/
15+
abstract class RandomDataSource extends MethodAccess {
16+
/**
17+
* Gets the integer lower bound, inclusive, of the values returned by this call,
18+
* if applicable to this method's type and a constant bound is known.
19+
*/
20+
int getLowerBound() { result = this.getLowerBoundExpr().(CompileTimeConstantExpr).getIntValue() }
21+
22+
/**
23+
* Gets the integer lower bound, inclusive, of the values returned by this call,
24+
* if applicable to this method's type and a bound is known.
25+
*/
26+
Expr getLowerBoundExpr() { none() }
27+
28+
/**
29+
* Gets the integer upper bound, exclusive, of the values returned by this call,
30+
* if applicable to this method's type and a constant bound is known.
31+
*/
32+
int getUpperBound() { result = this.getUpperBoundExpr().(CompileTimeConstantExpr).getIntValue() }
33+
34+
/**
35+
* Gets the integer upper bound, exclusive, of the values returned by this call,
36+
* if applicable to this method's type and a bound is known.
37+
*/
38+
Expr getUpperBoundExpr() { none() }
39+
40+
/**
41+
* Holds if this source of random data may return bounded values (e.g. integers between 1 and 10).
42+
* If it does not hold, it may return any value in the range of its result type (e.g., any possible integer).
43+
*/
44+
predicate resultMayBeBounded() { none() }
45+
46+
/**
47+
* Gets the result of this source of randomness: either the method access itself, or some argument
48+
* in the case where it writes random data to that argument.
49+
*/
50+
abstract Expr getOutput();
51+
}
52+
53+
/**
54+
* A method access calling a method declared on `java.util.Random`
55+
* that returns random data or writes random data to an argument.
56+
*/
57+
class StdlibRandomSource extends RandomDataSource {
58+
Method m;
59+
60+
StdlibRandomSource() {
61+
m = this.getMethod() and
62+
m.getName().matches("next%") and
63+
m.getDeclaringType().getAnAncestor().hasQualifiedName("java.util", "Random")
64+
}
65+
66+
// Note for the following bounds functions: `java.util.Random` only defines no-arg versions
67+
// of `nextInt` and `nextLong` plus `nextInt(int x)`, bounded to the range [0, x)
68+
// However `ThreadLocalRandom` provides one- and two-arg versions of `nextInt` and `nextLong`
69+
// which allow both lower and upper bounds for both types.
70+
override int getLowerBound() {
71+
// If this call is to `nextInt(int)` or `nextLong(long), the lower bound is zero.
72+
m.hasName(["nextInt", "nextLong"]) and
73+
m.getNumberOfParameters() = 1 and
74+
result = 0
75+
or
76+
result = super.getLowerBound() // Include a lower bound provided via `getLowerBoundExpr`
77+
}
78+
79+
override Expr getLowerBoundExpr() {
80+
// If this call is to `nextInt(int, int)` or `nextLong(long, long)`, the lower bound is the first argument.
81+
m.hasName(["nextInt", "nextLong"]) and
82+
m.getNumberOfParameters() = 2 and
83+
result = this.getArgument(0)
84+
}
85+
86+
override Expr getUpperBoundExpr() {
87+
// If this call is to `nextInt(int)` or `nextLong(long)`, the upper bound is the first argument.
88+
// If it calls `nextInt(int, int)` or `nextLong(long, long)`, the upper bound is the second argument.
89+
m.hasName(["nextInt", "nextLong"]) and
90+
(
91+
m.getNumberOfParameters() = 1 and
92+
result = this.getArgument(0)
93+
or
94+
m.getNumberOfParameters() = 2 and
95+
result = this.getArgument(1)
96+
)
97+
}
98+
99+
override predicate resultMayBeBounded() {
100+
// `next` may be restricted by its `bits` argument,
101+
// `nextBoolean` can't possibly be usefully bounded,
102+
// `nextDouble` and `nextFloat` are between 0 and 1,
103+
// `nextGaussian` is extremely unlikely to hit max values.
104+
m.hasName(["next", "nextBoolean", "nextDouble", "nextFloat", "nextGaussian"])
105+
or
106+
m.hasName(["nextInt", "nextLong"]) and
107+
m.getNumberOfParameters() = [1, 2]
108+
}
109+
110+
override Expr getOutput() {
111+
if m.hasName("getBytes") then result = this.getArgument(0) else result = this
112+
}
113+
}
114+
115+
/**
116+
* A method access calling a method declared on `org.apache.commons.lang3.RandomUtils`
117+
* that returns random data or writes random data to an argument.
118+
*/
119+
class ApacheCommonsRandomSource extends RandomDataSource {
120+
Method m;
121+
122+
ApacheCommonsRandomSource() {
123+
m = this.getMethod() and
124+
m.getName().matches("next%") and
125+
m.getDeclaringType().hasQualifiedName("org.apache.commons.lang3", "RandomUtils")
126+
}
127+
128+
override Expr getLowerBoundExpr() {
129+
// If this call is to `nextInt(int, int)` or `nextLong(long, long)`, the lower bound is the first argument.
130+
m.hasName(["nextInt", "nextLong"]) and
131+
m.getNumberOfParameters() = 2 and
132+
result = this.getArgument(0)
133+
}
134+
135+
override Expr getUpperBoundExpr() {
136+
// If this call is to `nextInt(int, int)` or `nextLong(long, long)`, the upper bound is the second argument.
137+
m.hasName(["nextInt", "nextLong"]) and
138+
m.getNumberOfParameters() = 2 and
139+
result = this.getArgument(1)
13140
}
141+
142+
override predicate resultMayBeBounded() {
143+
m.hasName(["nextDouble", "nextFloat"])
144+
or
145+
m.hasName(["nextInt", "nextLong"]) and
146+
m.getNumberOfParameters() = 2
147+
}
148+
149+
override Expr getOutput() { result = this }
150+
}
151+
152+
/**
153+
* A method access calling a method declared on `java.security.SecureRandom`
154+
* that returns random data or writes random data to an argument.
155+
*/
156+
class GetRandomData extends StdlibRandomSource {
157+
GetRandomData() { this.getQualifier().getType() instanceof SecureRandomNumberGenerator }
14158
}
15159

16160
private predicate isSeeded(RValue use) {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
| Test.java:10:5:10:25 | abs(...) | Incorrect computation of abs of signed integral random value. |
2+
| Test.java:11:5:11:26 | abs(...) | Incorrect computation of abs of signed integral random value. |
3+
| Test.java:14:5:14:35 | abs(...) | Incorrect computation of abs of signed integral random value. |
4+
| Test.java:15:5:15:36 | abs(...) | Incorrect computation of abs of signed integral random value. |
5+
| Test.java:20:5:20:27 | abs(...) | Incorrect computation of abs of signed integral random value. |
6+
| Test.java:21:5:21:28 | abs(...) | Incorrect computation of abs of signed integral random value. |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Likely Bugs/Arithmetic/BadAbsOfRandom.ql
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import java.util.Random;
2+
import java.util.concurrent.ThreadLocalRandom;
3+
import org.apache.commons.lang3.RandomUtils;
4+
5+
public class Test {
6+
7+
public static void test() {
8+
9+
Random r = new Random();
10+
Math.abs(r.nextInt());
11+
Math.abs(r.nextLong());
12+
Math.abs(r.nextInt(100)); // GOOD: random value already has a restricted range
13+
14+
Math.abs(RandomUtils.nextInt());
15+
Math.abs(RandomUtils.nextLong());
16+
Math.abs(RandomUtils.nextInt(1, 10)); // GOOD: random value already has a restricted range
17+
Math.abs(RandomUtils.nextLong(1, 10)); // GOOD: random value already has a restricted range
18+
19+
ThreadLocalRandom tlr = ThreadLocalRandom.current();
20+
Math.abs(tlr.nextInt());
21+
Math.abs(tlr.nextLong());
22+
Math.abs(tlr.nextInt(10)); // GOOD: random value already has a restricted range
23+
Math.abs(tlr.nextLong(10)); // GOOD: random value already has a restricted range
24+
Math.abs(tlr.nextInt(1, 10)); // GOOD: random value already has a restricted range
25+
Math.abs(tlr.nextLong(1, 10)); // GOOD: random value already has a restricted range
26+
27+
}
28+
29+
}

0 commit comments

Comments
 (0)