Skip to content

Commit cbab9a7

Browse files
committed
Improve local flow logic
1 parent bebf594 commit cbab9a7

File tree

5 files changed

+448
-237
lines changed

5 files changed

+448
-237
lines changed

java/ql/lib/semmle/code/java/dataflow/ListOfConstantsSanitizer.qll

Lines changed: 101 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ predicate methodCallHasConstantArguments(MethodCall mc) {
6363
)
6464
}
6565

66+
/** The name of a class implementing `java.util.Collection`. */
6667
class CollectionClass extends string {
6768
CollectionClass() { this = ["List", "Set"] }
6869
}
@@ -87,64 +88,127 @@ module Collection {
8788
}
8889
}
8990

90-
private class NonConstantElementAddition extends Expr {
91+
/** A call where a collection of constants of class `collectionClass` can be in */
92+
abstract class SafeCall extends Call {
93+
int arg;
9194
CollectionClass collectionClass;
9295

96+
SafeCall() {
97+
arg = -1 and exists(this.getQualifier())
98+
or
99+
exists(this.getArgument(arg))
100+
}
101+
93102
/** Gets whether the collection is a "List" or a "Set". */
94103
CollectionClass getCollectionClass() { result = collectionClass }
95104

96-
NonConstantElementAddition() {
97-
exists(Method m, RefType t, MethodCall mc |
98-
this = mc.getQualifier() and
99-
mc.getMethod() = m and
105+
/** Gets the argument index, or -1 for the qualifier. */
106+
int getArg() { result = arg }
107+
}
108+
109+
private class AddConstantElement extends SafeCall, MethodCall {
110+
AddConstantElement() {
111+
arg = -1 and
112+
exists(Method m, RefType t |
113+
this.getMethod() = m and
100114
t = m.getDeclaringType().getSourceDeclaration().getASourceSupertype*()
101115
|
102116
collectionClass = "List" and
103117
t.hasQualifiedName("java.util", "List") and
104118
m.getName() = ["add", "addFirst", "addLast"] and
105-
not mc.getArgument(m.getNumberOfParameters() - 1).isCompileTimeConstant()
119+
this.getArgument(m.getNumberOfParameters() - 1).isCompileTimeConstant()
106120
or
107121
collectionClass = "Set" and
108122
t.hasQualifiedName("java.util", "Set") and
109123
m.getName() = ["add"] and
110-
not mc.getArgument(0).isCompileTimeConstant()
111-
or
112-
// If a whole collection is added then we don't try to track if it contains
113-
// only compile-time constants, and conservatively assume that it does.
114-
t.hasQualifiedName("java.util", "Collection") and
115-
m.getName() = "addAll"
124+
this.getArgument(0).isCompileTimeConstant()
116125
)
117126
}
118127
}
119128

120-
private predicate collectionOfConstantsLocalFlowTo(CollectionClass collectionClass, Expr e) {
121-
exists(CollectionOfConstants loc |
122-
loc.getCollectionClass() = collectionClass and DataFlow::localExprFlow(loc, e)
123-
|
124-
loc.isImmutable()
125-
or
126-
not DataFlow::localExprFlow(any(NonConstantElementAddition ncea), e)
127-
)
129+
private class UnmodifiableCollection extends SafeCall, MethodCall {
130+
UnmodifiableCollection() {
131+
arg = 0 and
132+
exists(Method m |
133+
this.getMethod() = m and
134+
m.hasName("unmodifiable" + collectionClass) and
135+
m.getDeclaringType()
136+
.getSourceDeclaration()
137+
.getASourceSupertype*()
138+
.hasQualifiedName("java.util", "Collections")
139+
)
140+
}
141+
}
142+
143+
// Expr foo(Expr q, string s) {
144+
// q = any(CollectionContainsCall ccc).getQualifier() and
145+
// result = getALocalExprFlowRoot(q) and
146+
// (
147+
// if exists(Field f | DataFlow::localExprFlow(f.getAnAccess(), result))
148+
// then
149+
// exists(Field f | DataFlow::localExprFlow(f.getAnAccess(), result) |
150+
// f.isStatic() and
151+
// f.isFinal() and
152+
// s = "static final field"
153+
// or
154+
// not (
155+
// f.isStatic() and
156+
// f.isFinal()
157+
// ) and
158+
// s = "field read"
159+
// )
160+
// else
161+
// if result = any(MethodCall mc)
162+
// then s = "method call"
163+
// else
164+
// if result = any(ConstructorCall cc)
165+
// then s = "constructor call"
166+
// else
167+
// if result = any(Call cc)
168+
// then s = "other call"
169+
// else s = "something else"
170+
// )
171+
// }
172+
Expr getALocalExprFlowRoot(Expr e) {
173+
DataFlow::localExprFlow(result, e) and
174+
not exists(Expr e2 | e2 != result | DataFlow::localExprFlow(e2, result))
128175
}
129176

130-
private predicate collectionOfConstantsFlowsTo(CollectionClass collectionClass, Expr e) {
131-
collectionOfConstantsLocalFlowTo(collectionClass, e)
132-
or
133-
// Access a static final field to get an immutable list of constants.
134-
exists(Field f |
135-
f.isStatic() and
136-
f.isFinal() and
137-
forall(Expr v | v = f.getInitializer() or v = f.getAnAccess().(FieldWrite).getASource() |
138-
v =
139-
any(CollectionOfConstants loc |
140-
loc.getCollectionClass() = collectionClass and loc.isImmutable()
141-
)
177+
private predicate noUnsafeCalls(Expr e) {
178+
forall(MethodCall mc, int arg, Expr x |
179+
DataFlow::localExprFlow(x, e) and
180+
(
181+
arg = -1 and x = mc.getQualifier()
182+
or
183+
x = mc.getArgument(arg)
142184
)
143185
|
144-
DataFlow::localExprFlow(f.getAnAccess(), e)
186+
x = e or arg = mc.(SafeCall).getArg()
145187
)
146188
}
147189

190+
private predicate collectionOfConstantsFlowsTo(Expr e) {
191+
forex(Expr r | r = getALocalExprFlowRoot(e) |
192+
r instanceof CollectionOfConstants
193+
or
194+
// Access a static final field to get an immutable list of constants.
195+
exists(Field f | r = f.getAnAccess() |
196+
f.isStatic() and
197+
f.isFinal() and
198+
forall(Expr v | v = f.getInitializer() |
199+
v = any(CollectionOfConstants loc | loc.isImmutable())
200+
) and
201+
forall(Expr fieldSource | fieldSource = f.getAnAccess().(FieldWrite).getASource() |
202+
forall(Expr root | root = getALocalExprFlowRoot(fieldSource) |
203+
root.(CollectionOfConstants).isImmutable()
204+
) and
205+
noUnsafeCalls(fieldSource)
206+
)
207+
)
208+
) and
209+
noUnsafeCalls(e)
210+
}
211+
148212
/**
149213
* An invocation of `java.util.List.contains` where the qualifier contains only
150214
* compile-time constants.
@@ -155,7 +219,7 @@ module Collection {
155219
this = mc and
156220
e = mc.getArgument(0) and
157221
outcome = true and
158-
collectionOfConstantsFlowsTo(mc.getCollectionClass(), mc.getQualifier())
222+
collectionOfConstantsFlowsTo(mc.getQualifier())
159223
)
160224
}
161225
}
@@ -220,7 +284,8 @@ module Collection {
220284
.getASourceSupertype*()
221285
.hasQualifiedName("java.util", "Collection")
222286
) and
223-
collectionOfConstantsFlowsTo(_, this.getArgument(0))
287+
// Any collection can be used in the non-empty constructor.
288+
collectionOfConstantsFlowsTo(this.getArgument(0))
224289
}
225290

226291
override predicate isImmutable() { none() }
@@ -281,7 +346,7 @@ module Collection {
281346
.hasQualifiedName("java.util", "Collections") and
282347
this.getMethod() = m
283348
|
284-
collectionOfConstantsFlowsTo(collectionClass, this.getArgument(0))
349+
collectionOfConstantsFlowsTo(this.getArgument(0))
285350
)
286351
}
287352

java/ql/test/query-tests/security/CWE-089/semmle/examples/AllowListSanitizerWithJavaUtilList.java

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
import java.util.ArrayList;
1212
import java.util.Arrays;
1313
import java.util.Collections;
14+
import java.util.HashSet;
1415
import java.util.List;
16+
import java.util.Set;
1517

1618
class AllowListSanitizerWithJavaUtilList {
1719
public static Connection connection;
@@ -48,6 +50,7 @@ public static void main(String[] args) throws IOException, SQLException {
4850
testLocal(args);
4951
var x = new AllowListSanitizerWithJavaUtilList();
5052
x.testNonStaticFields(args);
53+
testMultipleSources(args);
5154
}
5255

5356
private static void testStaticFields(String[] args) throws IOException, SQLException {
@@ -226,6 +229,57 @@ private static void testLocal(String[] args) throws IOException, SQLException {
226229
ResultSet results = connection.createStatement().executeQuery(query);
227230
}
228231
}
232+
// BAD: an allowlist is used but it may contain a non-compile-time constant element
233+
{
234+
List<String> allowlist = new ArrayList<String>();
235+
allowlist.add("allowed1");
236+
possiblyMutate(allowlist);
237+
if(allowlist.contains(tainted)){
238+
String query = "SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='"
239+
+ tainted + "' ORDER BY PRICE";
240+
ResultSet results = connection.createStatement().executeQuery(query);
241+
}
242+
}
243+
}
244+
245+
private static void testMultipleSources(String[] args) throws IOException, SQLException {
246+
String tainted = args[1];
247+
boolean b = args[2] == "True";
248+
{
249+
// BAD: an allowlist is used which might contain constant strings
250+
List<String> allowlist = new ArrayList<String>();
251+
allowlist.add("allowed1");
252+
if (b) {
253+
allowlist.add(getNonConstantString());
254+
}
255+
if(allowlist.contains(tainted)){
256+
String query = "SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='"
257+
+ tainted + "' ORDER BY PRICE";
258+
ResultSet results = connection.createStatement().executeQuery(query);
259+
}
260+
}
261+
{
262+
// BAD: an allowlist is used which might contain constant strings
263+
List<String> allowlist = b ? goodAllowList1 : badAllowList1;
264+
if(allowlist.contains(tainted)){
265+
String query = "SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='"
266+
+ tainted + "' ORDER BY PRICE";
267+
ResultSet results = connection.createStatement().executeQuery(query);
268+
}
269+
}
270+
{
271+
// BAD: an allowlist is used which might contain constant strings
272+
List<String> allowlist = b ? goodAllowList1 : List.of("allowed1", "allowed2", args[2]);;
273+
if(allowlist.contains(tainted)){
274+
String query = "SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='"
275+
+ tainted + "' ORDER BY PRICE";
276+
ResultSet results = connection.createStatement().executeQuery(query);
277+
}
278+
}
279+
}
280+
281+
private static void possiblyMutate(List list) {
282+
list.add(getNonConstantString());
229283
}
230284

231285
}

0 commit comments

Comments
 (0)