+
+
+
+Jakarta Expression Language (EL) is an expression language for Java applications.
+There is a single language specification and multiple implementations
+such as Glassfish, Juel, Apache Commons EL, etc.
+The language allows invocation of methods available in the JVM.
+If an expression is built using attacker-controlled data,
+and then evaluated, it may allow the attacker to run arbitrary code.
+
+
+
+
+
+It is generally recommended to avoid using untrusted data in an EL expression.
+Before using untrusted data to build an EL expression, the data should be validated
+to ensure it is not evaluated as expression language. If the EL implementation offers
+configuring a sandbox for EL expressions, they should be run in a restrictive sandbox
+that allows accessing only explicitly allowed classes. If the EL implementation
+does not support sandboxing, consider using other expression language implementations
+with sandboxing capabilities such as Apache Commons JEXL or the Spring Expression Language.
+
+
+
+
+
+The following example shows how untrusted data is used to build and run an expression
+using the JUEL interpreter:
+
+
+
+
+JUEL does not support running expressions in a sandbox. To prevent running arbitrary code,
+incoming data has to be checked before including it in an expression. The next example
+uses a Regex pattern to check whether a user tries to run an allowed expression or not:
+
+
+
+
+
+
+
+ Eclipse Foundation:
+ Jakarta Expression Language.
+
+
+ Jakarta EE documentation:
+ Jakarta Expression Language API
+
+
+ OWASP:
+ Expression Language Injection.
+
+
+ JUEL:
+ Home page
+
+
+
diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjection.ql b/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjection.ql
new file mode 100644
index 000000000000..8190ec3d61f1
--- /dev/null
+++ b/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjection.ql
@@ -0,0 +1,20 @@
+/**
+ * @name Jakarta Expression Language injection
+ * @description Evaluation of a user-controlled expression
+ * may lead to arbitrary code execution.
+ * @kind path-problem
+ * @problem.severity error
+ * @precision high
+ * @id java/javaee-expression-injection
+ * @tags security
+ * external/cwe/cwe-094
+ */
+
+import java
+import JakartaExpressionInjectionLib
+import DataFlow::PathGraph
+
+from DataFlow::PathNode source, DataFlow::PathNode sink, JakartaExpressionInjectionConfig conf
+where conf.hasFlowPath(source, sink)
+select sink.getNode(), source, sink, "Jakarta Expression Language injection from $@.",
+ source.getNode(), "this user input"
diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjectionLib.qll b/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjectionLib.qll
new file mode 100644
index 000000000000..430909743647
--- /dev/null
+++ b/java/ql/src/experimental/Security/CWE/CWE-094/JakartaExpressionInjectionLib.qll
@@ -0,0 +1,108 @@
+import java
+import FlowUtils
+import semmle.code.java.dataflow.FlowSources
+import semmle.code.java.dataflow.TaintTracking
+
+/**
+ * A taint-tracking configuration for unsafe user input
+ * that is used to construct and evaluate an expression.
+ */
+class JakartaExpressionInjectionConfig extends TaintTracking::Configuration {
+ JakartaExpressionInjectionConfig() { this = "JakartaExpressionInjectionConfig" }
+
+ override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
+
+ override predicate isSink(DataFlow::Node sink) { sink instanceof ExpressionEvaluationSink }
+
+ override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
+ any(TaintPropagatingCall c).taintFlow(fromNode, toNode) or
+ hasGetterFlow(fromNode, toNode)
+ }
+}
+
+/**
+ * A sink for Expresssion Language injection vulnerabilities,
+ * i.e. method calls that run evaluation of an expression.
+ */
+private class ExpressionEvaluationSink extends DataFlow::ExprNode {
+ ExpressionEvaluationSink() {
+ exists(MethodAccess ma, Method m, Expr taintFrom |
+ ma.getMethod() = m and taintFrom = this.asExpr()
+ |
+ m.getDeclaringType() instanceof ValueExpression and
+ m.hasName(["getValue", "setValue"]) and
+ ma.getQualifier() = taintFrom
+ or
+ m.getDeclaringType() instanceof MethodExpression and
+ m.hasName("invoke") and
+ ma.getQualifier() = taintFrom
+ or
+ m.getDeclaringType() instanceof LambdaExpression and
+ m.hasName("invoke") and
+ ma.getQualifier() = taintFrom
+ or
+ m.getDeclaringType() instanceof ELProcessor and
+ m.hasName(["eval", "getValue", "setValue"]) and
+ ma.getArgument(0) = taintFrom
+ or
+ m.getDeclaringType() instanceof ELProcessor and
+ m.hasName("setVariable") and
+ ma.getArgument(1) = taintFrom
+ )
+ }
+}
+
+/**
+ * Defines method calls that propagate tainted expressions.
+ */
+private class TaintPropagatingCall extends Call {
+ Expr taintFromExpr;
+
+ TaintPropagatingCall() {
+ taintFromExpr = this.getArgument(1) and
+ (
+ exists(Method m | this.(MethodAccess).getMethod() = m |
+ m.getDeclaringType() instanceof ExpressionFactory and
+ m.hasName(["createValueExpression", "createMethodExpression"]) and
+ taintFromExpr.getType() instanceof TypeString
+ )
+ or
+ exists(Constructor c | this.(ConstructorCall).getConstructor() = c |
+ c.getDeclaringType() instanceof LambdaExpression and
+ taintFromExpr.getType() instanceof ValueExpression
+ )
+ )
+ }
+
+ /**
+ * Holds if `fromNode` to `toNode` is a dataflow step that propagates
+ * tainted data.
+ */
+ predicate taintFlow(DataFlow::Node fromNode, DataFlow::Node toNode) {
+ fromNode.asExpr() = taintFromExpr and toNode.asExpr() = this
+ }
+}
+
+private class JakartaType extends RefType {
+ JakartaType() { getPackage().hasName(["javax.el", "jakarta.el"]) }
+}
+
+private class ELProcessor extends JakartaType {
+ ELProcessor() { hasName("ELProcessor") }
+}
+
+private class ExpressionFactory extends JakartaType {
+ ExpressionFactory() { hasName("ExpressionFactory") }
+}
+
+private class ValueExpression extends JakartaType {
+ ValueExpression() { hasName("ValueExpression") }
+}
+
+private class MethodExpression extends JakartaType {
+ MethodExpression() { hasName("MethodExpression") }
+}
+
+private class LambdaExpression extends JakartaType {
+ LambdaExpression() { hasName("LambdaExpression") }
+}
diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/JexlInjectionLib.qll b/java/ql/src/experimental/Security/CWE/CWE-094/JexlInjectionLib.qll
index 561d7e46ae90..89d7cb496a41 100644
--- a/java/ql/src/experimental/Security/CWE/CWE-094/JexlInjectionLib.qll
+++ b/java/ql/src/experimental/Security/CWE/CWE-094/JexlInjectionLib.qll
@@ -1,4 +1,5 @@
import java
+import FlowUtils
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking
@@ -16,7 +17,7 @@ class JexlInjectionConfig extends TaintTracking::Configuration {
override predicate isAdditionalTaintStep(DataFlow::Node fromNode, DataFlow::Node toNode) {
any(TaintPropagatingJexlMethodCall c).taintFlow(fromNode, toNode) or
- returnsDataFromBean(fromNode, toNode)
+ hasGetterFlow(fromNode, toNode)
}
}
@@ -152,18 +153,6 @@ private predicate createsJexlEngine(DataFlow::Node fromNode, DataFlow::Node toNo
)
}
-/**
- * Holds if `fromNode` to `toNode` is a dataflow step that returns data from
- * a bean by calling one of its getters.
- */
-private predicate returnsDataFromBean(DataFlow::Node fromNode, DataFlow::Node toNode) {
- exists(MethodAccess ma, Method m | ma.getMethod() = m |
- m instanceof GetterMethod and
- ma.getQualifier() = fromNode.asExpr() and
- ma = toNode.asExpr()
- )
-}
-
/**
* A methods in the `JexlEngine` class that gets or sets a property with a JEXL expression.
*/
diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/SaferExpressionEvaluationWithJuel.java b/java/ql/src/experimental/Security/CWE/CWE-094/SaferExpressionEvaluationWithJuel.java
new file mode 100644
index 000000000000..3dfaaead68a5
--- /dev/null
+++ b/java/ql/src/experimental/Security/CWE/CWE-094/SaferExpressionEvaluationWithJuel.java
@@ -0,0 +1,10 @@
+String input = getRemoteUserInput();
+String pattern = "(inside|outside)\\.(temperature|humidity)";
+if (!input.matches(pattern)) {
+ throw new IllegalArgumentException("Unexpected expression");
+}
+String expression = "${" + input + "}";
+ExpressionFactory factory = new de.odysseus.el.ExpressionFactoryImpl();
+ValueExpression e = factory.createValueExpression(context, expression, Object.class);
+SimpleContext context = getContext();
+Object result = e.getValue(context);
diff --git a/java/ql/src/experimental/Security/CWE/CWE-094/UnsafeExpressionEvaluationWithJuel.java b/java/ql/src/experimental/Security/CWE/CWE-094/UnsafeExpressionEvaluationWithJuel.java
new file mode 100644
index 000000000000..27afa0fcb497
--- /dev/null
+++ b/java/ql/src/experimental/Security/CWE/CWE-094/UnsafeExpressionEvaluationWithJuel.java
@@ -0,0 +1,5 @@
+String expression = "${" + getRemoteUserInput() + "}";
+ExpressionFactory factory = new de.odysseus.el.ExpressionFactoryImpl();
+ValueExpression e = factory.createValueExpression(context, expression, Object.class);
+SimpleContext context = getContext();
+Object result = e.getValue(context);
\ No newline at end of file
diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/JakartaExpressionInjection.expected b/java/ql/test/experimental/query-tests/security/CWE-094/JakartaExpressionInjection.expected
new file mode 100644
index 000000000000..390871b54cf1
--- /dev/null
+++ b/java/ql/test/experimental/query-tests/security/CWE-094/JakartaExpressionInjection.expected
@@ -0,0 +1,46 @@
+edges
+| JakartaExpressionInjection.java:23:25:23:47 | getInputStream(...) : InputStream | JakartaExpressionInjection.java:25:31:25:40 | expression : String |
+| JakartaExpressionInjection.java:25:31:25:40 | expression : String | JakartaExpressionInjection.java:32:24:32:33 | expression : String |
+| JakartaExpressionInjection.java:25:31:25:40 | expression : String | JakartaExpressionInjection.java:40:24:40:33 | expression : String |
+| JakartaExpressionInjection.java:25:31:25:40 | expression : String | JakartaExpressionInjection.java:48:24:48:33 | expression : String |
+| JakartaExpressionInjection.java:25:31:25:40 | expression : String | JakartaExpressionInjection.java:59:24:59:33 | expression : String |
+| JakartaExpressionInjection.java:25:31:25:40 | expression : String | JakartaExpressionInjection.java:67:24:67:33 | expression : String |
+| JakartaExpressionInjection.java:25:31:25:40 | expression : String | JakartaExpressionInjection.java:75:24:75:33 | expression : String |
+| JakartaExpressionInjection.java:25:31:25:40 | expression : String | JakartaExpressionInjection.java:85:24:85:33 | expression : String |
+| JakartaExpressionInjection.java:25:31:25:40 | expression : String | JakartaExpressionInjection.java:95:24:95:33 | expression : String |
+| JakartaExpressionInjection.java:32:24:32:33 | expression : String | JakartaExpressionInjection.java:34:28:34:37 | expression |
+| JakartaExpressionInjection.java:40:24:40:33 | expression : String | JakartaExpressionInjection.java:42:32:42:41 | expression |
+| JakartaExpressionInjection.java:48:24:48:33 | expression : String | JakartaExpressionInjection.java:53:13:53:28 | lambdaExpression |
+| JakartaExpressionInjection.java:59:24:59:33 | expression : String | JakartaExpressionInjection.java:61:32:61:41 | expression |
+| JakartaExpressionInjection.java:67:24:67:33 | expression : String | JakartaExpressionInjection.java:69:43:69:52 | expression |
+| JakartaExpressionInjection.java:75:24:75:33 | expression : String | JakartaExpressionInjection.java:79:13:79:13 | e |
+| JakartaExpressionInjection.java:85:24:85:33 | expression : String | JakartaExpressionInjection.java:89:13:89:13 | e |
+| JakartaExpressionInjection.java:95:24:95:33 | expression : String | JakartaExpressionInjection.java:99:13:99:13 | e |
+nodes
+| JakartaExpressionInjection.java:23:25:23:47 | getInputStream(...) : InputStream | semmle.label | getInputStream(...) : InputStream |
+| JakartaExpressionInjection.java:25:31:25:40 | expression : String | semmle.label | expression : String |
+| JakartaExpressionInjection.java:32:24:32:33 | expression : String | semmle.label | expression : String |
+| JakartaExpressionInjection.java:34:28:34:37 | expression | semmle.label | expression |
+| JakartaExpressionInjection.java:40:24:40:33 | expression : String | semmle.label | expression : String |
+| JakartaExpressionInjection.java:42:32:42:41 | expression | semmle.label | expression |
+| JakartaExpressionInjection.java:48:24:48:33 | expression : String | semmle.label | expression : String |
+| JakartaExpressionInjection.java:53:13:53:28 | lambdaExpression | semmle.label | lambdaExpression |
+| JakartaExpressionInjection.java:59:24:59:33 | expression : String | semmle.label | expression : String |
+| JakartaExpressionInjection.java:61:32:61:41 | expression | semmle.label | expression |
+| JakartaExpressionInjection.java:67:24:67:33 | expression : String | semmle.label | expression : String |
+| JakartaExpressionInjection.java:69:43:69:52 | expression | semmle.label | expression |
+| JakartaExpressionInjection.java:75:24:75:33 | expression : String | semmle.label | expression : String |
+| JakartaExpressionInjection.java:79:13:79:13 | e | semmle.label | e |
+| JakartaExpressionInjection.java:85:24:85:33 | expression : String | semmle.label | expression : String |
+| JakartaExpressionInjection.java:89:13:89:13 | e | semmle.label | e |
+| JakartaExpressionInjection.java:95:24:95:33 | expression : String | semmle.label | expression : String |
+| JakartaExpressionInjection.java:99:13:99:13 | e | semmle.label | e |
+#select
+| JakartaExpressionInjection.java:34:28:34:37 | expression | JakartaExpressionInjection.java:23:25:23:47 | getInputStream(...) : InputStream | JakartaExpressionInjection.java:34:28:34:37 | expression | Jakarta Expression Language injection from $@. | JakartaExpressionInjection.java:23:25:23:47 | getInputStream(...) | this user input |
+| JakartaExpressionInjection.java:42:32:42:41 | expression | JakartaExpressionInjection.java:23:25:23:47 | getInputStream(...) : InputStream | JakartaExpressionInjection.java:42:32:42:41 | expression | Jakarta Expression Language injection from $@. | JakartaExpressionInjection.java:23:25:23:47 | getInputStream(...) | this user input |
+| JakartaExpressionInjection.java:53:13:53:28 | lambdaExpression | JakartaExpressionInjection.java:23:25:23:47 | getInputStream(...) : InputStream | JakartaExpressionInjection.java:53:13:53:28 | lambdaExpression | Jakarta Expression Language injection from $@. | JakartaExpressionInjection.java:23:25:23:47 | getInputStream(...) | this user input |
+| JakartaExpressionInjection.java:61:32:61:41 | expression | JakartaExpressionInjection.java:23:25:23:47 | getInputStream(...) : InputStream | JakartaExpressionInjection.java:61:32:61:41 | expression | Jakarta Expression Language injection from $@. | JakartaExpressionInjection.java:23:25:23:47 | getInputStream(...) | this user input |
+| JakartaExpressionInjection.java:69:43:69:52 | expression | JakartaExpressionInjection.java:23:25:23:47 | getInputStream(...) : InputStream | JakartaExpressionInjection.java:69:43:69:52 | expression | Jakarta Expression Language injection from $@. | JakartaExpressionInjection.java:23:25:23:47 | getInputStream(...) | this user input |
+| JakartaExpressionInjection.java:79:13:79:13 | e | JakartaExpressionInjection.java:23:25:23:47 | getInputStream(...) : InputStream | JakartaExpressionInjection.java:79:13:79:13 | e | Jakarta Expression Language injection from $@. | JakartaExpressionInjection.java:23:25:23:47 | getInputStream(...) | this user input |
+| JakartaExpressionInjection.java:89:13:89:13 | e | JakartaExpressionInjection.java:23:25:23:47 | getInputStream(...) : InputStream | JakartaExpressionInjection.java:89:13:89:13 | e | Jakarta Expression Language injection from $@. | JakartaExpressionInjection.java:23:25:23:47 | getInputStream(...) | this user input |
+| JakartaExpressionInjection.java:99:13:99:13 | e | JakartaExpressionInjection.java:23:25:23:47 | getInputStream(...) : InputStream | JakartaExpressionInjection.java:99:13:99:13 | e | Jakarta Expression Language injection from $@. | JakartaExpressionInjection.java:23:25:23:47 | getInputStream(...) | this user input |
diff --git a/java/ql/test/experimental/query-tests/security/CWE-094/JakartaExpressionInjection.java b/java/ql/test/experimental/query-tests/security/CWE-094/JakartaExpressionInjection.java
new file mode 100644
index 000000000000..ae5b6a8d5e41
--- /dev/null
+++ b/java/ql/test/experimental/query-tests/security/CWE-094/JakartaExpressionInjection.java
@@ -0,0 +1,103 @@
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.function.Consumer;
+
+import javax.el.ELContext;
+import javax.el.ELManager;
+import javax.el.ELProcessor;
+import javax.el.ExpressionFactory;
+import javax.el.LambdaExpression;
+import javax.el.MethodExpression;
+import javax.el.StandardELContext;
+import javax.el.ValueExpression;
+
+public class JakartaExpressionInjection {
+
+ // calls a consumer with a string received from a socket
+ private static void testWithSocket(Consumer