Skip to content

Commit 7570fa9

Browse files
committed
Query to detect LDAP injections in Java
JNDI and UnboundID sinks JNDI, UnboundID and Spring LDAP sanitizers
1 parent 367d13c commit 7570fa9

2 files changed

Lines changed: 261 additions & 0 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* @name LDAP query built from user-controlled sources
3+
* @description Building an LDAP query from user-controlled sources is vulnerable to insertion of
4+
* malicious LDAP code by the user.
5+
* @kind path-problem
6+
* @problem.severity error
7+
* @precision high
8+
* @id java/ldap-injection
9+
* @tags security
10+
* external/cwe/cwe-090
11+
*/
12+
13+
import semmle.code.java.Expr
14+
import semmle.code.java.dataflow.FlowSources
15+
import LdapInjectionLib
16+
import DataFlow::PathGraph
17+
18+
from
19+
DataFlow::PathNode source, DataFlow::PathNode sink, LdapInjectionFlowConfig conf
20+
where conf.hasFlowPath(source, sink)
21+
// select sink.getNode(), source, sink, "LDAP query might include code from $@.", source.getNode(),
22+
// "this user input",
23+
select source, sink, sink.getNode().getEnclosingCallable().getName(), sink.getNode().getLocation().getStartLine()
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import java
2+
import semmle.code.java.dataflow.FlowSources
3+
import DataFlow
4+
5+
/** The class `com.unboundid.ldap.sdk.SearchRequest`. */
6+
class TypeSearchRequest extends Class {
7+
TypeSearchRequest() { this.hasQualifiedName("com.unboundid.ldap.sdk", "SearchRequest") }
8+
}
9+
10+
/** The class `com.unboundid.ldap.sdk.ReadOnlySearchRequest`. */
11+
class TypeReadOnlySearchRequest extends Interface {
12+
TypeReadOnlySearchRequest() {
13+
this.hasQualifiedName("com.unboundid.ldap.sdk", "ReadOnlySearchRequest")
14+
}
15+
}
16+
17+
/** The class `com.unboundid.ldap.sdk.Filter`. */
18+
class TypeFilter extends Class {
19+
TypeFilter() { this.hasQualifiedName("com.unboundid.ldap.sdk", "Filter") }
20+
}
21+
22+
/** The class `com.unboundid.ldap.sdk.LDAPConnection`. */
23+
class TypeLDAPConnection extends Class {
24+
TypeLDAPConnection() { this.hasQualifiedName("com.unboundid.ldap.sdk", "LDAPConnection") }
25+
}
26+
27+
/** The class `org.springframework.ldap.support.LdapEncoder`. */
28+
class TypeLdapEncoder extends Class {
29+
TypeLdapEncoder() { this.hasQualifiedName("org.springframework.ldap.support", "LdapEncoder") }
30+
}
31+
32+
/** A data flow source for unvalidated user input that is used to construct LDAP queries. */
33+
abstract class LdapInjectionSource extends DataFlow::Node { }
34+
35+
/** A data flow sink for unvalidated user input that is used to construct LDAP queries. */
36+
abstract class LdapInjectionSink extends DataFlow::ExprNode { }
37+
38+
/** A sanitizer for unvalidated user input that is used to construct LDAP queries. */
39+
abstract class LdapInjectionSanitizer extends DataFlow::ExprNode { }
40+
41+
/**
42+
* A taint-tracking configuration for unvalidated user input that is used to construct LDAP queries.
43+
*/
44+
class LdapInjectionFlowConfig extends TaintTracking::Configuration {
45+
LdapInjectionFlowConfig() { this = "LdapInjectionFlowConfig" }
46+
47+
override predicate isSource(DataFlow::Node source) { source instanceof LdapInjectionSource }
48+
49+
override predicate isSink(DataFlow::Node sink) { sink instanceof LdapInjectionSink }
50+
51+
override predicate isSanitizer(DataFlow::Node node) { node instanceof LdapInjectionSanitizer }
52+
53+
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
54+
filterStep(node1, node2) or searchRequestStep(node1, node2)
55+
}
56+
}
57+
58+
/** A source of remote user input. */
59+
class RemoteSource extends LdapInjectionSource {
60+
RemoteSource() { this instanceof RemoteFlowSource }
61+
}
62+
63+
/** A source of local user input. */
64+
class LocalSource extends LdapInjectionSource {
65+
LocalSource() { this instanceof LocalUserInput }
66+
}
67+
68+
abstract class Context extends RefType { }
69+
70+
/**
71+
* The interface `javax.naming.directory.DirContext` or
72+
* the class `javax.naming.directory.InitialDirContext`.
73+
*/
74+
class DirContext extends Context {
75+
DirContext() {
76+
this.hasQualifiedName("javax.naming.directory", "DirContext") or
77+
this.hasQualifiedName("javax.naming.directory", "InitialDirContext")
78+
}
79+
}
80+
81+
/**
82+
* The interface `javax.naming.ldap.LdapContext` or
83+
* the class `javax.naming.ldap.InitialLdapContext`.
84+
*/
85+
class LdapContext extends Context {
86+
LdapContext() {
87+
this.hasQualifiedName("javax.naming.ldap", "LdapContext") or
88+
this.hasQualifiedName("javax.naming.ldap", "InitialLdapContext")
89+
}
90+
}
91+
92+
/**
93+
* JNDI sink for LDAP injection vulnerabilities, i.e. 2nd argument to search method from
94+
* DirContext, InitialDirContext, LdapContext or InitialLdapContext.
95+
*/
96+
class JndiLdapInjectionSink extends LdapInjectionSink {
97+
JndiLdapInjectionSink() {
98+
exists(MethodAccess ma, Method m, int index |
99+
ma.getMethod() = m and
100+
ma.getArgument(index) = this.getExpr()
101+
|
102+
m.getDeclaringType() instanceof Context and m.hasName("search") and index = 1
103+
)
104+
}
105+
}
106+
107+
/**
108+
* UnboundID sink for LDAP injection vulnerabilities,
109+
* i.e. LDAPConnection.search or LDAPConnection.searchForEntry method.
110+
*/
111+
class UnboundIdLdapInjectionSink extends LdapInjectionSink {
112+
UnboundIdLdapInjectionSink() {
113+
exists(MethodAccess ma, Method m, int index, RefType argType |
114+
ma.getMethod() = m and
115+
ma.getArgument(index) = this.getExpr() and
116+
ma.getArgument(index).getType() = argType
117+
|
118+
// LDAPConnection.search or LDAPConnection.searchForEntry method
119+
m.getDeclaringType() instanceof TypeLDAPConnection and
120+
(m.hasName("search") or m.hasName("searchForEntry")) and
121+
(
122+
// Parameter type is SearchRequest or ReadOnlySearchRequest
123+
(
124+
argType instanceof TypeReadOnlySearchRequest or
125+
argType instanceof TypeSearchRequest
126+
) or
127+
// Or parameter index is 2, 3, 5, 6 or 7 (this is where filter parameter is)
128+
// but it's not the last one nor beyond the last one (varargs representing attributes)
129+
index = any(int i |
130+
(i = [2..3] or i = [5..7]) and i < ma.getMethod().getNumberOfParameters() - 1
131+
)
132+
)
133+
)
134+
}
135+
}
136+
137+
/**
138+
* Spring LDAP sink for LDAP injection vulnerabilities,
139+
* i.e. LDAPConnection.search or LDAPConnection.searchForEntry method.
140+
*/
141+
// LdapTemplate:
142+
// find(LdapQuery query, Class<T> clazz)
143+
// find(Name base, Filter filter, SearchControls searchControls, Class<T> clazz)
144+
// findOne(LdapQuery query, Class<T> clazz)
145+
// search - 2nd param if String (filter)
146+
// search - 1st param if LdapQuery
147+
// searchForContext(LdapQuery query)
148+
// searchForObject - 2nd param if String (filter)
149+
// searchForObject - 1st param if LdapQuery
150+
class SpringLdapInjectionSink extends LdapInjectionSink {
151+
SpringLdapInjectionSink() {
152+
exists(MethodAccess ma, Method m, int index, RefType argType |
153+
ma.getMethod() = m and
154+
ma.getArgument(index) = this.getExpr() and
155+
ma.getArgument(index).getType() = argType
156+
|
157+
// LDAPConnection.search or LDAPConnection.searchForEntry method
158+
m.getDeclaringType() instanceof TypeLDAPConnection and
159+
(m.hasName("search") or m.hasName("searchForEntry")) and
160+
(
161+
// Parameter type is SearchRequest or ReadOnlySearchRequest
162+
(
163+
argType instanceof TypeReadOnlySearchRequest or
164+
argType instanceof TypeSearchRequest
165+
) or
166+
// Or parameter index is 2, 3, 5, 6 or 7 (this is where filter parameter is)
167+
// but it's not the last one nor beyond the last one (varargs representing attributes)
168+
index = any(int i |
169+
(i = [2..3] or i = [5..7]) and i < ma.getMethod().getNumberOfParameters() - 1
170+
)
171+
)
172+
)
173+
}
174+
}
175+
176+
/** An expression node with a primitive type. */
177+
class PrimitiveTypeSanitizer extends LdapInjectionSanitizer {
178+
PrimitiveTypeSanitizer() { this.getType() instanceof PrimitiveType }
179+
}
180+
181+
/** An expression node with a boxed type. */
182+
class BoxedTypeSanitizer extends LdapInjectionSanitizer {
183+
BoxedTypeSanitizer() { this.getType() instanceof BoxedType }
184+
}
185+
186+
/** encodeForLDAP and encodeForDN from OWASP ESAPI. */
187+
class EsapiSanitizer extends LdapInjectionSanitizer {
188+
EsapiSanitizer() {
189+
this.getExpr().(MethodAccess).getMethod().hasName("encodeForLDAP")
190+
}
191+
}
192+
193+
/** LdapEncoder.filterEncode and LdapEncoder.nameEncode from Spring LDAP. */
194+
class SpringLdapSanitizer extends LdapInjectionSanitizer {
195+
SpringLdapSanitizer() {
196+
this.getType() instanceof TypeLdapEncoder and
197+
this.getExpr().(MethodAccess).getMethod().hasName("filterEncode")
198+
}
199+
}
200+
201+
/** Filter.encodeValue from UnboundID. */
202+
class UnboundIdSanitizer extends LdapInjectionSanitizer {
203+
UnboundIdSanitizer() {
204+
this.getType() instanceof TypeFilter and
205+
this.getExpr().(MethodAccess).getMethod().hasName("encodeValue")
206+
}
207+
}
208+
209+
/**
210+
* Holds if `n1` to `n2` is a dataflow step that converts between `String` and UnboundID `Filter`,
211+
* i.e. `Filter.create(tainted)`.
212+
*/
213+
predicate filterStep(ExprNode n1, ExprNode n2) {
214+
exists(MethodAccess ma, Method m |
215+
n1.asExpr() = ma.getQualifier() or
216+
n1.asExpr() = ma.getAnArgument()
217+
|
218+
n2.asExpr() = ma and
219+
ma.getMethod() = m and
220+
m.getDeclaringType() instanceof TypeFilter and
221+
m.hasName("create")
222+
)
223+
}
224+
225+
/**
226+
* Holds if `n1` to `n2` is a dataflow step that converts between `String` and UnboundID
227+
* `SearchRequest`, i.e. `new SearchRequest([...], tainted, [...])`, where `tainted` is
228+
* parameter number 3, 4, 7, 8 or 9, but not the last one or beyond the last one (varargs).
229+
*/
230+
predicate searchRequestStep(ExprNode n1, ExprNode n2) {
231+
exists(ConstructorCall cc, int index | cc.getConstructedType() instanceof TypeSearchRequest |
232+
n1.asExpr() = cc.getArgument(index) and
233+
n2.asExpr() = cc and
234+
index = any(int i |
235+
(i = [2..3] or i = [6..8]) and i < cc.getConstructor().getNumberOfParameters() - 1
236+
)
237+
)
238+
}

0 commit comments

Comments
 (0)