Skip to content

Commit c36e621

Browse files
authored
Merge pull request github#3288 from ggolawski/jndi-injection
CodeQL query to detect JNDI injections
2 parents 0c081a8 + 73e736b commit c36e621

File tree

32 files changed

+948
-0
lines changed

32 files changed

+948
-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: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
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 semmle.code.java.frameworks.SpringLdap
7+
import experimental.semmle.code.java.frameworks.Shiro
8+
9+
/**
10+
* A taint-tracking configuration for unvalidated user input that is used in JNDI lookup.
11+
*/
12+
class JndiInjectionFlowConfig extends TaintTracking::Configuration {
13+
JndiInjectionFlowConfig() { this = "JndiInjectionFlowConfig" }
14+
15+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
16+
17+
override predicate isSink(DataFlow::Node sink) { sink instanceof JndiInjectionSink }
18+
19+
override predicate isSanitizer(DataFlow::Node node) {
20+
node.getType() instanceof PrimitiveType or node.getType() instanceof BoxedType
21+
}
22+
23+
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
24+
nameStep(node1, node2) or
25+
jmxServiceUrlStep(node1, node2) or
26+
jmxConnectorStep(node1, node2) or
27+
rmiConnectorStep(node1, node2)
28+
}
29+
}
30+
31+
/** The class `java.util.Hashtable`. */
32+
class TypeHashtable extends Class {
33+
TypeHashtable() { this.getSourceDeclaration().hasQualifiedName("java.util", "Hashtable") }
34+
}
35+
36+
/** The class `javax.naming.directory.SearchControls`. */
37+
class TypeSearchControls extends Class {
38+
TypeSearchControls() { this.hasQualifiedName("javax.naming.directory", "SearchControls") }
39+
}
40+
41+
/**
42+
* The interface `org.springframework.ldap.core.LdapOperations` (spring-ldap 1.2.x and newer) or
43+
* `org.springframework.ldap.LdapOperations` (spring-ldap 1.1.x).
44+
*/
45+
class TypeSpringLdapOperations extends Interface {
46+
TypeSpringLdapOperations() {
47+
this.hasQualifiedName("org.springframework.ldap.core", "LdapOperations") or
48+
this.hasQualifiedName("org.springframework.ldap", "LdapOperations")
49+
}
50+
}
51+
52+
/**
53+
* The interface `org.springframework.ldap.core.ContextMapper` (spring-ldap 1.2.x and newer) or
54+
* `org.springframework.ldap.ContextMapper` (spring-ldap 1.1.x).
55+
*/
56+
class TypeSpringContextMapper extends Interface {
57+
TypeSpringContextMapper() {
58+
this.getSourceDeclaration().hasQualifiedName("org.springframework.ldap.core", "ContextMapper") or
59+
this.getSourceDeclaration().hasQualifiedName("org.springframework.ldap", "ContextMapper")
60+
}
61+
}
62+
63+
/** The interface `javax.management.remote.JMXConnector`. */
64+
class TypeJMXConnector extends Interface {
65+
TypeJMXConnector() { this.hasQualifiedName("javax.management.remote", "JMXConnector") }
66+
}
67+
68+
/** The class `javax.management.remote.rmi.RMIConnector`. */
69+
class TypeRMIConnector extends Class {
70+
TypeRMIConnector() { this.hasQualifiedName("javax.management.remote.rmi", "RMIConnector") }
71+
}
72+
73+
/** The class `javax.management.remote.JMXConnectorFactory`. */
74+
class TypeJMXConnectorFactory extends Class {
75+
TypeJMXConnectorFactory() {
76+
this.hasQualifiedName("javax.management.remote", "JMXConnectorFactory")
77+
}
78+
}
79+
80+
/** The class `javax.management.remote.JMXServiceURL`. */
81+
class TypeJMXServiceURL extends Class {
82+
TypeJMXServiceURL() { this.hasQualifiedName("javax.management.remote", "JMXServiceURL") }
83+
}
84+
85+
/** The interface `javax.naming.Context`. */
86+
class TypeNamingContext extends Interface {
87+
TypeNamingContext() { this.hasQualifiedName("javax.naming", "Context") }
88+
}
89+
90+
/**
91+
* JNDI sink for JNDI injection vulnerabilities, i.e. 1st argument to `lookup`, `lookupLink`,
92+
* `doLookup`, `rename`, `list` or `listBindings` method from `InitialContext`.
93+
*/
94+
predicate jndiSinkMethod(Method m, int index) {
95+
m.getDeclaringType().getAnAncestor() instanceof TypeInitialContext and
96+
(
97+
m.hasName("lookup") or
98+
m.hasName("lookupLink") or
99+
m.hasName("doLookup") or
100+
m.hasName("rename") or
101+
m.hasName("list") or
102+
m.hasName("listBindings")
103+
) and
104+
index = 0
105+
}
106+
107+
/**
108+
* Spring sink for JNDI injection vulnerabilities, i.e. 1st argument to `lookup` method from
109+
* Spring's `JndiTemplate`.
110+
*/
111+
predicate springJndiTemplateSinkMethod(Method m, int index) {
112+
m.getDeclaringType() instanceof TypeSpringJndiTemplate and
113+
m.hasName("lookup") and
114+
index = 0
115+
}
116+
117+
/**
118+
* Spring sink for JNDI injection vulnerabilities, i.e. 1st argument to `lookup`, `lookupContext`,
119+
* `findByDn`, `rename`, `list`, `listBindings`, `unbind`, `search` or `searchForObject` method
120+
* from Spring's `LdapOperations`.
121+
*/
122+
predicate springLdapTemplateSinkMethod(MethodAccess ma, Method m, int index) {
123+
m.getDeclaringType().getAnAncestor() instanceof TypeSpringLdapOperations and
124+
(
125+
m.hasName("lookup")
126+
or
127+
m.hasName("lookupContext")
128+
or
129+
m.hasName("findByDn")
130+
or
131+
m.hasName("rename")
132+
or
133+
m.hasName("list")
134+
or
135+
m.hasName("listBindings")
136+
or
137+
m.hasName("unbind") and ma.getArgument(1).(CompileTimeConstantExpr).getBooleanValue() = true
138+
or
139+
m.getName().matches("search%") and
140+
m.getParameterType(m.getNumberOfParameters() - 1) instanceof TypeSpringContextMapper and
141+
not m.getAParamType() instanceof TypeSearchControls
142+
or
143+
m.hasName("search") and ma.getArgument(3).(CompileTimeConstantExpr).getBooleanValue() = true
144+
) and
145+
index = 0
146+
}
147+
148+
/**
149+
* Apache Shiro sink for JNDI injection vulnerabilities, i.e. 1st argument to `lookup` method from
150+
* Shiro's `JndiTemplate`.
151+
*/
152+
predicate shiroSinkMethod(Method m, int index) {
153+
m.getDeclaringType() instanceof TypeShiroJndiTemplate and
154+
m.hasName("lookup") and
155+
index = 0
156+
}
157+
158+
/**
159+
* `JMXConnectorFactory` sink for JNDI injection vulnerabilities, i.e. 1st argument to `connect`
160+
* method from `JMXConnectorFactory`.
161+
*/
162+
predicate jmxConnectorFactorySinkMethod(Method m, int index) {
163+
m.getDeclaringType() instanceof TypeJMXConnectorFactory and
164+
m.hasName("connect") and
165+
index = 0
166+
}
167+
168+
/**
169+
* Tainted value passed to env `Hashtable` as the provider URL, i.e.
170+
* `env.put(Context.PROVIDER_URL, tainted)` or `env.setProperty(Context.PROVIDER_URL, tainted)`.
171+
*/
172+
predicate providerUrlEnv(MethodAccess ma, Method m, int index) {
173+
m.getDeclaringType().getAnAncestor() instanceof TypeHashtable and
174+
(m.hasName("put") or m.hasName("setProperty")) and
175+
(
176+
ma.getArgument(0).(CompileTimeConstantExpr).getStringValue() = "java.naming.provider.url"
177+
or
178+
exists(Field f |
179+
ma.getArgument(0) = f.getAnAccess() and
180+
f.hasName("PROVIDER_URL") and
181+
f.getDeclaringType() instanceof TypeNamingContext
182+
)
183+
) and
184+
index = 1
185+
}
186+
187+
/** Holds if parameter at index `index` in method `m` is JNDI injection sink. */
188+
predicate jndiInjectionSinkMethod(MethodAccess ma, Method m, int index) {
189+
jndiSinkMethod(m, index) or
190+
springJndiTemplateSinkMethod(m, index) or
191+
springLdapTemplateSinkMethod(ma, m, index) or
192+
shiroSinkMethod(m, index) or
193+
jmxConnectorFactorySinkMethod(m, index) or
194+
providerUrlEnv(ma, m, index)
195+
}
196+
197+
/** A data flow sink for unvalidated user input that is used in JNDI lookup. */
198+
class JndiInjectionSink extends DataFlow::ExprNode {
199+
JndiInjectionSink() {
200+
exists(MethodAccess ma, Method m, int index |
201+
ma.getMethod() = m and
202+
ma.getArgument(index) = this.getExpr() and
203+
jndiInjectionSinkMethod(ma, m, index)
204+
)
205+
or
206+
exists(MethodAccess ma, Method m |
207+
ma.getMethod() = m and
208+
ma.getQualifier() = this.getExpr() and
209+
m.getDeclaringType().getAnAncestor() instanceof TypeJMXConnector and
210+
m.hasName("connect")
211+
)
212+
}
213+
}
214+
215+
/**
216+
* Holds if `n1` to `n2` is a dataflow step that converts between `String` and `CompositeName` or
217+
* `CompoundName`, i.e. `new CompositeName(tainted)` or `new CompoundName(tainted)`.
218+
*/
219+
predicate nameStep(ExprNode n1, ExprNode n2) {
220+
exists(ConstructorCall cc |
221+
cc.getConstructedType() instanceof TypeCompositeName or
222+
cc.getConstructedType() instanceof TypeCompoundName
223+
|
224+
n1.asExpr() = cc.getAnArgument() and
225+
n2.asExpr() = cc
226+
)
227+
}
228+
229+
/**
230+
* Holds if `n1` to `n2` is a dataflow step that converts between `String` and `JMXServiceURL`,
231+
* i.e. `new JMXServiceURL(tainted)`.
232+
*/
233+
predicate jmxServiceUrlStep(ExprNode n1, ExprNode n2) {
234+
exists(ConstructorCall cc | cc.getConstructedType() instanceof TypeJMXServiceURL |
235+
n1.asExpr() = cc.getAnArgument() and
236+
n2.asExpr() = cc
237+
)
238+
}
239+
240+
/**
241+
* Holds if `n1` to `n2` is a dataflow step that converts between `JMXServiceURL` and
242+
* `JMXConnector`, i.e. `JMXConnectorFactory.newJMXConnector(tainted)`.
243+
*/
244+
predicate jmxConnectorStep(ExprNode n1, ExprNode n2) {
245+
exists(MethodAccess ma, Method m | n1.asExpr() = ma.getArgument(0) and n2.asExpr() = ma |
246+
ma.getMethod() = m and
247+
m.getDeclaringType() instanceof TypeJMXConnectorFactory and
248+
m.hasName("newJMXConnector")
249+
)
250+
}
251+
252+
/**
253+
* Holds if `n1` to `n2` is a dataflow step that converts between `JMXServiceURL` and
254+
* `RMIConnector`, i.e. `new RMIConnector(tainted)`.
255+
*/
256+
predicate rmiConnectorStep(ExprNode n1, ExprNode n2) {
257+
exists(ConstructorCall cc | cc.getConstructedType() instanceof TypeRMIConnector |
258+
n1.asExpr() = cc.getAnArgument() and
259+
n2.asExpr() = cc
260+
)
261+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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+
}
12+
13+
/** The class `javax.naming.CompoundName`. */
14+
class TypeCompoundName extends Class {
15+
TypeCompoundName() { this.hasQualifiedName("javax.naming", "CompoundName") }
16+
}
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+
}

0 commit comments

Comments
 (0)