Skip to content

Commit af48bc3

Browse files
committed
CodeQL query to detect JNDI injections
1 parent 86ba03b commit af48bc3

File tree

16 files changed

+431
-0
lines changed

16 files changed

+431
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import javax.naming.Context;
2+
import javax.naming.InitialContext;
3+
4+
public void jndiLookup(HttpServletRequest request) throws NamingException {
5+
String name = request.getParameter("name");
6+
7+
Hashtable<String, String> env = new Hashtable<String, String>();
8+
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
9+
env.put(Context.PROVIDER_URL, "rmi://trusted-server:1099");
10+
InitialContext ctx = new InitialContext(env);
11+
12+
// BAD: User input used in lookup
13+
ctx.lookup(name);
14+
15+
// GOOD: The name is validated before being used in lookup
16+
if (isValid(name)) {
17+
ctx.lookup(name);
18+
} else {
19+
// Reject the request
20+
}
21+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>The Java Naming and Directory Interface (JNDI) is a Java API for a directory service that allows
7+
Java software clients to discover and look up data and resources (in the form of Java objects) via
8+
a name. If the name being used to look up the data is controlled by the user, it can point to a
9+
malicious server, which can return an arbitrary object. In the worst case, this can allow remote
10+
code execution.</p>
11+
</overview>
12+
13+
<recommendation>
14+
<p>The general recommendation is to not pass untrusted data to the <code>InitialContext.lookup
15+
</code> method. If the name being used to look up the object must be provided by the user, make
16+
sure that it's not in the form of an absolute URL or that it's the URL pointing to a trused server.
17+
</p>
18+
</recommendation>
19+
20+
<example>
21+
<p>In the following examples, the code accepts a name from the user, which it uses to look up an
22+
object.</p>
23+
24+
<p>In the first example, the user provided name is used to look up an object.</p>
25+
26+
<p>The second example validates the name before using it to look up an object.</p>
27+
28+
<sample src="JndiInjection.java" />
29+
</example>
30+
31+
<references>
32+
<li>Oracle: <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jndi/">Java Naming and Directory Interface (JNDI)</a>.</li>
33+
<li>Black Hat materials: <a href="https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE-wp.pdf">A Journey from JNDI/LDAP Manipulation to Remote Code Execution Dream Land</a>.</li>
34+
<li>Veracode: <a href="https://www.veracode.com/blog/research/exploiting-jndi-injections-java">Exploiting JNDI Injections in Java</a>.</li>
35+
</references>
36+
</qhelp>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* @name JNDI lookup with user-controlled name
3+
* @description Doing a JNDI lookup with user-controlled name can lead to download an untrusted
4+
* object and to execution of arbitrary code.
5+
* @kind path-problem
6+
* @problem.severity error
7+
* @precision high
8+
* @id java/jndi-injection
9+
* @tags security
10+
* external/cwe/cwe-074
11+
*/
12+
13+
import java
14+
import semmle.code.java.dataflow.FlowSources
15+
import JndiInjectionLib
16+
import DataFlow::PathGraph
17+
18+
from DataFlow::PathNode source, DataFlow::PathNode sink, JndiInjectionFlowConfig conf
19+
where conf.hasFlowPath(source, sink)
20+
select sink.getNode(), source, sink, "JNDI lookup might include name from $@.", source.getNode(),
21+
"this user input"
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import java
2+
import semmle.code.java.dataflow.FlowSources
3+
import DataFlow
4+
import experimental.semmle.code.java.frameworks.Jndi
5+
import experimental.semmle.code.java.frameworks.spring.SpringJndi
6+
import experimental.semmle.code.java.frameworks.Shiro
7+
8+
/**
9+
* A taint-tracking configuration for unvalidated user input that is used in JNDI lookup.
10+
*/
11+
class JndiInjectionFlowConfig extends TaintTracking::Configuration {
12+
JndiInjectionFlowConfig() { this = "JndiInjectionFlowConfig" }
13+
14+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
15+
16+
override predicate isSink(DataFlow::Node sink) { sink instanceof JndiInjectionSink }
17+
18+
override predicate isSanitizer(DataFlow::Node node) {
19+
node.getType() instanceof PrimitiveType or node.getType() instanceof BoxedType
20+
}
21+
22+
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
23+
compositeNameStep(node1, node2)
24+
}
25+
}
26+
27+
/**
28+
* JNDI sink for JNDI injection vulnerabilities, i.e. 1st argument to `lookup`, `lookupLink`,
29+
* `doLookup`, `rename`, `list` or `listBindings` method from `InitialContext`.
30+
*/
31+
predicate jndiSinkMethod(Method m, int index) {
32+
m.getDeclaringType().getAnAncestor() instanceof TypeInitialContext and
33+
(
34+
m.hasName("lookup") or
35+
m.hasName("lookupLink") or
36+
m.hasName("doLookup") or
37+
m.hasName("rename") or
38+
m.hasName("list") or
39+
m.hasName("listBindings")
40+
) and
41+
index = 0
42+
}
43+
44+
/**
45+
* Spring sink for JNDI injection vulnerabilities, i.e. 1st argument to `lookup` method from
46+
* Spring's `JndiTemplate`.
47+
*/
48+
predicate springSinkMethod(Method m, int index) {
49+
m.getDeclaringType() instanceof TypeSpringJndiTemplate and
50+
m.hasName("lookup") and
51+
index = 0
52+
}
53+
54+
/**
55+
* Apache Shiro sink for JNDI injection vulnerabilities, i.e. 1st argument to `lookup` method from
56+
* Shiro's `JndiTemplate`.
57+
*/
58+
predicate shiroSinkMethod(Method m, int index) {
59+
m.getDeclaringType() instanceof TypeShiroJndiTemplate and
60+
m.hasName("lookup") and
61+
index = 0
62+
}
63+
64+
/** Holds if parameter at index `index` in method `m` is JNDI injection sink. */
65+
predicate jndiInjectionSinkMethod(Method m, int index) {
66+
jndiSinkMethod(m, index) or
67+
springSinkMethod(m, index) or
68+
shiroSinkMethod(m, index)
69+
}
70+
71+
/** A data flow sink for unvalidated user input that is used in JNDI lookup. */
72+
class JndiInjectionSink extends DataFlow::ExprNode {
73+
JndiInjectionSink() {
74+
exists(MethodAccess ma, Method m, int index |
75+
ma.getMethod() = m and
76+
ma.getArgument(index) = this.getExpr() and
77+
jndiInjectionSinkMethod(m, index)
78+
)
79+
}
80+
}
81+
82+
/**
83+
* Holds if `n1` to `n2` is a dataflow step that converts between `String` and `CompositeName`,
84+
* i.e. `new CompositeName(tainted)`.
85+
*/
86+
predicate compositeNameStep(ExprNode n1, ExprNode n2) {
87+
exists(ConstructorCall cc | cc.getConstructedType() instanceof TypeCompositeName |
88+
n1.asExpr() = cc.getAnArgument() and
89+
n2.asExpr() = cc
90+
)
91+
}

