Skip to content

Commit 36eef84

Browse files
committed
Upload LDAP Injection query, qhelp and tests
1 parent 6bab41c commit 36eef84

File tree

10 files changed

+561
-0
lines changed

10 files changed

+561
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
2+
3+
<qhelp>
4+
<overview>
5+
<p>If an LDAP query is built by a not sanitized user-provided value, a user is likely to be able to run malicious LDAP queries.</p>
6+
</overview>
7+
8+
<recommendation>
9+
<p>In case user input must compose an LDAP query, it should be escaped in order to avoid a malicious user supplying special characters that change the actual purpose of the query. To do so, functions that ldap frameworks provide such as <code>escape_filter_chars</code> should be applied to that user input.
10+
<recommendation>
11+
12+
13+
<references>
14+
<li>
15+
OWASP
16+
<a href="https://owasp.org/www-community/attacks/LDAP_Injection">LDAP Injection</a>
17+
</li>
18+
<li>
19+
SonarSource
20+
<a href="https://rules.sonarsource.com/python/RSPEC-2078">RSPEC-2078</a>
21+
</li>
22+
<li>
23+
Python
24+
<a href="https://www.python-ldap.org/en/python-ldap-3.3.0/reference/ldap.html">LDAP Documentation</a>
25+
</li>
26+
<li>
27+
CWE-
28+
<a href="https://cwe.mitre.org/data/definitions/90.html">090</a>
29+
</li>
30+
</references>
31+
32+
</qhelp>
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* @name Python LDAP Injection
3+
* @description Python LDAP Injection through search filter
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @id python/ldap-injection
7+
* @tags experimental
8+
* security
9+
* external/cwe/cwe-090
10+
*/
11+
12+
import python
13+
import semmle.python.dataflow.new.RemoteFlowSources
14+
import semmle.python.dataflow.new.DataFlow
15+
import semmle.python.dataflow.new.TaintTracking
16+
import semmle.python.dataflow.new.internal.TaintTrackingPublic
17+
import DataFlow::PathGraph
18+
19+
class InitializeSink extends DataFlow::Node {
20+
InitializeSink() {
21+
exists(SsaVariable initVar, CallNode searchCall |
22+
// get variable whose value equals a call to ldap.initialize
23+
initVar.getDefinition().getImmediateDominator() = Value::named("ldap.initialize").getACall() and
24+
// get the Call in which the previous variable is used
25+
initVar.getAUse().getNode() = searchCall.getNode().getFunc().(Attribute).getObject() and
26+
// restrict that call's attribute (something.this) to match %search%
27+
searchCall.getNode().getFunc().(Attribute).getName().matches("%search%") and
28+
// set the third argument (search_filter) as sink
29+
this.asExpr() = searchCall.getArg(2).getNode()
30+
// set the first argument (DN) as sink
31+
// or this.asExpr() = searchCall.getArg(0) // Should this be set?
32+
)
33+
}
34+
}
35+
36+
class ConnectionSink extends DataFlow::Node {
37+
ConnectionSink() {
38+
exists(SsaVariable connVar, CallNode searchCall |
39+
// get variable whose value equals a call to ldap.initialize
40+
connVar.getDefinition().getImmediateDominator() = Value::named("ldap3.Connection").getACall() and
41+
// get the Call in which the previous variable is used
42+
connVar.getAUse().getNode() = searchCall.getNode().getFunc().(Attribute).getObject() and
43+
// restrict that call's attribute (something.this) to match %search%
44+
searchCall.getNode().getFunc().(Attribute).getName().matches("%search%") and
45+
// set the second argument (search_filter) as sink
46+
this.asExpr() = searchCall.getArg(1).getNode()
47+
// set the first argument (DN) as sink
48+
// or this.asExpr() = searchCall.getArg(0) // Should this be set?
49+
)
50+
}
51+
}
52+
53+
class EscapeSanitizer extends DataFlow::Node {
54+
EscapeSanitizer() {
55+
exists(Call c |
56+
(
57+
// avoid flow through any %escape% function
58+
c.getFunc().(Attribute).getName().matches("%escape%") or // something.%escape%()
59+
c.getFunc().(Name).getId().matches("%escape%") // %escape%()
60+
) and
61+
this.asExpr() = c
62+
)
63+
}
64+
}
65+
66+
class LDAPInjectionSink extends DataFlow::Node {
67+
LDAPInjectionSink() {
68+
this instanceof InitializeSink or
69+
this instanceof ConnectionSink
70+
}
71+
}
72+
73+
class LDAPInjectionFlowConfig extends TaintTracking::Configuration {
74+
LDAPInjectionFlowConfig() { this = "LDAPInjectionFlowConfig" }
75+
76+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
77+
78+
override predicate isSink(DataFlow::Node sink) { sink instanceof LDAPInjectionSink }
79+
80+
override predicate isSanitizer(DataFlow::Node sanitizer) { sanitizer instanceof EscapeSanitizer }
81+
}
82+
83+
from LDAPInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
84+
where config.hasFlowPath(source, sink)
85+
select sink.getNode(), source, sink, "$@ LDAP query executes $@.", sink.getNode(), "This",
86+
source.getNode(), "a user-provided value"
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import ldap3
2+
from ldap3.utils.conv import escape_filter_chars
3+
from flask import request, Flask
4+
5+
app = Flask(__name__)
6+
7+
8+
@app.route("/tainted_var")
9+
def tainted_var():
10+
sanitized_dn = "dc=%s" % request.args['dc']
11+
sanitized_filter = "(&(objectClass=*)(uid=%s))" % escape_filter_chars(
12+
request.args['username'])
13+
14+
srv = ldap3.Server('localhost', port=1337)
15+
conn = ldap3.Connection(srv, user=sanitized_dn, auto_bind=True)
16+
conn.search(sanitized_dn, sanitized_filter)
17+
return conn.response
18+
19+
20+
@app.route("/var_tainted")
21+
def var_tainted():
22+
sanitized_dn = request.args['dc']
23+
sanitized_filter = request.args['username']
24+
25+
dn = "dc=%s" % sanitized_dn
26+
search_filter = "(&(objectClass=*)(uid=%s))" % escape_filter_chars(sanitized_filter)
27+
28+
srv = ldap3.Server('localhost', port=1337)
29+
conn = ldap3.Connection(srv, user=dn, auto_bind=True)
30+
conn.search(dn, search_filter)
31+
return conn.response
32+
33+
34+
@app.route("/direct")
35+
def direct():
36+
srv = ldap3.Server('localhost', port=1337)
37+
conn = ldap3.Connection(srv, user="dc=%s" %
38+
request.args['dc'], auto_bind=True)
39+
conn.search("dc=%s" % request.args['dc'], "(&(objectClass=*)(uid=%s))" %
40+
escape_filter_chars(request.args['username']))
41+
return conn.response
42+
43+
44+
@ app.route("/with_")
45+
def with_():
46+
sanitized_dn = request.args['dc']
47+
sanitized_filter = escape_filter_chars(request.args['username'])
48+
49+
dn = "dc=%s" % sanitized_dn
50+
search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter
51+
52+
srv = ldap3.Server('localhost', port=1337)
53+
with ldap3.Connection(server, auto_bind=True) as conn:
54+
conn.search(dn, search_filter)
55+
return conn.response
56+
57+
# if __name__ == "__main__":
58+
# app.run(debug=True)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from ldap3 import Server, Connection
2+
from ldap3.utils.conv import escape_filter_chars
3+
from flask import request, Flask
4+
5+
app = Flask(__name__)
6+
7+
8+
@app.route("/tainted_var")
9+
def tainted_var():
10+
sanitized_dn = "dc=%s" % request.args['dc']
11+
sanitized_filter = "(&(objectClass=*)(uid=%s))" % escape_filter_chars(
12+
request.args['username'])
13+
14+
srv = Server('localhost', port=1337)
15+
conn = Connection(srv, user=sanitized_dn, auto_bind=True)
16+
conn.search(sanitized_dn, sanitized_filter)
17+
return conn.response
18+
19+
20+
@app.route("/var_tainted")
21+
def var_tainted():
22+
sanitized_dn = request.args['dc']
23+
sanitized_filter = request.args['username']
24+
25+
dn = "dc=%s" % sanitized_dn
26+
search_filter = "(&(objectClass=*)(uid=%s))" % escape_filter_chars(sanitized_filter)
27+
28+
srv = Server('localhost', port=1337)
29+
conn = Connection(srv, user=dn, auto_bind=True)
30+
conn.search(dn, search_filter)
31+
return conn.response
32+
33+
34+
@app.route("/direct")
35+
def direct():
36+
srv = Server('localhost', port=1337)
37+
conn = Connection(srv, user="dc=%s" % request.args['dc'], auto_bind=True)
38+
conn.search("dc=%s" % request.args['dc'], "(&(objectClass=*)(uid=%s))" %
39+
escape_filter_chars(request.args['username']))
40+
return conn.response
41+
42+
43+
@app.route("/with_2")
44+
def with_2():
45+
sanitized_dn = request.args['dc']
46+
sanitized_filter = escape_filter_chars(request.args['username'])
47+
48+
dn = "dc=%s" % sanitized_dn
49+
search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter
50+
51+
srv = Server('localhost', port=1337)
52+
with Connection(server, auto_bind=True) as conn:
53+
conn.search(dn, search_filter)
54+
return conn.response
55+
56+
# if __name__ == "__main__":
57+
# app.run(debug=True)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import ldap3
2+
from flask import request, Flask
3+
4+
app = Flask(__name__)
5+
6+
7+
@app.route("/tainted_var")
8+
def tainted_var():
9+
unsanitized_dn = "dc=%s" % request.args['dc']
10+
unsanitized_filter = "(&(objectClass=*)(uid=%s))" % request.args['username']
11+
12+
srv = ldap3.Server('localhost', port=1337)
13+
conn = ldap3.Connection(srv, user=unsanitized_dn, auto_bind=True)
14+
conn.search(unsanitized_dn, unsanitized_filter)
15+
return conn.response
16+
17+
18+
@app.route("/var_tainted")
19+
def var_tainted():
20+
unsanitized_dn = request.args['dc']
21+
unsanitized_filter = request.args['username']
22+
23+
dn = "dc=%s" % unsanitized_dn
24+
search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter
25+
26+
srv = ldap3.Server('localhost', port=1337)
27+
conn = ldap3.Connection(srv, user=dn, auto_bind=True)
28+
conn.search(dn, search_filter)
29+
return conn.response
30+
31+
32+
@app.route("/direct")
33+
def direct():
34+
srv = ldap3.Server('localhost', port=1337)
35+
conn = ldap3.Connection(srv, user="dc=%s" %
36+
request.args['dc'], auto_bind=True)
37+
conn.search("dc=%s" % unsanitized_dn,
38+
"(&(objectClass=*)(uid=%s))" % request.args['username'])
39+
return conn.response
40+
41+
42+
@app.route("/with_")
43+
def with_():
44+
unsanitized_dn = request.args['dc']
45+
unsanitized_filter = request.args['username']
46+
47+
dn = "dc=%s" % unsanitized_dn
48+
search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter
49+
50+
srv = ldap3.Server('localhost', port=1337)
51+
with ldap3.Connection(server, auto_bind=True) as conn:
52+
conn.search(dn, search_filter)
53+
return conn.response
54+
55+
# if __name__ == "__main__":
56+
# app.run(debug=True)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from ldap3 import Server, Connection
2+
from flask import request, Flask
3+
4+
app = Flask(__name__)
5+
6+
7+
@app.route("/tainted_var")
8+
def tainted_var():
9+
unsanitized_dn = "dc=%s" % request.args['dc']
10+
unsanitized_filter = "(&(objectClass=*)(uid=%s))" % request.args['username']
11+
12+
srv = Server('localhost', port=1337)
13+
conn = Connection(srv, user=unsanitized_dn, auto_bind=True)
14+
conn.search(unsanitized_dn, unsanitized_filter)
15+
return conn.response
16+
17+
18+
@app.route("/var_tainted")
19+
def var_tainted():
20+
unsanitized_dn = request.args['dc']
21+
unsanitized_filter = request.args['username']
22+
23+
dn = "dc=%s" % unsanitized_dn
24+
search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter
25+
26+
srv = Server('localhost', port=1337)
27+
conn = Connection(srv, user=dn, auto_bind=True)
28+
conn.search(dn, search_filter)
29+
return conn.response
30+
31+
32+
@app.route("/direct")
33+
def direct():
34+
srv = Server('localhost', port=1337)
35+
conn = Connection(srv, user="dc=%s" % request.args['dc'], auto_bind=True)
36+
conn.search(
37+
"dc=%s" % request.args['dc'], "(&(objectClass=*)(uid=%s))" % request.args['username'])
38+
return conn.response
39+
40+
41+
@app.route("/with_2")
42+
def with_2():
43+
unsanitized_dn = request.args['dc']
44+
unsanitized_filter = request.args['username']
45+
46+
dn = "dc=%s" % unsanitized_dn
47+
search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter
48+
49+
srv = Server('localhost', port=1337)
50+
with Connection(server, auto_bind=True) as conn:
51+
conn.search(dn, search_filter)
52+
return conn.response
53+
54+
# if __name__ == "__main__":
55+
# app.run(debug=True)

0 commit comments

Comments
 (0)