Skip to content

Commit 6bfc0af

Browse files
Java: Improve the ExecTainted query
1 parent 5079deb commit 6bfc0af

File tree

3 files changed

+221
-2
lines changed

3 files changed

+221
-2
lines changed

java/ql/src/Security/CWE/CWE-078/ExecCommon.qll

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import semmle.code.java.dataflow.FlowSources
22
import semmle.code.java.security.ExternalProcess
3+
import semmle.code.java.security.CommandArguments
34

45
private class RemoteUserInputToArgumentToExecFlowConfig extends TaintTracking::Configuration {
56
RemoteUserInputToArgumentToExecFlowConfig() {
@@ -11,7 +12,11 @@ private class RemoteUserInputToArgumentToExecFlowConfig extends TaintTracking::C
1112
override predicate isSink(DataFlow::Node sink) { sink.asExpr() instanceof ArgumentToExec }
1213

1314
override predicate isSanitizer(DataFlow::Node node) {
14-
node.getType() instanceof PrimitiveType or node.getType() instanceof BoxedType
15+
node.getType() instanceof PrimitiveType
16+
or
17+
node.getType() instanceof BoxedType
18+
or
19+
isSafeCommandArgument(node.asExpr())
1520
}
1621
}
1722

java/ql/src/Security/CWE/CWE-078/ExecTainted.ql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import semmle.code.java.security.ExternalProcess
1717
import ExecCommon
1818
import DataFlow::PathGraph
1919

20-
from DataFlow::PathNode source, DataFlow::PathNode sink, StringArgumentToExec execArg
20+
from DataFlow::PathNode source, DataFlow::PathNode sink, ArgumentToExec execArg
2121
where execTainted(source, sink, execArg)
2222
select execArg, source, sink, "$@ flows to here and is used in a command.", source.getNode(),
2323
"User-provided value"
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import java
2+
import semmle.code.java.dataflow.SSA
3+
import semmle.code.java.Collections
4+
5+
/**
6+
* Holds if `ex` is used safely as an argument to a command;
7+
* i.e. it's not in the first position and it's not a shell command.
8+
*/
9+
predicate isSafeCommandArgument(Expr ex) {
10+
exists(ArrayInit ai, int i |
11+
ex = ai.getInit(i) and
12+
i > 0 and
13+
not isShell(ai.getInit(0))
14+
)
15+
or
16+
exists(CommandArgumentList cal |
17+
cal.isNotShell() and
18+
ex = cal.getASubsequentAdd().getArgument(0)
19+
)
20+
or
21+
exists(CommandArgumentArray caa |
22+
caa.isNotShell() and
23+
ex = caa.getAWrite(any(int i | i > 0))
24+
)
25+
}
26+
27+
/**
28+
* Holds if the given expression is the name of a shell command such as bash or python
29+
*/
30+
private predicate isShell(Expr ex) {
31+
exists(string cmd | cmd = ex.(StringLiteral).getValue() |
32+
cmd.regexpMatch(".*(sh|javac?|python[23]?|osascript|cmd)(\\.exe)?$")
33+
)
34+
or
35+
exists(SsaVariable ssa |
36+
ex = ssa.getAUse() and
37+
isShell(ssa.getAnUltimateDefinition().(SsaExplicitUpdate).getDefiningExpr())
38+
)
39+
or
40+
isShell(ex.(Assignment).getRhs())
41+
or
42+
isShell(ex.(LocalVariableDeclExpr).getInit())
43+
}
44+
45+
/**
46+
* A type that could be a list of strings. Includes raw `List` types.
47+
*/
48+
private class ListOfStringType extends CollectionType {
49+
ListOfStringType() {
50+
this.getSourceDeclaration().getASourceSupertype*().hasQualifiedName("java.util", "List") and
51+
this.getElementType().getASubtype*() instanceof TypeString
52+
}
53+
}
54+
55+
/**
56+
* A variable that could be used as a list of arguments to a command.
57+
*/
58+
private class CommandArgumentList extends SsaExplicitUpdate {
59+
CommandArgumentList() {
60+
this.getSourceVariable().getType() instanceof ListOfStringType and
61+
forex(CollectionMutation ma | ma.getQualifier() = this.getAUse() |
62+
ma.getMethod().getName().matches("add%")
63+
)
64+
}
65+
66+
/** Gets a use of the veriable for which the list could be empty. */
67+
private RValue getAUseBeforeFirstAdd() {
68+
result = getAFirstUse()
69+
or
70+
exists(RValue mid |
71+
mid = getAUseBeforeFirstAdd() and
72+
adjacentUseUse(mid, result) and
73+
not exists(MethodAccess ma |
74+
mid = ma.getQualifier() and
75+
ma.getMethod().hasName("add")
76+
)
77+
)
78+
}
79+
80+
/**
81+
* Gets an addition to this list, i.e. a call to an `add` or `addAll` method.
82+
*/
83+
MethodAccess getAnAdd() {
84+
result.getQualifier() = getAUse() and
85+
result.getMethod().getName().matches("add%")
86+
}
87+
88+
/** Gets an addition to this list which could be its first element. */
89+
MethodAccess getAFirstAdd() {
90+
result = getAnAdd() and
91+
result.getQualifier() = getAUseBeforeFirstAdd()
92+
}
93+
94+
/** Gets an addition to this list which is not the first element. */
95+
MethodAccess getASubsequentAdd() {
96+
result = getAnAdd() and
97+
not result = getAFirstAdd()
98+
}
99+
100+
/** Holds if the first element of this list isn't a shell command. */
101+
predicate isNotShell() {
102+
forex(MethodAccess ma | ma = getAFirstAdd() | not isShell(ma.getArgument(0)))
103+
}
104+
}
105+
106+
/**
107+
* The type `String[]`.
108+
*/
109+
private class ArrayOfStringType extends Array {
110+
ArrayOfStringType() { this.getElementType() instanceof TypeString }
111+
}
112+
113+
private predicate arrayLValue(ArrayAccess acc) { exists(Assignment a | a.getDest() = acc) }
114+
115+
/**
116+
* A variable that could be an array of arguments to a command.
117+
*/
118+
private class CommandArgumentArray extends SsaExplicitUpdate {
119+
CommandArgumentArray() {
120+
this.getSourceVariable().getType() instanceof ArrayOfStringType and
121+
forall(ArrayAccess a | a.getArray() = getAUse() and arrayLValue(a) |
122+
a.getIndexExpr() instanceof CompileTimeConstantExpr
123+
)
124+
}
125+
126+
/** Gets an expression that is written to the given index of this array at he given use. */
127+
Expr getAWrite(int index, RValue use) {
128+
exists(Assignment a, ArrayAccess acc |
129+
acc.getArray() = use and
130+
use = this.getAUse() and
131+
index = acc.getIndexExpr().(CompileTimeConstantExpr).getIntValue() and
132+
acc = a.getDest() and
133+
result = a.getRhs()
134+
)
135+
}
136+
137+
/** Gets an expression that is written to the given index of this array. */
138+
Expr getAWrite(int index) { result = getAWrite(index, _) }
139+
140+
predicate isNotShell() {
141+
exists(Expr e | e = this.(CommandArgArrayImmutableFirst).getFirstElement() |
142+
not isShell(e)
143+
)
144+
}
145+
}
146+
147+
/**
148+
* A `CommandArgArray` whose element at index 0 is never written to, except possibly once to initialise it.
149+
*/
150+
private class CommandArgArrayImmutableFirst extends CommandArgumentArray {
151+
CommandArgArrayImmutableFirst() {
152+
this.getDefiningExpr() instanceof ImmutableFirstArrayExpr and
153+
forall(RValue use | exists(this.getAWrite(0, use)) | use = this.getAFirstUse())
154+
}
155+
156+
/** Gets the first element of this array. */
157+
Expr getFirstElement() {
158+
result = getAWrite(0)
159+
or
160+
not exists(getAWrite(0)) and
161+
result = getDefiningExpr().(ImmutableFirstArrayExpr).getFirstElement()
162+
}
163+
}
164+
165+
/**
166+
* An expression that evaluates to an array of strings whose first element is immutable.
167+
*/
168+
private class ImmutableFirstArrayExpr extends Expr {
169+
ImmutableFirstArrayExpr() {
170+
this.getType() instanceof ArrayOfStringType and
171+
(
172+
this.(Assignment).getRhs() instanceof ImmutableFirstArrayExpr
173+
or
174+
this.(LocalVariableDeclExpr).getInit() instanceof ImmutableFirstArrayExpr
175+
or
176+
this instanceof ArrayInit
177+
or
178+
this instanceof ArrayCreationExpr
179+
or
180+
this.(RValue) = any(CommandArgArrayImmutableFirst caa).getAUse()
181+
or
182+
exists(MethodAccess ma, Method m |
183+
ma.getMethod() = m and
184+
m.getDeclaringType().hasQualifiedName("java.util", "Arrays") and
185+
m.hasName("copyOf") and
186+
ma.getArgument(0) instanceof ImmutableFirstArrayExpr
187+
)
188+
or
189+
exists(Field f | this = f.getAnAccess() and
190+
f.isFinal() and
191+
f.getInitializer() instanceof ImmutableFirstArrayExpr
192+
)
193+
)
194+
}
195+
196+
/** Gets the first element of this array. */
197+
Expr getFirstElement() {
198+
result = this.(Assignment).getRhs().(ImmutableFirstArrayExpr).getFirstElement()
199+
or
200+
result = this.(LocalVariableDeclExpr).getInit().(ImmutableFirstArrayExpr).getFirstElement()
201+
or
202+
exists(CommandArgArrayImmutableFirst caa | this = caa.getAUse() |
203+
result = caa.getFirstElement()
204+
)
205+
or
206+
result = this.(MethodAccess).getArgument(0).(ImmutableFirstArrayExpr).getFirstElement()
207+
or
208+
result = this.(FieldAccess).getField().getInitializer()
209+
or
210+
result = this.(ArrayInit).getInit(0)
211+
or
212+
result = this.(ArrayCreationExpr).getInit().getInit(0)
213+
}
214+
}

0 commit comments

Comments
 (0)