java/ql/src/experimental/qlpack.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
name: codeql-java-experimental
2+
version: 0.0.0
3+
libraryPathDependencies: codeql-java
4+
extractor: java
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import java
2+
3+
/** The class `javax.naming.InitialContext`. */
4+
class TypeInitialContext extends Class {
5+
TypeInitialContext() { this.hasQualifiedName("javax.naming", "InitialContext") }
6+
}
7+
8+
/** The class `javax.naming.CompositeName`. */
9+
class TypeCompositeName extends Class {
10+
TypeCompositeName() { this.hasQualifiedName("javax.naming", "CompositeName") }
11+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import java
2+
3+
/** The class `org.apache.shiro.jndi.JndiTemplate`. */
4+
class TypeShiroJndiTemplate extends Class {
5+
TypeShiroJndiTemplate() { this.hasQualifiedName("org.apache.shiro.jndi", "JndiTemplate") }
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import java
2+
3+
/** The class `org.springframework.jndi.JndiTemplate`. */
4+
class TypeSpringJndiTemplate extends Class {
5+
TypeSpringJndiTemplate() { this.hasQualifiedName("org.springframework.jndi", "JndiTemplate") }
6+
}

java/ql/test/experimental/qlpack.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
name: codeql-java-experimental-tests
2+
version: 0.0.0
3+
libraryPathDependencies: codeql-java-experimental
4+
extractor: java

0 commit comments

Comments
 (0)