Skip to content

Commit 00983c8

Browse files
authored
Merge pull request github#4965 from artem-smotrakov/jexl-injection
Java: Query for detecting JEXL injections
2 parents 20ccb52 + 7d52b53 commit 00983c8

40 files changed

+1505
-2
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
2+
<qhelp>
3+
4+
<overview>
5+
<p>
6+
Java EXpression Language (JEXL) is a simple expression language
7+
provided by the Apache Commons JEXL library.
8+
The syntax is close to a mix of ECMAScript and shell-script.
9+
The language allows invocation of methods available in the JVM.
10+
If a JEXL expression is built using attacker-controlled data,
11+
and then evaluated, then it may allow the attacker to run arbitrary code.
12+
</p>
13+
</overview>
14+
15+
<recommendation>
16+
<p>
17+
It is generally recommended to avoid using untrusted input in a JEXL expression.
18+
If it is not possible, JEXL expressions should be run in a sandbox that allows accessing only
19+
explicitly allowed classes.
20+
</p>
21+
</recommendation>
22+
23+
<example>
24+
<p>
25+
The following example uses untrusted data to build and run a JEXL expression.
26+
</p>
27+
<sample src="UnsafeJexlExpressionEvaluation.java" />
28+
29+
<p>
30+
The next example shows how an untrusted JEXL expression can be run
31+
in a sandbox that allows accessing only methods in the <code>java.lang.Math</code> class.
32+
The sandbox is implemented using <code>JexlSandbox</code> class that is provided by
33+
Apache Commons JEXL 3.
34+
</p>
35+
<sample src="SaferJexlExpressionEvaluationWithSandbox.java" />
36+
37+
<p>
38+
The next example shows another way how a sandbox can be implemented.
39+
It uses a custom implementation of <code>JexlUberspect</code>
40+
that checks if callees are instances of allowed classes.
41+
</p>
42+
<sample src="SaferJexlExpressionEvaluationWithUberspectSandbox.java" />
43+
</example>
44+
45+
<references>
46+
<li>
47+
Apache Commons JEXL:
48+
<a href="https://commons.apache.org/proper/commons-jexl/">Project page</a>.
49+
</li>
50+
<li>
51+
Apache Commons JEXL documentation:
52+
<a href="https://commons.apache.org/proper/commons-jexl/javadocs/apidocs-2.1.1/">JEXL 2.1.1 API</a>.
53+
</li>
54+
<li>
55+
Apache Commons JEXL documentation:
56+
<a href="https://commons.apache.org/proper/commons-jexl/apidocs/index.html">JEXL 3.1 API</a>.
57+
</li>
58+
<li>
59+
OWASP:
60+
<a href="https://owasp.org/www-community/vulnerabilities/Expression_Language_Injection">Expression Language Injection</a>.
61+
</li>
62+
</references>
63+
</qhelp>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @name Expression language injection (JEXL)
3+
* @description Evaluation of a user-controlled JEXL expression
4+
* may lead to arbitrary code execution.
5+
* @kind path-problem
6+
* @problem.severity error
7+
* @precision high
8+
* @id java/jexl-expression-injection
9+
* @tags security
10+
* external/cwe/cwe-094
11+
*/
12+
13+
import java
14+
import JexlInjectionLib
15+
import DataFlow::PathGraph
16+
17+
from DataFlow::PathNode source, DataFlow::PathNode sink, JexlInjectionConfig conf
18+
where conf.hasFlowPath(source, sink)
19+
select sink.getNode(), source, sink, "JEXL injection from $@.", source.getNode(), "this user input"
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import java
2+
import semmle.code.java.dataflow.FlowSources
3+
import semmle.code.java.dataflow.TaintTracking
4+
5+
/**
6+
* A taint-tracking configuration for unsafe user input
7+
* that is used to construct and evaluate a JEXL expression.
8+
* It supports both JEXL 2 and 3.
9+
*/
10+
class JexlInjectionConfig extends TaintTracking::Configuration {
11+
JexlInjectionConfig() { this = "JexlInjectionConfig" }
12+
13+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
14+
15+
override predicate isSink(DataFlow::Node sink) { sink instanceof JexlEvaluationSink }
16+
17+
override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
18+
any(TaintPropagatingJexlMethodCall c).taintFlow(fromNode, toNode) or
19+
returnsDataFromBean(fromNode, toNode)
20+
}
21+
}
22+
23+
/**
24+
* A sink for Expresssion Language injection vulnerabilities via Jexl,
25+
* i.e. method calls that run evaluation of a JEXL expression.
26+
*
27+
* Creating a `Callable` from a tainted JEXL expression or script is considered as a sink
28+
* although the tainted expression is not executed at this point.
29+
* Here we assume that it will get executed at some point,
30+
* maybe stored in an object field and then reached by a different flow.
31+
*/
32+
private class JexlEvaluationSink extends DataFlow::ExprNode {
33+
JexlEvaluationSink() {
34+
exists(MethodAccess ma, Method m, Expr taintFrom |
35+
ma.getMethod() = m and taintFrom = this.asExpr()
36+
|
37+
m instanceof DirectJexlEvaluationMethod and ma.getQualifier() = taintFrom
38+
or
39+
m instanceof CreateJexlCallableMethod and ma.getQualifier() = taintFrom
40+
or
41+
m instanceof JexlEngineGetSetPropertyMethod and
42+
taintFrom.getType() instanceof TypeString and
43+
ma.getAnArgument() = taintFrom
44+
)
45+
}
46+
}
47+
48+
/**
49+
* Defines method calls that propagate tainted data via one of the methods
50+
* from JEXL library.
51+
*/
52+
private class TaintPropagatingJexlMethodCall extends MethodAccess {
53+
Expr taintFromExpr;
54+
55+
TaintPropagatingJexlMethodCall() {
56+
exists(Method m, RefType taintType |
57+
this.getMethod() = m and
58+
taintType = taintFromExpr.getType()
59+
|
60+
isUnsafeEngine(this.getQualifier()) and
61+
(
62+
m instanceof CreateJexlScriptMethod and
63+
taintFromExpr = this.getArgument(0) and
64+
taintType instanceof TypeString
65+
or
66+
m instanceof CreateJexlExpressionMethod and
67+
taintFromExpr = this.getAnArgument() and
68+
taintType instanceof TypeString
69+
or
70+
m instanceof CreateJexlTemplateMethod and
71+
(taintType instanceof TypeString or taintType instanceof Reader) and
72+
taintFromExpr = this.getArgument([0, 1])
73+
)
74+
)
75+
}
76+
77+
/**
78+
* Holds if `fromNode` to `toNode` is a dataflow step that propagates
79+
* tainted data.
80+
*/
81+
predicate taintFlow(DataFlow::Node fromNode, DataFlow::Node toNode) {
82+
fromNode.asExpr() = taintFromExpr and toNode.asExpr() = this
83+
}
84+
}
85+
86+
/**
87+
* Holds if `expr` is a JEXL engine that is not configured with a sandbox.
88+
*/
89+
private predicate isUnsafeEngine(Expr expr) {
90+
not exists(SandboxedJexlFlowConfig config | config.hasFlowTo(DataFlow::exprNode(expr)))
91+
}
92+
93+
/**
94+
* A configuration for a tracking sandboxed JEXL engines.
95+
*/
96+
private class SandboxedJexlFlowConfig extends DataFlow2::Configuration {
97+
SandboxedJexlFlowConfig() { this = "JexlInjection::SandboxedJexlFlowConfig" }
98+
99+
override predicate isSource(DataFlow::Node node) { node instanceof SandboxedJexlSource }
100+
101+
override predicate isSink(DataFlow::Node node) {
102+
exists(MethodAccess ma, Method m | ma.getMethod() = m |
103+
(
104+
m instanceof CreateJexlScriptMethod or
105+
m instanceof CreateJexlExpressionMethod or
106+
m instanceof CreateJexlTemplateMethod
107+
) and
108+
ma.getQualifier() = node.asExpr()
109+
)
110+
}
111+
112+
override predicate isAdditionalFlowStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
113+
createsJexlEngine(fromNode, toNode)
114+
}
115+
}
116+
117+
/**
118+
* Defines a data flow source for JEXL engines configured with a sandbox.
119+
*/
120+
private class SandboxedJexlSource extends DataFlow::ExprNode {
121+
SandboxedJexlSource() {
122+
exists(MethodAccess ma, Method m | m = ma.getMethod() |
123+
m.getDeclaringType() instanceof JexlBuilder and
124+
m.hasName(["uberspect", "sandbox"]) and
125+
m.getReturnType() instanceof JexlBuilder and
126+
this.asExpr() = [ma, ma.getQualifier()]
127+
)
128+
or
129+
exists(ConstructorCall cc |
130+
cc.getConstructedType() instanceof JexlEngine and
131+
cc.getArgument(0).getType() instanceof JexlUberspect and
132+
cc = this.asExpr()
133+
)
134+
}
135+
}
136+
137+
/**
138+
* Holds if `fromNode` to `toNode` is a dataflow step that creates one of the JEXL engines.
139+
*/
140+
private predicate createsJexlEngine(DataFlow::Node fromNode, DataFlow::Node toNode) {
141+
exists(MethodAccess ma, Method m | m = ma.getMethod() |
142+
(m.getDeclaringType() instanceof JexlBuilder or m.getDeclaringType() instanceof JexlEngine) and
143+
m.hasName(["create", "createJxltEngine"]) and
144+
ma.getQualifier() = fromNode.asExpr() and
145+
ma = toNode.asExpr()
146+
)
147+
or
148+
exists(ConstructorCall cc |
149+
cc.getConstructedType() instanceof UnifiedJexl and
150+
cc.getArgument(0) = fromNode.asExpr() and
151+
cc = toNode.asExpr()
152+
)
153+
}
154+
155+
/**
156+
* Holds if `fromNode` to `toNode` is a dataflow step that returns data from
157+
* a bean by calling one of its getters.
158+
*/
159+
private predicate returnsDataFromBean(DataFlow::Node fromNode, DataFlow::Node toNode) {
160+
exists(MethodAccess ma, Method m | ma.getMethod() = m |
161+
m instanceof GetterMethod and
162+
ma.getQualifier() = fromNode.asExpr() and
163+
ma = toNode.asExpr()
164+
)
165+
}
166+
167+
/**
168+
* A methods in the `JexlEngine` class that gets or sets a property with a JEXL expression.
169+
*/
170+
private class JexlEngineGetSetPropertyMethod extends Method {
171+
JexlEngineGetSetPropertyMethod() {
172+
getDeclaringType() instanceof JexlEngine and
173+
hasName(["getProperty", "setProperty"])
174+
}
175+
}
176+
177+
/**
178+
* A method that triggers direct evaluation of JEXL expressions.
179+
*/
180+
private class DirectJexlEvaluationMethod extends Method {
181+
DirectJexlEvaluationMethod() {
182+
getDeclaringType() instanceof JexlExpression and hasName("evaluate")
183+
or
184+
getDeclaringType() instanceof JexlScript and hasName("execute")
185+
or
186+
getDeclaringType() instanceof JxltEngineExpression and hasName(["evaluate", "prepare"])
187+
or
188+
getDeclaringType() instanceof JxltEngineTemplate and hasName("evaluate")
189+
or
190+
getDeclaringType() instanceof UnifiedJexlExpression and hasName(["evaluate", "prepare"])
191+
or
192+
getDeclaringType() instanceof UnifiedJexlTemplate and hasName("evaluate")
193+
}
194+
}
195+
196+
/**
197+
* A method that creates a JEXL script.
198+
*/
199+
private class CreateJexlScriptMethod extends Method {
200+
CreateJexlScriptMethod() { getDeclaringType() instanceof JexlEngine and hasName("createScript") }
201+
}
202+
203+
/**
204+
* A method that creates a `Callable` for a JEXL expression or script.
205+
*/
206+
private class CreateJexlCallableMethod extends Method {
207+
CreateJexlCallableMethod() {
208+
(getDeclaringType() instanceof JexlExpression or getDeclaringType() instanceof JexlScript) and
209+
hasName("callable")
210+
}
211+
}
212+
213+
/**
214+
* A method that creates a JEXL template.
215+
*/
216+
private class CreateJexlTemplateMethod extends Method {
217+
CreateJexlTemplateMethod() {
218+
(getDeclaringType() instanceof JxltEngine or getDeclaringType() instanceof UnifiedJexl) and
219+
hasName("createTemplate")
220+
}
221+
}
222+
223+
/**
224+
* A method that creates a JEXL expression.
225+
*/
226+
private class CreateJexlExpressionMethod extends Method {
227+
CreateJexlExpressionMethod() {
228+
(getDeclaringType() instanceof JexlEngine or getDeclaringType() instanceof JxltEngine) and
229+
hasName("createExpression")
230+
or
231+
getDeclaringType() instanceof UnifiedJexl and hasName("parse")
232+
}
233+
}
234+
235+
private class JexlRefType extends RefType {
236+
JexlRefType() { getPackage().hasName(["org.apache.commons.jexl2", "org.apache.commons.jexl3"]) }
237+
}
238+
239+
private class JexlExpression extends JexlRefType {
240+
JexlExpression() { hasName(["Expression", "JexlExpression"]) }
241+
}
242+
243+
private class JexlScript extends JexlRefType {
244+
JexlScript() { hasName(["Script", "JexlScript"]) }
245+
}
246+
247+
private class JexlBuilder extends JexlRefType {
248+
JexlBuilder() { hasName("JexlBuilder") }
249+
}
250+
251+
private class JexlEngine extends JexlRefType {
252+
JexlEngine() { hasName("JexlEngine") }
253+
}
254+
255+
private class JxltEngine extends JexlRefType {
256+
JxltEngine() { hasName("JxltEngine") }
257+
}
258+
259+
private class UnifiedJexl extends JexlRefType {
260+
UnifiedJexl() { hasName("UnifiedJEXL") }
261+
}
262+
263+
private class JexlUberspect extends Interface {
264+
JexlUberspect() {
265+
hasQualifiedName("org.apache.commons.jexl2.introspection", "Uberspect") or
266+
hasQualifiedName("org.apache.commons.jexl3.introspection", "JexlUberspect")
267+
}
268+
}
269+
270+
private class JxltEngineExpression extends NestedType {
271+
JxltEngineExpression() { getEnclosingType() instanceof JxltEngine and hasName("Expression") }
272+
}
273+
274+
private class JxltEngineTemplate extends NestedType {
275+
JxltEngineTemplate() { getEnclosingType() instanceof JxltEngine and hasName("Template") }
276+
}
277+
278+
private class UnifiedJexlExpression extends NestedType {
279+
UnifiedJexlExpression() { getEnclosingType() instanceof UnifiedJexl and hasName("Expression") }
280+
}
281+
282+
private class UnifiedJexlTemplate extends NestedType {
283+
UnifiedJexlTemplate() { getEnclosingType() instanceof UnifiedJexl and hasName("Template") }
284+
}
285+
286+
private class Reader extends RefType {
287+
Reader() { hasQualifiedName("java.io", "Reader") }
288+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
public void evaluate(Socket socket) throws IOException {
2+
try (BufferedReader reader = new BufferedReader(
3+
new InputStreamReader(socket.getInputStream()))) {
4+
5+
JexlSandbox onlyMath = new JexlSandbox(false);
6+
onlyMath.white("java.lang.Math");
7+
JexlEngine jexl = new JexlBuilder().sandbox(onlyMath).create();
8+
9+
String input = reader.readLine();
10+
JexlExpression expression = jexl.createExpression(input);
11+
JexlContext context = new MapContext();
12+
expression.evaluate(context);
13+
}
14+
}

0 commit comments

Comments
 (0)