From 799d509f26d0954025fea5302945387194fdef46 Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Thu, 18 Mar 2021 17:31:41 +0100
Subject: [PATCH 01/34] Upload LDAP Injection query, qhelp and tests
---
.../src/Security/CWE-090/LDAPInjection.qhelp | 32 +++++++
.../ql/src/Security/CWE-090/LDAPInjection.ql | 86 +++++++++++++++++++
.../Security/CWE-090/tests/ldap3_sanitized.py | 58 +++++++++++++
.../CWE-090/tests/ldap3_sanitized_asObj.py | 57 ++++++++++++
.../CWE-090/tests/ldap3_unsanitized.py | 56 ++++++++++++
.../CWE-090/tests/ldap3_unsanitized_asObj.py | 55 ++++++++++++
.../Security/CWE-090/tests/ldap_sanitized.py | 56 ++++++++++++
.../CWE-090/tests/ldap_sanitized_asObj.py | 57 ++++++++++++
.../CWE-090/tests/ldap_unsanitized.py | 52 +++++++++++
.../CWE-090/tests/ldap_unsanitized_asObj.py | 52 +++++++++++
10 files changed, 561 insertions(+)
create mode 100644 python/ql/src/Security/CWE-090/LDAPInjection.qhelp
create mode 100644 python/ql/src/Security/CWE-090/LDAPInjection.ql
create mode 100644 python/ql/src/Security/CWE-090/tests/ldap3_sanitized.py
create mode 100644 python/ql/src/Security/CWE-090/tests/ldap3_sanitized_asObj.py
create mode 100644 python/ql/src/Security/CWE-090/tests/ldap3_unsanitized.py
create mode 100644 python/ql/src/Security/CWE-090/tests/ldap3_unsanitized_asObj.py
create mode 100644 python/ql/src/Security/CWE-090/tests/ldap_sanitized.py
create mode 100644 python/ql/src/Security/CWE-090/tests/ldap_sanitized_asObj.py
create mode 100644 python/ql/src/Security/CWE-090/tests/ldap_unsanitized.py
create mode 100644 python/ql/src/Security/CWE-090/tests/ldap_unsanitized_asObj.py
diff --git a/python/ql/src/Security/CWE-090/LDAPInjection.qhelp b/python/ql/src/Security/CWE-090/LDAPInjection.qhelp
new file mode 100644
index 000000000000..e077ccc3afe4
--- /dev/null
+++ b/python/ql/src/Security/CWE-090/LDAPInjection.qhelp
@@ -0,0 +1,32 @@
+
+
+
+
+ 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.
+
+
+
+ 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 escape_filter_chars should be applied to that user input.
+
+
+
+
+
+ OWASP
+ LDAP Injection
+
+
+ SonarSource
+ RSPEC-2078
+
+
+ Python
+ LDAP Documentation
+
+
+ CWE-
+ 090
+
+
+
+
\ No newline at end of file
diff --git a/python/ql/src/Security/CWE-090/LDAPInjection.ql b/python/ql/src/Security/CWE-090/LDAPInjection.ql
new file mode 100644
index 000000000000..d6ff1ad56074
--- /dev/null
+++ b/python/ql/src/Security/CWE-090/LDAPInjection.ql
@@ -0,0 +1,86 @@
+/**
+ * @name Python LDAP Injection
+ * @description Python LDAP Injection through search filter
+ * @kind path-problem
+ * @problem.severity error
+ * @id python/ldap-injection
+ * @tags experimental
+ * security
+ * external/cwe/cwe-090
+ */
+
+import python
+import semmle.python.dataflow.new.RemoteFlowSources
+import semmle.python.dataflow.new.DataFlow
+import semmle.python.dataflow.new.TaintTracking
+import semmle.python.dataflow.new.internal.TaintTrackingPublic
+import DataFlow::PathGraph
+
+class InitializeSink extends DataFlow::Node {
+ InitializeSink() {
+ exists(SsaVariable initVar, CallNode searchCall |
+ // get variable whose value equals a call to ldap.initialize
+ initVar.getDefinition().getImmediateDominator() = Value::named("ldap.initialize").getACall() and
+ // get the Call in which the previous variable is used
+ initVar.getAUse().getNode() = searchCall.getNode().getFunc().(Attribute).getObject() and
+ // restrict that call's attribute (something.this) to match %search%
+ searchCall.getNode().getFunc().(Attribute).getName().matches("%search%") and
+ // set the third argument (search_filter) as sink
+ this.asExpr() = searchCall.getArg(2).getNode()
+ // set the first argument (DN) as sink
+ // or this.asExpr() = searchCall.getArg(0) // Should this be set?
+ )
+ }
+}
+
+class ConnectionSink extends DataFlow::Node {
+ ConnectionSink() {
+ exists(SsaVariable connVar, CallNode searchCall |
+ // get variable whose value equals a call to ldap.initialize
+ connVar.getDefinition().getImmediateDominator() = Value::named("ldap3.Connection").getACall() and
+ // get the Call in which the previous variable is used
+ connVar.getAUse().getNode() = searchCall.getNode().getFunc().(Attribute).getObject() and
+ // restrict that call's attribute (something.this) to match %search%
+ searchCall.getNode().getFunc().(Attribute).getName().matches("%search%") and
+ // set the second argument (search_filter) as sink
+ this.asExpr() = searchCall.getArg(1).getNode()
+ // set the first argument (DN) as sink
+ // or this.asExpr() = searchCall.getArg(0) // Should this be set?
+ )
+ }
+}
+
+class EscapeSanitizer extends DataFlow::Node {
+ EscapeSanitizer() {
+ exists(Call c |
+ (
+ // avoid flow through any %escape% function
+ c.getFunc().(Attribute).getName().matches("%escape%") or // something.%escape%()
+ c.getFunc().(Name).getId().matches("%escape%") // %escape%()
+ ) and
+ this.asExpr() = c
+ )
+ }
+}
+
+class LDAPInjectionSink extends DataFlow::Node {
+ LDAPInjectionSink() {
+ this instanceof InitializeSink or
+ this instanceof ConnectionSink
+ }
+}
+
+class LDAPInjectionFlowConfig extends TaintTracking::Configuration {
+ LDAPInjectionFlowConfig() { this = "LDAPInjectionFlowConfig" }
+
+ override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
+
+ override predicate isSink(DataFlow::Node sink) { sink instanceof LDAPInjectionSink }
+
+ override predicate isSanitizer(DataFlow::Node sanitizer) { sanitizer instanceof EscapeSanitizer }
+}
+
+from LDAPInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
+where config.hasFlowPath(source, sink)
+select sink.getNode(), source, sink, "$@ LDAP query executes $@.", sink.getNode(), "This",
+ source.getNode(), "a user-provided value"
diff --git a/python/ql/src/Security/CWE-090/tests/ldap3_sanitized.py b/python/ql/src/Security/CWE-090/tests/ldap3_sanitized.py
new file mode 100644
index 000000000000..05bc1e24f716
--- /dev/null
+++ b/python/ql/src/Security/CWE-090/tests/ldap3_sanitized.py
@@ -0,0 +1,58 @@
+import ldap3
+from ldap3.utils.conv import escape_filter_chars
+from flask import request, Flask
+
+app = Flask(__name__)
+
+
+@app.route("/tainted_var")
+def tainted_var():
+ sanitized_dn = "dc=%s" % request.args['dc']
+ sanitized_filter = "(&(objectClass=*)(uid=%s))" % escape_filter_chars(
+ request.args['username'])
+
+ srv = ldap3.Server('localhost', port=1337)
+ conn = ldap3.Connection(srv, user=sanitized_dn, auto_bind=True)
+ conn.search(sanitized_dn, sanitized_filter)
+ return conn.response
+
+
+@app.route("/var_tainted")
+def var_tainted():
+ sanitized_dn = request.args['dc']
+ sanitized_filter = request.args['username']
+
+ dn = "dc=%s" % sanitized_dn
+ search_filter = "(&(objectClass=*)(uid=%s))" % escape_filter_chars(sanitized_filter)
+
+ srv = ldap3.Server('localhost', port=1337)
+ conn = ldap3.Connection(srv, user=dn, auto_bind=True)
+ conn.search(dn, search_filter)
+ return conn.response
+
+
+@app.route("/direct")
+def direct():
+ srv = ldap3.Server('localhost', port=1337)
+ conn = ldap3.Connection(srv, user="dc=%s" %
+ request.args['dc'], auto_bind=True)
+ conn.search("dc=%s" % request.args['dc'], "(&(objectClass=*)(uid=%s))" %
+ escape_filter_chars(request.args['username']))
+ return conn.response
+
+
+@ app.route("/with_")
+def with_():
+ sanitized_dn = request.args['dc']
+ sanitized_filter = escape_filter_chars(request.args['username'])
+
+ dn = "dc=%s" % sanitized_dn
+ search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter
+
+ srv = ldap3.Server('localhost', port=1337)
+ with ldap3.Connection(server, auto_bind=True) as conn:
+ conn.search(dn, search_filter)
+ return conn.response
+
+# if __name__ == "__main__":
+# app.run(debug=True)
diff --git a/python/ql/src/Security/CWE-090/tests/ldap3_sanitized_asObj.py b/python/ql/src/Security/CWE-090/tests/ldap3_sanitized_asObj.py
new file mode 100644
index 000000000000..c7dbb3450588
--- /dev/null
+++ b/python/ql/src/Security/CWE-090/tests/ldap3_sanitized_asObj.py
@@ -0,0 +1,57 @@
+from ldap3 import Server, Connection
+from ldap3.utils.conv import escape_filter_chars
+from flask import request, Flask
+
+app = Flask(__name__)
+
+
+@app.route("/tainted_var")
+def tainted_var():
+ sanitized_dn = "dc=%s" % request.args['dc']
+ sanitized_filter = "(&(objectClass=*)(uid=%s))" % escape_filter_chars(
+ request.args['username'])
+
+ srv = Server('localhost', port=1337)
+ conn = Connection(srv, user=sanitized_dn, auto_bind=True)
+ conn.search(sanitized_dn, sanitized_filter)
+ return conn.response
+
+
+@app.route("/var_tainted")
+def var_tainted():
+ sanitized_dn = request.args['dc']
+ sanitized_filter = request.args['username']
+
+ dn = "dc=%s" % sanitized_dn
+ search_filter = "(&(objectClass=*)(uid=%s))" % escape_filter_chars(sanitized_filter)
+
+ srv = Server('localhost', port=1337)
+ conn = Connection(srv, user=dn, auto_bind=True)
+ conn.search(dn, search_filter)
+ return conn.response
+
+
+@app.route("/direct")
+def direct():
+ srv = Server('localhost', port=1337)
+ conn = Connection(srv, user="dc=%s" % request.args['dc'], auto_bind=True)
+ conn.search("dc=%s" % request.args['dc'], "(&(objectClass=*)(uid=%s))" %
+ escape_filter_chars(request.args['username']))
+ return conn.response
+
+
+@app.route("/with_2")
+def with_2():
+ sanitized_dn = request.args['dc']
+ sanitized_filter = escape_filter_chars(request.args['username'])
+
+ dn = "dc=%s" % sanitized_dn
+ search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter
+
+ srv = Server('localhost', port=1337)
+ with Connection(server, auto_bind=True) as conn:
+ conn.search(dn, search_filter)
+ return conn.response
+
+# if __name__ == "__main__":
+# app.run(debug=True)
diff --git a/python/ql/src/Security/CWE-090/tests/ldap3_unsanitized.py b/python/ql/src/Security/CWE-090/tests/ldap3_unsanitized.py
new file mode 100644
index 000000000000..fcd00c0269ca
--- /dev/null
+++ b/python/ql/src/Security/CWE-090/tests/ldap3_unsanitized.py
@@ -0,0 +1,56 @@
+import ldap3
+from flask import request, Flask
+
+app = Flask(__name__)
+
+
+@app.route("/tainted_var")
+def tainted_var():
+ unsanitized_dn = "dc=%s" % request.args['dc']
+ unsanitized_filter = "(&(objectClass=*)(uid=%s))" % request.args['username']
+
+ srv = ldap3.Server('localhost', port=1337)
+ conn = ldap3.Connection(srv, user=unsanitized_dn, auto_bind=True)
+ conn.search(unsanitized_dn, unsanitized_filter)
+ return conn.response
+
+
+@app.route("/var_tainted")
+def var_tainted():
+ unsanitized_dn = request.args['dc']
+ unsanitized_filter = request.args['username']
+
+ dn = "dc=%s" % unsanitized_dn
+ search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter
+
+ srv = ldap3.Server('localhost', port=1337)
+ conn = ldap3.Connection(srv, user=dn, auto_bind=True)
+ conn.search(dn, search_filter)
+ return conn.response
+
+
+@app.route("/direct")
+def direct():
+ srv = ldap3.Server('localhost', port=1337)
+ conn = ldap3.Connection(srv, user="dc=%s" %
+ request.args['dc'], auto_bind=True)
+ conn.search("dc=%s" % unsanitized_dn,
+ "(&(objectClass=*)(uid=%s))" % request.args['username'])
+ return conn.response
+
+
+@app.route("/with_")
+def with_():
+ unsanitized_dn = request.args['dc']
+ unsanitized_filter = request.args['username']
+
+ dn = "dc=%s" % unsanitized_dn
+ search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter
+
+ srv = ldap3.Server('localhost', port=1337)
+ with ldap3.Connection(server, auto_bind=True) as conn:
+ conn.search(dn, search_filter)
+ return conn.response
+
+# if __name__ == "__main__":
+# app.run(debug=True)
diff --git a/python/ql/src/Security/CWE-090/tests/ldap3_unsanitized_asObj.py b/python/ql/src/Security/CWE-090/tests/ldap3_unsanitized_asObj.py
new file mode 100644
index 000000000000..634ee992f3e5
--- /dev/null
+++ b/python/ql/src/Security/CWE-090/tests/ldap3_unsanitized_asObj.py
@@ -0,0 +1,55 @@
+from ldap3 import Server, Connection
+from flask import request, Flask
+
+app = Flask(__name__)
+
+
+@app.route("/tainted_var")
+def tainted_var():
+ unsanitized_dn = "dc=%s" % request.args['dc']
+ unsanitized_filter = "(&(objectClass=*)(uid=%s))" % request.args['username']
+
+ srv = Server('localhost', port=1337)
+ conn = Connection(srv, user=unsanitized_dn, auto_bind=True)
+ conn.search(unsanitized_dn, unsanitized_filter)
+ return conn.response
+
+
+@app.route("/var_tainted")
+def var_tainted():
+ unsanitized_dn = request.args['dc']
+ unsanitized_filter = request.args['username']
+
+ dn = "dc=%s" % unsanitized_dn
+ search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter
+
+ srv = Server('localhost', port=1337)
+ conn = Connection(srv, user=dn, auto_bind=True)
+ conn.search(dn, search_filter)
+ return conn.response
+
+
+@app.route("/direct")
+def direct():
+ srv = Server('localhost', port=1337)
+ conn = Connection(srv, user="dc=%s" % request.args['dc'], auto_bind=True)
+ conn.search(
+ "dc=%s" % request.args['dc'], "(&(objectClass=*)(uid=%s))" % request.args['username'])
+ return conn.response
+
+
+@app.route("/with_2")
+def with_2():
+ unsanitized_dn = request.args['dc']
+ unsanitized_filter = request.args['username']
+
+ dn = "dc=%s" % unsanitized_dn
+ search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter
+
+ srv = Server('localhost', port=1337)
+ with Connection(server, auto_bind=True) as conn:
+ conn.search(dn, search_filter)
+ return conn.response
+
+# if __name__ == "__main__":
+# app.run(debug=True)
diff --git a/python/ql/src/Security/CWE-090/tests/ldap_sanitized.py b/python/ql/src/Security/CWE-090/tests/ldap_sanitized.py
new file mode 100644
index 000000000000..83bafa791956
--- /dev/null
+++ b/python/ql/src/Security/CWE-090/tests/ldap_sanitized.py
@@ -0,0 +1,56 @@
+from flask import request, Flask
+import ldap
+import ldap.filter
+import ldap.dn
+
+app = Flask(__name__)
+
+
+@app.route("/tainted_var")
+def tainted_var():
+ sanitized_dn = "dc=%s" % ldap.dn.escape_dn_chars(request.args['dc'])
+ sanitized_filter = "(&(objectClass=*)(uid=%s))" % ldap.filter.escape_filter_chars(
+ request.args['username'])
+
+ ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
+ user = ldap_connection.search_s(
+ sanitized_dn, ldap.SCOPE_SUBTREE, sanitized_filter)
+ return user[0]
+
+
+@app.route("/var_tainted")
+def var_tainted():
+ sanitized_dn = request.args['dc']
+ sanitized_filter = request.args['username']
+
+ dn = "dc=%s" % ldap.dn.escape_dn_chars(sanitized_dn)
+ search_filter = "(&(objectClass=*)(uid=%s))" % ldap.filter.escape_filter_chars(sanitized_filter)
+
+ ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
+ user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter)
+ return user[0]
+
+
+@app.route("/direct")
+def direct():
+ ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
+ user = ldap_connection.search_s("dc=%s" % ldap.dn.escape_dn_chars(
+ request.args['dc']), ldap.SCOPE_SUBTREE, "(&(objectClass=*)(uid=%s))" % ldap.filter.escape_filter_chars(request.args['username']))
+ return user[0]
+
+
+@app.route("/with_")
+def with_():
+ sanitized_dn = ldap.dn.escape_dn_chars(request.args['dc'])
+ sanitized_filter = ldap.filter.escape_filter_chars(
+ request.args['username'])
+
+ dn = "dc=%s" % sanitized_dn
+ search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter
+
+ with ldap.initialize("ldap://127.0.0.1:1337") as ldap_connection:
+ user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter)
+ return user[0]
+
+# if __name__ == "__main__":
+# app.run(debug=True)
diff --git a/python/ql/src/Security/CWE-090/tests/ldap_sanitized_asObj.py b/python/ql/src/Security/CWE-090/tests/ldap_sanitized_asObj.py
new file mode 100644
index 000000000000..1c9662a4bcf8
--- /dev/null
+++ b/python/ql/src/Security/CWE-090/tests/ldap_sanitized_asObj.py
@@ -0,0 +1,57 @@
+from flask import request, Flask
+from ldap import initialize
+import ldap.filter
+import ldap.dn
+
+app = Flask(__name__)
+
+
+@app.route("/tainted_var_2")
+def tainted_var_2():
+ sanitized_dn = "dc=%s" % ldap.dn.escape_dn_chars(request.args['dc'])
+ sanitized_filter = "(&(objectClass=*)(uid=%s))" % ldap.filter.escape_filter_chars(
+ request.args['username'])
+
+ ldap_connection = initialize("ldap://127.0.0.1:1337")
+ user = ldap_connection.search_s(
+ sanitized_dn, ldap.SCOPE_SUBTREE, sanitized_filter)
+ return user[0]
+
+
+@app.route("/var_tainted_2")
+def var_tainted_2():
+ sanitized_dn = ldap.dn.escape_dn_chars(request.args['dc'])
+ sanitized_filter = ldap.filter.escape_filter_chars(
+ request.args['username'])
+
+ dn = "dc=%s" % sanitized_dn
+ search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter
+
+ ldap_connection = initialize("ldap://127.0.0.1:1337")
+ user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter)
+ return user[0]
+
+
+@app.route("/direct_2")
+def direct_2():
+ ldap_connection = initialize("ldap://127.0.0.1:1337")
+ user = ldap_connection.search_s("dc=%s" % ldap.dn.escape_dn_chars(
+ request.args['dc']), ldap.SCOPE_SUBTREE, "(&(objectClass=*)(uid=%s))" % ldap.filter.escape_filter_chars(request.args['username']))
+ return user[0]
+
+
+@app.route("/with_2")
+def with_2():
+ sanitized_dn = ldap.dn.escape_dn_chars(request.args['dc'])
+ sanitized_filter = ldap.filter.escape_filter_chars(
+ request.args['username'])
+
+ dn = "dc=%s" % sanitized_dn
+ search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter
+
+ with initialize("ldap://127.0.0.1:1337") as ldap_connection:
+ user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter)
+ return user[0]
+
+# if __name__ == "__main__":
+# app.run(debug=True)
diff --git a/python/ql/src/Security/CWE-090/tests/ldap_unsanitized.py b/python/ql/src/Security/CWE-090/tests/ldap_unsanitized.py
new file mode 100644
index 000000000000..8b22596b578d
--- /dev/null
+++ b/python/ql/src/Security/CWE-090/tests/ldap_unsanitized.py
@@ -0,0 +1,52 @@
+from flask import request, Flask
+import ldap
+
+app = Flask(__name__)
+
+
+@app.route("/tainted_var")
+def tainted_var():
+ unsanitized_dn = "dc=%s" % request.args['dc']
+ unsanitized_filter = "(&(objectClass=*)(uid=%s))" % request.args['username']
+
+ ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
+ user = ldap_connection.search_s(
+ unsanitized_dn, ldap.SCOPE_SUBTREE, unsanitized_filter)
+ return user[0]
+
+
+@app.route("/var_tainted")
+def var_tainted():
+ unsanitized_dn = request.args['dc']
+ unsanitized_filter = request.args['username']
+
+ dn = "dc=%s" % unsanitized_dn
+ search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter
+
+ ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
+ user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter)
+ return user[0]
+
+
+@app.route("/direct")
+def direct():
+ ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
+ user = ldap_connection.search_s(
+ "dc=%s" % request.args['dc'], ldap.SCOPE_SUBTREE, "(&(objectClass=*)(uid=%s))" % request.args['username'])
+ return user[0]
+
+
+@app.route("/with_")
+def with_():
+ sanitized_dn = request.args['dc']
+ sanitized_filter = request.args['username']
+
+ dn = "dc=%s" % sanitized_dn
+ search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter
+
+ with ldap.initialize("ldap://127.0.0.1:1337") as ldap_connection:
+ user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter)
+ return user[0]
+
+# if __name__ == "__main__":
+# app.run(debug=True)
diff --git a/python/ql/src/Security/CWE-090/tests/ldap_unsanitized_asObj.py b/python/ql/src/Security/CWE-090/tests/ldap_unsanitized_asObj.py
new file mode 100644
index 000000000000..49774dda2ec9
--- /dev/null
+++ b/python/ql/src/Security/CWE-090/tests/ldap_unsanitized_asObj.py
@@ -0,0 +1,52 @@
+from flask import request, Flask
+from ldap import initialize
+
+app = Flask(__name__)
+
+
+@app.route("/tainted_var")
+def tainted_var():
+ unsanitized_dn = "dc=%s" % request.args['dc']
+ unsanitized_filter = "(&(objectClass=*)(uid=%s))" % request.args['username']
+
+ ldap_connection = initialize("ldap://127.0.0.1:1337")
+ user = ldap_connection.search_s(
+ unsanitized_dn, ldap.SCOPE_SUBTREE, unsanitized_filter)
+ return user[0]
+
+
+@app.route("/var_tainted")
+def var_tainted():
+ unsanitized_dn = request.args['dc']
+ unsanitized_filter = request.args['username']
+
+ dn = "dc=%s" % unsanitized_dn
+ search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter
+
+ ldap_connection = initialize("ldap://127.0.0.1:1337")
+ user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter)
+ return user[0]
+
+
+@app.route("/direct")
+def direct():
+ ldap_connection = initialize("ldap://127.0.0.1:1337")
+ user = ldap_connection.search_s(
+ "dc=%s" % request.args['dc'], ldap.SCOPE_SUBTREE, "(&(objectClass=*)(uid=%s))" % request.args['username'])
+ return user[0]
+
+
+@app.route("/with_2")
+def with_2():
+ sanitized_dn = request.args['dc']
+ sanitized_filter = request.args['username']
+
+ dn = "dc=%s" % sanitized_dn
+ search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter
+
+ with initialize("ldap://127.0.0.1:1337") as ldap_connection:
+ user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter)
+ return user[0]
+
+# if __name__ == "__main__":
+# app.run(debug=True)
From 719b48cbaf8193f57560bd499e890f679c8d7931 Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Thu, 18 Mar 2021 20:21:47 +0100
Subject: [PATCH 02/34] Move to experimental folder
---
.../src/{ => experimental}/Security/CWE-090/LDAPInjection.qhelp | 0
.../ql/src/{ => experimental}/Security/CWE-090/LDAPInjection.ql | 0
.../{ => experimental}/Security/CWE-090/tests/ldap3_sanitized.py | 0
.../Security/CWE-090/tests/ldap3_sanitized_asObj.py | 0
.../Security/CWE-090/tests/ldap3_unsanitized.py | 0
.../Security/CWE-090/tests/ldap3_unsanitized_asObj.py | 0
.../{ => experimental}/Security/CWE-090/tests/ldap_sanitized.py | 0
.../Security/CWE-090/tests/ldap_sanitized_asObj.py | 0
.../{ => experimental}/Security/CWE-090/tests/ldap_unsanitized.py | 0
.../Security/CWE-090/tests/ldap_unsanitized_asObj.py | 0
10 files changed, 0 insertions(+), 0 deletions(-)
rename python/ql/src/{ => experimental}/Security/CWE-090/LDAPInjection.qhelp (100%)
rename python/ql/src/{ => experimental}/Security/CWE-090/LDAPInjection.ql (100%)
rename python/ql/src/{ => experimental}/Security/CWE-090/tests/ldap3_sanitized.py (100%)
rename python/ql/src/{ => experimental}/Security/CWE-090/tests/ldap3_sanitized_asObj.py (100%)
rename python/ql/src/{ => experimental}/Security/CWE-090/tests/ldap3_unsanitized.py (100%)
rename python/ql/src/{ => experimental}/Security/CWE-090/tests/ldap3_unsanitized_asObj.py (100%)
rename python/ql/src/{ => experimental}/Security/CWE-090/tests/ldap_sanitized.py (100%)
rename python/ql/src/{ => experimental}/Security/CWE-090/tests/ldap_sanitized_asObj.py (100%)
rename python/ql/src/{ => experimental}/Security/CWE-090/tests/ldap_unsanitized.py (100%)
rename python/ql/src/{ => experimental}/Security/CWE-090/tests/ldap_unsanitized_asObj.py (100%)
diff --git a/python/ql/src/Security/CWE-090/LDAPInjection.qhelp b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp
similarity index 100%
rename from python/ql/src/Security/CWE-090/LDAPInjection.qhelp
rename to python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp
diff --git a/python/ql/src/Security/CWE-090/LDAPInjection.ql b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
similarity index 100%
rename from python/ql/src/Security/CWE-090/LDAPInjection.ql
rename to python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
diff --git a/python/ql/src/Security/CWE-090/tests/ldap3_sanitized.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap3_sanitized.py
similarity index 100%
rename from python/ql/src/Security/CWE-090/tests/ldap3_sanitized.py
rename to python/ql/src/experimental/Security/CWE-090/tests/ldap3_sanitized.py
diff --git a/python/ql/src/Security/CWE-090/tests/ldap3_sanitized_asObj.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap3_sanitized_asObj.py
similarity index 100%
rename from python/ql/src/Security/CWE-090/tests/ldap3_sanitized_asObj.py
rename to python/ql/src/experimental/Security/CWE-090/tests/ldap3_sanitized_asObj.py
diff --git a/python/ql/src/Security/CWE-090/tests/ldap3_unsanitized.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap3_unsanitized.py
similarity index 100%
rename from python/ql/src/Security/CWE-090/tests/ldap3_unsanitized.py
rename to python/ql/src/experimental/Security/CWE-090/tests/ldap3_unsanitized.py
diff --git a/python/ql/src/Security/CWE-090/tests/ldap3_unsanitized_asObj.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap3_unsanitized_asObj.py
similarity index 100%
rename from python/ql/src/Security/CWE-090/tests/ldap3_unsanitized_asObj.py
rename to python/ql/src/experimental/Security/CWE-090/tests/ldap3_unsanitized_asObj.py
diff --git a/python/ql/src/Security/CWE-090/tests/ldap_sanitized.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized.py
similarity index 100%
rename from python/ql/src/Security/CWE-090/tests/ldap_sanitized.py
rename to python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized.py
diff --git a/python/ql/src/Security/CWE-090/tests/ldap_sanitized_asObj.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized_asObj.py
similarity index 100%
rename from python/ql/src/Security/CWE-090/tests/ldap_sanitized_asObj.py
rename to python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized_asObj.py
diff --git a/python/ql/src/Security/CWE-090/tests/ldap_unsanitized.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized.py
similarity index 100%
rename from python/ql/src/Security/CWE-090/tests/ldap_unsanitized.py
rename to python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized.py
diff --git a/python/ql/src/Security/CWE-090/tests/ldap_unsanitized_asObj.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized_asObj.py
similarity index 100%
rename from python/ql/src/Security/CWE-090/tests/ldap_unsanitized_asObj.py
rename to python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized_asObj.py
From 95a1dae3154367e47fcaaa97cc35dfdb9b1c1fcb Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Thu, 18 Mar 2021 20:36:38 +0100
Subject: [PATCH 03/34] Precision warn and Remove CWE reference
---
.../ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp | 4 ----
python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql | 1 +
2 files changed, 1 insertion(+), 4 deletions(-)
diff --git a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp
index e077ccc3afe4..a570549abfc4 100644
--- a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp
+++ b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp
@@ -23,10 +23,6 @@
Python
LDAP Documentation
-
- CWE-
- 090
-
\ No newline at end of file
diff --git a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
index d6ff1ad56074..55e477be50ae 100644
--- a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
+++ b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
@@ -9,6 +9,7 @@
* external/cwe/cwe-090
*/
+// Determine precision above
import python
import semmle.python.dataflow.new.RemoteFlowSources
import semmle.python.dataflow.new.DataFlow
From 85ec82a389a76ef721d879e7c294550bb56fe8ac Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Sun, 28 Mar 2021 21:07:08 +0200
Subject: [PATCH 04/34] Refactor in progress
---
.../Security/CWE-090/LDAPInjection.ql | 82 +++----------------
.../ldap3_unsanitized.py => ldap3_bad.py} | 0
.../ldap3_sanitized.py => ldap3_good.py} | 0
.../CWE-090/tests/ldap3_sanitized_asObj.py | 57 -------------
.../CWE-090/tests/ldap3_unsanitized_asObj.py | 55 -------------
.../Security/CWE-090/tests/ldap_sanitized.py | 56 -------------
.../CWE-090/tests/ldap_sanitized_asObj.py | 57 -------------
.../CWE-090/tests/ldap_unsanitized.py | 52 ------------
.../CWE-090/tests/ldap_unsanitized_asObj.py | 52 ------------
.../Security/CWE-090/unit_tests/ldap_bad.py | 46 +++++++++++
.../Security/CWE-090/unit_tests/ldap_good.py | 60 ++++++++++++++
.../experimental/semmle/python/Concepts.qll | 43 ++++++++++
.../semmle/python/frameworks/Stdlib.qll | 56 +++++++++++++
.../security/injection/LDAPInjection.qll | 21 +++++
14 files changed, 236 insertions(+), 401 deletions(-)
rename python/ql/src/experimental/Security/CWE-090/{tests/ldap3_unsanitized.py => ldap3_bad.py} (100%)
rename python/ql/src/experimental/Security/CWE-090/{tests/ldap3_sanitized.py => ldap3_good.py} (100%)
delete mode 100644 python/ql/src/experimental/Security/CWE-090/tests/ldap3_sanitized_asObj.py
delete mode 100644 python/ql/src/experimental/Security/CWE-090/tests/ldap3_unsanitized_asObj.py
delete mode 100644 python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized.py
delete mode 100644 python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized_asObj.py
delete mode 100644 python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized.py
delete mode 100644 python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized_asObj.py
create mode 100644 python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_bad.py
create mode 100644 python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_good.py
create mode 100644 python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll
diff --git a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
index 55e477be50ae..778b771f8839 100644
--- a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
+++ b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
@@ -11,77 +11,15 @@
// Determine precision above
import python
-import semmle.python.dataflow.new.RemoteFlowSources
-import semmle.python.dataflow.new.DataFlow
-import semmle.python.dataflow.new.TaintTracking
-import semmle.python.dataflow.new.internal.TaintTrackingPublic
+import experimental.semmle.python.security.injection.LDAPInjection
import DataFlow::PathGraph
-class InitializeSink extends DataFlow::Node {
- InitializeSink() {
- exists(SsaVariable initVar, CallNode searchCall |
- // get variable whose value equals a call to ldap.initialize
- initVar.getDefinition().getImmediateDominator() = Value::named("ldap.initialize").getACall() and
- // get the Call in which the previous variable is used
- initVar.getAUse().getNode() = searchCall.getNode().getFunc().(Attribute).getObject() and
- // restrict that call's attribute (something.this) to match %search%
- searchCall.getNode().getFunc().(Attribute).getName().matches("%search%") and
- // set the third argument (search_filter) as sink
- this.asExpr() = searchCall.getArg(2).getNode()
- // set the first argument (DN) as sink
- // or this.asExpr() = searchCall.getArg(0) // Should this be set?
- )
- }
-}
-
-class ConnectionSink extends DataFlow::Node {
- ConnectionSink() {
- exists(SsaVariable connVar, CallNode searchCall |
- // get variable whose value equals a call to ldap.initialize
- connVar.getDefinition().getImmediateDominator() = Value::named("ldap3.Connection").getACall() and
- // get the Call in which the previous variable is used
- connVar.getAUse().getNode() = searchCall.getNode().getFunc().(Attribute).getObject() and
- // restrict that call's attribute (something.this) to match %search%
- searchCall.getNode().getFunc().(Attribute).getName().matches("%search%") and
- // set the second argument (search_filter) as sink
- this.asExpr() = searchCall.getArg(1).getNode()
- // set the first argument (DN) as sink
- // or this.asExpr() = searchCall.getArg(0) // Should this be set?
- )
- }
-}
-
-class EscapeSanitizer extends DataFlow::Node {
- EscapeSanitizer() {
- exists(Call c |
- (
- // avoid flow through any %escape% function
- c.getFunc().(Attribute).getName().matches("%escape%") or // something.%escape%()
- c.getFunc().(Name).getId().matches("%escape%") // %escape%()
- ) and
- this.asExpr() = c
- )
- }
-}
-
-class LDAPInjectionSink extends DataFlow::Node {
- LDAPInjectionSink() {
- this instanceof InitializeSink or
- this instanceof ConnectionSink
- }
-}
-
-class LDAPInjectionFlowConfig extends TaintTracking::Configuration {
- LDAPInjectionFlowConfig() { this = "LDAPInjectionFlowConfig" }
-
- override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
-
- override predicate isSink(DataFlow::Node sink) { sink instanceof LDAPInjectionSink }
-
- override predicate isSanitizer(DataFlow::Node sanitizer) { sanitizer instanceof EscapeSanitizer }
-}
-
-from LDAPInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
-where config.hasFlowPath(source, sink)
-select sink.getNode(), source, sink, "$@ LDAP query executes $@.", sink.getNode(), "This",
- source.getNode(), "a user-provided value"
+from
+ LDAPInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink,
+ LDAPQuery castedSink
+where
+ config.hasFlowPath(source, sink) and
+ castedSink = sink.getNode()
+select sink.getNode(), source, sink, "$@ LDAP query executes $@ as a $@ probably leaking $@.",
+ sink.getNode(), "This", source.getNode(), "a user-provided value", castedSink.getLDAPNode(),
+ castedSink.getLDAPPart(), castedSink.getAttrList(), "this attribute(s)"
diff --git a/python/ql/src/experimental/Security/CWE-090/tests/ldap3_unsanitized.py b/python/ql/src/experimental/Security/CWE-090/ldap3_bad.py
similarity index 100%
rename from python/ql/src/experimental/Security/CWE-090/tests/ldap3_unsanitized.py
rename to python/ql/src/experimental/Security/CWE-090/ldap3_bad.py
diff --git a/python/ql/src/experimental/Security/CWE-090/tests/ldap3_sanitized.py b/python/ql/src/experimental/Security/CWE-090/ldap3_good.py
similarity index 100%
rename from python/ql/src/experimental/Security/CWE-090/tests/ldap3_sanitized.py
rename to python/ql/src/experimental/Security/CWE-090/ldap3_good.py
diff --git a/python/ql/src/experimental/Security/CWE-090/tests/ldap3_sanitized_asObj.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap3_sanitized_asObj.py
deleted file mode 100644
index c7dbb3450588..000000000000
--- a/python/ql/src/experimental/Security/CWE-090/tests/ldap3_sanitized_asObj.py
+++ /dev/null
@@ -1,57 +0,0 @@
-from ldap3 import Server, Connection
-from ldap3.utils.conv import escape_filter_chars
-from flask import request, Flask
-
-app = Flask(__name__)
-
-
-@app.route("/tainted_var")
-def tainted_var():
- sanitized_dn = "dc=%s" % request.args['dc']
- sanitized_filter = "(&(objectClass=*)(uid=%s))" % escape_filter_chars(
- request.args['username'])
-
- srv = Server('localhost', port=1337)
- conn = Connection(srv, user=sanitized_dn, auto_bind=True)
- conn.search(sanitized_dn, sanitized_filter)
- return conn.response
-
-
-@app.route("/var_tainted")
-def var_tainted():
- sanitized_dn = request.args['dc']
- sanitized_filter = request.args['username']
-
- dn = "dc=%s" % sanitized_dn
- search_filter = "(&(objectClass=*)(uid=%s))" % escape_filter_chars(sanitized_filter)
-
- srv = Server('localhost', port=1337)
- conn = Connection(srv, user=dn, auto_bind=True)
- conn.search(dn, search_filter)
- return conn.response
-
-
-@app.route("/direct")
-def direct():
- srv = Server('localhost', port=1337)
- conn = Connection(srv, user="dc=%s" % request.args['dc'], auto_bind=True)
- conn.search("dc=%s" % request.args['dc'], "(&(objectClass=*)(uid=%s))" %
- escape_filter_chars(request.args['username']))
- return conn.response
-
-
-@app.route("/with_2")
-def with_2():
- sanitized_dn = request.args['dc']
- sanitized_filter = escape_filter_chars(request.args['username'])
-
- dn = "dc=%s" % sanitized_dn
- search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter
-
- srv = Server('localhost', port=1337)
- with Connection(server, auto_bind=True) as conn:
- conn.search(dn, search_filter)
- return conn.response
-
-# if __name__ == "__main__":
-# app.run(debug=True)
diff --git a/python/ql/src/experimental/Security/CWE-090/tests/ldap3_unsanitized_asObj.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap3_unsanitized_asObj.py
deleted file mode 100644
index 634ee992f3e5..000000000000
--- a/python/ql/src/experimental/Security/CWE-090/tests/ldap3_unsanitized_asObj.py
+++ /dev/null
@@ -1,55 +0,0 @@
-from ldap3 import Server, Connection
-from flask import request, Flask
-
-app = Flask(__name__)
-
-
-@app.route("/tainted_var")
-def tainted_var():
- unsanitized_dn = "dc=%s" % request.args['dc']
- unsanitized_filter = "(&(objectClass=*)(uid=%s))" % request.args['username']
-
- srv = Server('localhost', port=1337)
- conn = Connection(srv, user=unsanitized_dn, auto_bind=True)
- conn.search(unsanitized_dn, unsanitized_filter)
- return conn.response
-
-
-@app.route("/var_tainted")
-def var_tainted():
- unsanitized_dn = request.args['dc']
- unsanitized_filter = request.args['username']
-
- dn = "dc=%s" % unsanitized_dn
- search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter
-
- srv = Server('localhost', port=1337)
- conn = Connection(srv, user=dn, auto_bind=True)
- conn.search(dn, search_filter)
- return conn.response
-
-
-@app.route("/direct")
-def direct():
- srv = Server('localhost', port=1337)
- conn = Connection(srv, user="dc=%s" % request.args['dc'], auto_bind=True)
- conn.search(
- "dc=%s" % request.args['dc'], "(&(objectClass=*)(uid=%s))" % request.args['username'])
- return conn.response
-
-
-@app.route("/with_2")
-def with_2():
- unsanitized_dn = request.args['dc']
- unsanitized_filter = request.args['username']
-
- dn = "dc=%s" % unsanitized_dn
- search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter
-
- srv = Server('localhost', port=1337)
- with Connection(server, auto_bind=True) as conn:
- conn.search(dn, search_filter)
- return conn.response
-
-# if __name__ == "__main__":
-# app.run(debug=True)
diff --git a/python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized.py
deleted file mode 100644
index 83bafa791956..000000000000
--- a/python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized.py
+++ /dev/null
@@ -1,56 +0,0 @@
-from flask import request, Flask
-import ldap
-import ldap.filter
-import ldap.dn
-
-app = Flask(__name__)
-
-
-@app.route("/tainted_var")
-def tainted_var():
- sanitized_dn = "dc=%s" % ldap.dn.escape_dn_chars(request.args['dc'])
- sanitized_filter = "(&(objectClass=*)(uid=%s))" % ldap.filter.escape_filter_chars(
- request.args['username'])
-
- ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
- user = ldap_connection.search_s(
- sanitized_dn, ldap.SCOPE_SUBTREE, sanitized_filter)
- return user[0]
-
-
-@app.route("/var_tainted")
-def var_tainted():
- sanitized_dn = request.args['dc']
- sanitized_filter = request.args['username']
-
- dn = "dc=%s" % ldap.dn.escape_dn_chars(sanitized_dn)
- search_filter = "(&(objectClass=*)(uid=%s))" % ldap.filter.escape_filter_chars(sanitized_filter)
-
- ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
- user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter)
- return user[0]
-
-
-@app.route("/direct")
-def direct():
- ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
- user = ldap_connection.search_s("dc=%s" % ldap.dn.escape_dn_chars(
- request.args['dc']), ldap.SCOPE_SUBTREE, "(&(objectClass=*)(uid=%s))" % ldap.filter.escape_filter_chars(request.args['username']))
- return user[0]
-
-
-@app.route("/with_")
-def with_():
- sanitized_dn = ldap.dn.escape_dn_chars(request.args['dc'])
- sanitized_filter = ldap.filter.escape_filter_chars(
- request.args['username'])
-
- dn = "dc=%s" % sanitized_dn
- search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter
-
- with ldap.initialize("ldap://127.0.0.1:1337") as ldap_connection:
- user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter)
- return user[0]
-
-# if __name__ == "__main__":
-# app.run(debug=True)
diff --git a/python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized_asObj.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized_asObj.py
deleted file mode 100644
index 1c9662a4bcf8..000000000000
--- a/python/ql/src/experimental/Security/CWE-090/tests/ldap_sanitized_asObj.py
+++ /dev/null
@@ -1,57 +0,0 @@
-from flask import request, Flask
-from ldap import initialize
-import ldap.filter
-import ldap.dn
-
-app = Flask(__name__)
-
-
-@app.route("/tainted_var_2")
-def tainted_var_2():
- sanitized_dn = "dc=%s" % ldap.dn.escape_dn_chars(request.args['dc'])
- sanitized_filter = "(&(objectClass=*)(uid=%s))" % ldap.filter.escape_filter_chars(
- request.args['username'])
-
- ldap_connection = initialize("ldap://127.0.0.1:1337")
- user = ldap_connection.search_s(
- sanitized_dn, ldap.SCOPE_SUBTREE, sanitized_filter)
- return user[0]
-
-
-@app.route("/var_tainted_2")
-def var_tainted_2():
- sanitized_dn = ldap.dn.escape_dn_chars(request.args['dc'])
- sanitized_filter = ldap.filter.escape_filter_chars(
- request.args['username'])
-
- dn = "dc=%s" % sanitized_dn
- search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter
-
- ldap_connection = initialize("ldap://127.0.0.1:1337")
- user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter)
- return user[0]
-
-
-@app.route("/direct_2")
-def direct_2():
- ldap_connection = initialize("ldap://127.0.0.1:1337")
- user = ldap_connection.search_s("dc=%s" % ldap.dn.escape_dn_chars(
- request.args['dc']), ldap.SCOPE_SUBTREE, "(&(objectClass=*)(uid=%s))" % ldap.filter.escape_filter_chars(request.args['username']))
- return user[0]
-
-
-@app.route("/with_2")
-def with_2():
- sanitized_dn = ldap.dn.escape_dn_chars(request.args['dc'])
- sanitized_filter = ldap.filter.escape_filter_chars(
- request.args['username'])
-
- dn = "dc=%s" % sanitized_dn
- search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter
-
- with initialize("ldap://127.0.0.1:1337") as ldap_connection:
- user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter)
- return user[0]
-
-# if __name__ == "__main__":
-# app.run(debug=True)
diff --git a/python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized.py
deleted file mode 100644
index 8b22596b578d..000000000000
--- a/python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from flask import request, Flask
-import ldap
-
-app = Flask(__name__)
-
-
-@app.route("/tainted_var")
-def tainted_var():
- unsanitized_dn = "dc=%s" % request.args['dc']
- unsanitized_filter = "(&(objectClass=*)(uid=%s))" % request.args['username']
-
- ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
- user = ldap_connection.search_s(
- unsanitized_dn, ldap.SCOPE_SUBTREE, unsanitized_filter)
- return user[0]
-
-
-@app.route("/var_tainted")
-def var_tainted():
- unsanitized_dn = request.args['dc']
- unsanitized_filter = request.args['username']
-
- dn = "dc=%s" % unsanitized_dn
- search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter
-
- ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
- user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter)
- return user[0]
-
-
-@app.route("/direct")
-def direct():
- ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
- user = ldap_connection.search_s(
- "dc=%s" % request.args['dc'], ldap.SCOPE_SUBTREE, "(&(objectClass=*)(uid=%s))" % request.args['username'])
- return user[0]
-
-
-@app.route("/with_")
-def with_():
- sanitized_dn = request.args['dc']
- sanitized_filter = request.args['username']
-
- dn = "dc=%s" % sanitized_dn
- search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter
-
- with ldap.initialize("ldap://127.0.0.1:1337") as ldap_connection:
- user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter)
- return user[0]
-
-# if __name__ == "__main__":
-# app.run(debug=True)
diff --git a/python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized_asObj.py b/python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized_asObj.py
deleted file mode 100644
index 49774dda2ec9..000000000000
--- a/python/ql/src/experimental/Security/CWE-090/tests/ldap_unsanitized_asObj.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from flask import request, Flask
-from ldap import initialize
-
-app = Flask(__name__)
-
-
-@app.route("/tainted_var")
-def tainted_var():
- unsanitized_dn = "dc=%s" % request.args['dc']
- unsanitized_filter = "(&(objectClass=*)(uid=%s))" % request.args['username']
-
- ldap_connection = initialize("ldap://127.0.0.1:1337")
- user = ldap_connection.search_s(
- unsanitized_dn, ldap.SCOPE_SUBTREE, unsanitized_filter)
- return user[0]
-
-
-@app.route("/var_tainted")
-def var_tainted():
- unsanitized_dn = request.args['dc']
- unsanitized_filter = request.args['username']
-
- dn = "dc=%s" % unsanitized_dn
- search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter
-
- ldap_connection = initialize("ldap://127.0.0.1:1337")
- user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter)
- return user[0]
-
-
-@app.route("/direct")
-def direct():
- ldap_connection = initialize("ldap://127.0.0.1:1337")
- user = ldap_connection.search_s(
- "dc=%s" % request.args['dc'], ldap.SCOPE_SUBTREE, "(&(objectClass=*)(uid=%s))" % request.args['username'])
- return user[0]
-
-
-@app.route("/with_2")
-def with_2():
- sanitized_dn = request.args['dc']
- sanitized_filter = request.args['username']
-
- dn = "dc=%s" % sanitized_dn
- search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter
-
- with initialize("ldap://127.0.0.1:1337") as ldap_connection:
- user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter)
- return user[0]
-
-# if __name__ == "__main__":
-# app.run(debug=True)
diff --git a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_bad.py b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_bad.py
new file mode 100644
index 000000000000..a409a38e019f
--- /dev/null
+++ b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_bad.py
@@ -0,0 +1,46 @@
+from flask import request, Flask
+import ldap
+
+app = Flask(__name__)
+
+
+@app.route("/normal")
+def normal():
+ unsafe_dn = "dc=%s" % request.args['dc']
+ unsafe_filter = "(user=%s)" % request.args['username']
+
+ ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
+ user = ldap_connection.search_s(
+ unsafe_dn, ldap.SCOPE_SUBTREE, unsafe_filter, ["testAttr1", "testAttr2"])
+
+
+@app.route("/direct")
+def direct():
+ unsafe_dn = "dc=%s" % request.args['dc']
+ unsafe_filter = "(user=%s)" % request.args['username']
+
+ user = ldap.initialize("ldap://127.0.0.1:1337").search_s(
+ unsafe_dn, ldap.SCOPE_SUBTREE, unsafe_filter, ["testAttr1", "testAttr2"])
+
+
+@app.route("/normal_argbyname")
+def normal_argbyname():
+ unsafe_dn = "dc=%s" % request.args['dc']
+ unsafe_filter = "(user=%s)" % request.args['username']
+
+ ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
+ user = ldap_connection.search_s(
+ unsafe_dn, ldap.SCOPE_SUBTREE, attrlist=["testAttr1", "testAttr2"], filterstr=unsafe_filter)
+
+
+@app.route("/direct_argbyname")
+def direct_argbyname():
+ unsafe_dn = "dc=%s" % request.args['dc']
+ unsafe_filter = "(user=%s)" % request.args['username']
+
+ user = ldap.initialize("ldap://127.0.0.1:1337").search_s(
+ unsafe_dn, ldap.SCOPE_SUBTREE, attrlist=["testAttr1", "testAttr2"], filterstr=unsafe_filter)
+
+
+# if __name__ == "__main__":
+# app.run(debug=True)
diff --git a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_good.py b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_good.py
new file mode 100644
index 000000000000..1d04505fa286
--- /dev/null
+++ b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_good.py
@@ -0,0 +1,60 @@
+from flask import request, Flask
+import ldap
+import ldap.filter
+import ldap.dn
+
+app = Flask(__name__)
+
+
+@app.route("/normal")
+def normal():
+ unsafe_dn = "dc=%s" % request.args['dc']
+ unsafe_filter = "(user=%s)" % request.args['username']
+
+ safe_dn = ldap.dn.escape_dn_chars(unsafe_dn)
+ safe_filter = ldap.filter.escape_filter_chars(unsafe_filter)
+
+ ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
+ user = ldap_connection.search_s(
+ safe_dn, ldap.SCOPE_SUBTREE, safe_filter, ["testAttr1", "testAttr2"])
+
+
+@app.route("/direct")
+def direct():
+ unsafe_dn = "dc=%s" % request.args['dc']
+ unsafe_filter = "(user=%s)" % request.args['username']
+
+ safe_dn = ldap.dn.escape_dn_chars(unsafe_dn)
+ safe_filter = ldap.filter.escape_filter_chars(unsafe_filter)
+
+ user = ldap.initialize("ldap://127.0.0.1:1337").search_s(
+ safe_dn, ldap.SCOPE_SUBTREE, safe_filter, ["testAttr1", "testAttr2"])
+
+
+@app.route("/normal_argbyname")
+def normal_argbyname():
+ unsafe_dn = "dc=%s" % request.args['dc']
+ unsafe_filter = "(user=%s)" % request.args['username']
+
+ safe_dn = ldap.dn.escape_dn_chars(unsafe_dn)
+ safe_filter = ldap.filter.escape_filter_chars(unsafe_filter)
+
+ ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
+ user = ldap_connection.search_s(
+ safe_dn, ldap.SCOPE_SUBTREE, attrlist=["testAttr1", "testAttr2"], filterstr=safe_filter)
+
+
+@app.route("/direct_argbyname")
+def direct_argbyname():
+ unsafe_dn = "dc=%s" % request.args['dc']
+ unsafe_filter = "(user=%s)" % request.args['username']
+
+ safe_dn = ldap.dn.escape_dn_chars(unsafe_dn)
+ safe_filter = ldap.filter.escape_filter_chars(unsafe_filter)
+
+ user = ldap.initialize("ldap://127.0.0.1:1337").search_s(
+ safe_dn, ldap.SCOPE_SUBTREE, attrlist=["testAttr1", "testAttr2"], filterstr=safe_filter)
+
+
+# if __name__ == "__main__":
+# app.run(debug=True)
diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll
index 904b7967ee87..e2b278e40da4 100644
--- a/python/ql/src/experimental/semmle/python/Concepts.qll
+++ b/python/ql/src/experimental/semmle/python/Concepts.qll
@@ -13,3 +13,46 @@ private import semmle.python.dataflow.new.DataFlow
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.dataflow.new.TaintTracking
private import experimental.semmle.python.Frameworks
+private import semmle.python.ApiGraphs
+
+/**
+ * To-Do
+ *
+ * LDAPQuery -> collect functions executing a search filter/DN
+ * LDAPEscape -> collect functions escaping a search filter/DN
+ */
+module LDAPQuery {
+ abstract class Range extends DataFlow::Node {
+ abstract DataFlow::Node getLDAPNode();
+
+ abstract string getLDAPPart();
+
+ abstract DataFlow::Node getAttrs();
+ }
+}
+
+class LDAPQuery extends DataFlow::Node {
+ LDAPQuery::Range range;
+
+ LDAPQuery() { this = range }
+
+ DataFlow::Node getLDAPNode() { result = range.getLDAPNode() }
+
+ string getLDAPPart() { result = range.getLDAPPart() }
+
+ DataFlow::Node getAttrs() { result = range.getAttrs() }
+}
+
+module LDAPEscape {
+ abstract class Range extends DataFlow::Node {
+ abstract DataFlow::Node getEscapeNode();
+ }
+}
+
+class LDAPEscape extends DataFlow::Node {
+ LDAPEscape::Range range;
+
+ LDAPEscape() { this = range }
+
+ DataFlow::Node getEscapeNode() { result = range.getEscapeNode() }
+}
diff --git a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
index 420caf0d73bb..1ef42edbdbb2 100644
--- a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
+++ b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
@@ -9,3 +9,59 @@ private import semmle.python.dataflow.new.TaintTracking
private import semmle.python.dataflow.new.RemoteFlowSources
private import experimental.semmle.python.Concepts
private import semmle.python.ApiGraphs
+
+private module LDAP {
+ private module LDAP2 {
+ private class LDAP2QueryMethods extends string {
+ LDAP2QueryMethods() {
+ this in ["search", "search_s", "search_st", "search_ext", "search_ext_s"]
+ }
+ }
+
+ private class LDAP2Query extends DataFlow::CallCfgNode, LDAPQuery::Range {
+ DataFlow::Node ldapNode;
+ string ldapPart;
+ DataFlow::Node attrs;
+
+ LDAP2Query() {
+ exists(DataFlow::AttrRead searchMethod, DataFlow::CallCfgNode initCall |
+ this.getFunction() = searchMethod and
+ initCall = API::moduleImport("ldap").getMember("initialize").getACall() and
+ initCall = searchMethod.getObject().getALocalSource() and
+ searchMethod.getAttributeName() instanceof LDAP2QueryMethods and
+ (
+ (
+ ldapNode = this.getArg(2) or
+ ldapNode = this.getArgByName("filterstr")
+ ) and
+ ldapPart = "search_filter"
+ or
+ ldapNode = this.getArg(0) and
+ ldapPart = "DN"
+ ) and
+ ( // what if they're not set?
+ attrs = this.getArg(3) or
+ attrs = this.getArgByName("attrlist")
+ )
+ )
+ }
+
+ override DataFlow::Node getLDAPNode() { result = ldapNode }
+
+ override string getLDAPPart() { result = ldapPart }
+
+ override DataFlow::Node getAttrs() { result = attrs }
+ }
+
+ private class LDAP2EscapeDN extends DataFlow::CallCfgNode, LDAPEscape::Range {
+ DataFlow::Node escapeNode;
+
+ LDAP2EscapeDN() {
+ this = API::moduleImport("ldap").getMember("dn").getMember("escape_dn_chars").getACall() and
+ escapeNode = this.getArg(0)
+ }
+
+ override DataFlow::Node getEscapeNode() { result = escapeNode }
+ }
+ }
+}
diff --git a/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll b/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll
new file mode 100644
index 000000000000..f5fa2306f310
--- /dev/null
+++ b/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll
@@ -0,0 +1,21 @@
+/**
+ * Provides a taint-tracking configuration for detecting LDAP injection vulnerabilities
+ */
+
+import python
+import experimental.semmle.python.Concepts
+import semmle.python.dataflow.new.DataFlow
+import semmle.python.dataflow.new.TaintTracking
+import semmle.python.dataflow.new.RemoteFlowSources
+
+/**
+ * A taint-tracking configuration for detecting regular expression injections.
+ */
+class LDAPInjectionFlowConfig extends TaintTracking::Configuration {
+ LDAPInjectionFlowConfig() { this = "LDAPInjectionFlowConfig" }
+
+ override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
+
+ override predicate isSink(DataFlow::Node sink) { sink = any(LDAPQuery lQ).getLDAPNode() }
+ // override predicate isSanitizer(DataFlow::Node sanitizer) { sanitizer instanceof RemoteFlowSource } // any(LDAPEscape ldapEsc).getEscapeNode() }
+}
From ad36bea9d4ce61f804cdbfedddc5d3bebecdf1f6 Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Mon, 29 Mar 2021 09:14:35 +0200
Subject: [PATCH 05/34] Refactor LDAP3 stuff (untested)
---
.../Security/CWE-090/LDAPInjection.ql | 5 +-
.../experimental/semmle/python/Concepts.qll | 6 --
.../semmle/python/frameworks/Stdlib.qll | 95 +++++++++++++++++--
.../security/injection/LDAPInjection.qll | 5 +-
4 files changed, 94 insertions(+), 17 deletions(-)
diff --git a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
index 778b771f8839..7ce3a118918c 100644
--- a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
+++ b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
@@ -19,7 +19,8 @@ from
LDAPQuery castedSink
where
config.hasFlowPath(source, sink) and
- castedSink = sink.getNode()
+ castedSink = sink.getNode() //and
+// if exists(castedSink.getAttrs()) then
select sink.getNode(), source, sink, "$@ LDAP query executes $@ as a $@ probably leaking $@.",
sink.getNode(), "This", source.getNode(), "a user-provided value", castedSink.getLDAPNode(),
- castedSink.getLDAPPart(), castedSink.getAttrList(), "this attribute(s)"
+ castedSink.getLDAPPart(), castedSink.getAttrs(), "this attribute(s)"
diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll
index e2b278e40da4..e0f5c1bdec34 100644
--- a/python/ql/src/experimental/semmle/python/Concepts.qll
+++ b/python/ql/src/experimental/semmle/python/Concepts.qll
@@ -15,12 +15,6 @@ private import semmle.python.dataflow.new.TaintTracking
private import experimental.semmle.python.Frameworks
private import semmle.python.ApiGraphs
-/**
- * To-Do
- *
- * LDAPQuery -> collect functions executing a search filter/DN
- * LDAPEscape -> collect functions escaping a search filter/DN
- */
module LDAPQuery {
abstract class Range extends DataFlow::Node {
abstract DataFlow::Node getLDAPNode();
diff --git a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
index 1ef42edbdbb2..aa9013a76c06 100644
--- a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
+++ b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
@@ -30,18 +30,14 @@ private module LDAP {
initCall = searchMethod.getObject().getALocalSource() and
searchMethod.getAttributeName() instanceof LDAP2QueryMethods and
(
+ ldapNode = this.getArg(0) and
+ ldapPart = "DN"
+ or
(
ldapNode = this.getArg(2) or
ldapNode = this.getArgByName("filterstr")
) and
ldapPart = "search_filter"
- or
- ldapNode = this.getArg(0) and
- ldapPart = "DN"
- ) and
- ( // what if they're not set?
- attrs = this.getArg(3) or
- attrs = this.getArgByName("attrlist")
)
)
}
@@ -50,7 +46,9 @@ private module LDAP {
override string getLDAPPart() { result = ldapPart }
- override DataFlow::Node getAttrs() { result = attrs }
+ override DataFlow::Node getAttrs() {
+ result = this.getArg(3) or result = this.getArgByName("attrlist")
+ }
}
private class LDAP2EscapeDN extends DataFlow::CallCfgNode, LDAPEscape::Range {
@@ -63,5 +61,86 @@ private module LDAP {
override DataFlow::Node getEscapeNode() { result = escapeNode }
}
+
+ private class LDAP2EscapeFilter extends DataFlow::CallCfgNode, LDAPEscape::Range {
+ DataFlow::Node escapeNode;
+
+ LDAP2EscapeFilter() {
+ this =
+ API::moduleImport("ldap").getMember("filter").getMember("escape_filter_chars").getACall() and
+ escapeNode = this.getArg(0)
+ }
+
+ override DataFlow::Node getEscapeNode() { result = escapeNode }
+ }
+ }
+
+ private module LDAP3 {
+ private class LDAP3QueryMethods extends string {
+ // pending to dig into this although https://github.com/cannatag/ldap3/blob/21001d9087c0d24c399eec433a261c455b7bc97f/ldap3/core/connection.py#L760
+ LDAP3QueryMethods() { this in ["search"] }
+ }
+
+ private class LDAP3Query extends DataFlow::CallCfgNode, LDAPQuery::Range {
+ DataFlow::Node ldapNode;
+ string ldapPart;
+ DataFlow::Node attrs;
+
+ LDAP3Query() {
+ exists(DataFlow::AttrRead searchMethod, DataFlow::CallCfgNode connCall |
+ this.getFunction() = searchMethod and
+ connCall = API::moduleImport("ldap3").getMember("Connection").getACall() and
+ connCall = searchMethod.getObject().getALocalSource() and
+ searchMethod.getAttributeName() instanceof LDAP3QueryMethods and
+ (
+ ldapNode = this.getArg(0) and
+ ldapPart = "DN"
+ or
+ ldapNode = this.getArg(1) and
+ ldapPart = "search_filter"
+ )
+ )
+ }
+
+ override DataFlow::Node getLDAPNode() { result = ldapNode }
+
+ override string getLDAPPart() { result = ldapPart }
+
+ override DataFlow::Node getAttrs() {
+ result = this.getArg(3) or result = this.getArgByName("attributes")
+ }
+ }
+
+ private class LDAP3EscapeDN extends DataFlow::CallCfgNode, LDAPEscape::Range {
+ DataFlow::Node escapeNode;
+
+ LDAP3EscapeDN() {
+ this =
+ API::moduleImport("ldap3")
+ .getMember("utils")
+ .getMember("dn")
+ .getMember("escape_rdn")
+ .getACall() and
+ escapeNode = this.getArg(0)
+ }
+
+ override DataFlow::Node getEscapeNode() { result = escapeNode }
+ }
+
+ private class LDAP3EscapeFilter extends DataFlow::CallCfgNode, LDAPEscape::Range {
+ DataFlow::Node escapeNode;
+
+ LDAP3EscapeFilter() {
+ this =
+ API::moduleImport("ldap3")
+ .getMember("utils")
+ .getMember("conv")
+ .getMember("escape_filter_chars")
+ .getACall() and
+ escapeNode = this.getArg(0)
+ }
+
+ override DataFlow::Node getEscapeNode() { result = escapeNode }
+ }
}
}
diff --git a/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll b/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll
index f5fa2306f310..db8bdda7db61 100644
--- a/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll
+++ b/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll
@@ -17,5 +17,8 @@ class LDAPInjectionFlowConfig extends TaintTracking::Configuration {
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
override predicate isSink(DataFlow::Node sink) { sink = any(LDAPQuery lQ).getLDAPNode() }
- // override predicate isSanitizer(DataFlow::Node sanitizer) { sanitizer instanceof RemoteFlowSource } // any(LDAPEscape ldapEsc).getEscapeNode() }
+
+ override predicate isSanitizer(DataFlow::Node sanitizer) {
+ sanitizer = any(LDAPEscape lE).getEscapeNode()
+ }
}
From 8223539f0c0988e4d1ef79710c80c82533b12901 Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Mon, 29 Mar 2021 23:28:28 +0200
Subject: [PATCH 06/34] Add a test without attributes
---
.../Security/CWE-090/unit_tests/ldap_bad.py | 10 ++++++++++
.../Security/CWE-090/unit_tests/ldap_good.py | 13 +++++++++++++
2 files changed, 23 insertions(+)
diff --git a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_bad.py b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_bad.py
index a409a38e019f..011f3b828654 100644
--- a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_bad.py
+++ b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_bad.py
@@ -14,6 +14,16 @@ def normal():
unsafe_dn, ldap.SCOPE_SUBTREE, unsafe_filter, ["testAttr1", "testAttr2"])
+@app.route("/normal_noAttrs")
+def normal_noAttrs():
+ unsafe_dn = "dc=%s" % request.args['dc']
+ unsafe_filter = "(user=%s)" % request.args['username']
+
+ ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
+ user = ldap_connection.search_s(
+ unsafe_dn, ldap.SCOPE_SUBTREE, unsafe_filter)
+
+
@app.route("/direct")
def direct():
unsafe_dn = "dc=%s" % request.args['dc']
diff --git a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_good.py b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_good.py
index 1d04505fa286..eda3883da6dc 100644
--- a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_good.py
+++ b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_good.py
@@ -19,6 +19,19 @@ def normal():
safe_dn, ldap.SCOPE_SUBTREE, safe_filter, ["testAttr1", "testAttr2"])
+@app.route("/normal_noAttrs")
+def normal_noAttrs():
+ unsafe_dn = "dc=%s" % request.args['dc']
+ unsafe_filter = "(user=%s)" % request.args['username']
+
+ safe_dn = ldap.dn.escape_dn_chars(unsafe_dn)
+ safe_filter = ldap.filter.escape_filter_chars(unsafe_filter)
+
+ ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
+ user = ldap_connection.search_s(
+ safe_dn, ldap.SCOPE_SUBTREE, safe_filter)
+
+
@app.route("/direct")
def direct():
unsafe_dn = "dc=%s" % request.args['dc']
From 3cda2e52073b72dfc126f7863041a85005d818ae Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Mon, 29 Mar 2021 23:39:49 +0200
Subject: [PATCH 07/34] Polish up ldap3 tests
---
.../Security/CWE-090/ldap3_bad.py | 56 ------------------
.../Security/CWE-090/ldap3_good.py | 58 -------------------
.../Security/CWE-090/unit_tests/ldap3_bad.py | 38 ++++++++++++
.../Security/CWE-090/unit_tests/ldap3_good.py | 49 ++++++++++++++++
4 files changed, 87 insertions(+), 114 deletions(-)
delete mode 100644 python/ql/src/experimental/Security/CWE-090/ldap3_bad.py
delete mode 100644 python/ql/src/experimental/Security/CWE-090/ldap3_good.py
create mode 100644 python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_bad.py
create mode 100644 python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_good.py
diff --git a/python/ql/src/experimental/Security/CWE-090/ldap3_bad.py b/python/ql/src/experimental/Security/CWE-090/ldap3_bad.py
deleted file mode 100644
index fcd00c0269ca..000000000000
--- a/python/ql/src/experimental/Security/CWE-090/ldap3_bad.py
+++ /dev/null
@@ -1,56 +0,0 @@
-import ldap3
-from flask import request, Flask
-
-app = Flask(__name__)
-
-
-@app.route("/tainted_var")
-def tainted_var():
- unsanitized_dn = "dc=%s" % request.args['dc']
- unsanitized_filter = "(&(objectClass=*)(uid=%s))" % request.args['username']
-
- srv = ldap3.Server('localhost', port=1337)
- conn = ldap3.Connection(srv, user=unsanitized_dn, auto_bind=True)
- conn.search(unsanitized_dn, unsanitized_filter)
- return conn.response
-
-
-@app.route("/var_tainted")
-def var_tainted():
- unsanitized_dn = request.args['dc']
- unsanitized_filter = request.args['username']
-
- dn = "dc=%s" % unsanitized_dn
- search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter
-
- srv = ldap3.Server('localhost', port=1337)
- conn = ldap3.Connection(srv, user=dn, auto_bind=True)
- conn.search(dn, search_filter)
- return conn.response
-
-
-@app.route("/direct")
-def direct():
- srv = ldap3.Server('localhost', port=1337)
- conn = ldap3.Connection(srv, user="dc=%s" %
- request.args['dc'], auto_bind=True)
- conn.search("dc=%s" % unsanitized_dn,
- "(&(objectClass=*)(uid=%s))" % request.args['username'])
- return conn.response
-
-
-@app.route("/with_")
-def with_():
- unsanitized_dn = request.args['dc']
- unsanitized_filter = request.args['username']
-
- dn = "dc=%s" % unsanitized_dn
- search_filter = "(&(objectClass=*)(uid=%s))" % unsanitized_filter
-
- srv = ldap3.Server('localhost', port=1337)
- with ldap3.Connection(server, auto_bind=True) as conn:
- conn.search(dn, search_filter)
- return conn.response
-
-# if __name__ == "__main__":
-# app.run(debug=True)
diff --git a/python/ql/src/experimental/Security/CWE-090/ldap3_good.py b/python/ql/src/experimental/Security/CWE-090/ldap3_good.py
deleted file mode 100644
index 05bc1e24f716..000000000000
--- a/python/ql/src/experimental/Security/CWE-090/ldap3_good.py
+++ /dev/null
@@ -1,58 +0,0 @@
-import ldap3
-from ldap3.utils.conv import escape_filter_chars
-from flask import request, Flask
-
-app = Flask(__name__)
-
-
-@app.route("/tainted_var")
-def tainted_var():
- sanitized_dn = "dc=%s" % request.args['dc']
- sanitized_filter = "(&(objectClass=*)(uid=%s))" % escape_filter_chars(
- request.args['username'])
-
- srv = ldap3.Server('localhost', port=1337)
- conn = ldap3.Connection(srv, user=sanitized_dn, auto_bind=True)
- conn.search(sanitized_dn, sanitized_filter)
- return conn.response
-
-
-@app.route("/var_tainted")
-def var_tainted():
- sanitized_dn = request.args['dc']
- sanitized_filter = request.args['username']
-
- dn = "dc=%s" % sanitized_dn
- search_filter = "(&(objectClass=*)(uid=%s))" % escape_filter_chars(sanitized_filter)
-
- srv = ldap3.Server('localhost', port=1337)
- conn = ldap3.Connection(srv, user=dn, auto_bind=True)
- conn.search(dn, search_filter)
- return conn.response
-
-
-@app.route("/direct")
-def direct():
- srv = ldap3.Server('localhost', port=1337)
- conn = ldap3.Connection(srv, user="dc=%s" %
- request.args['dc'], auto_bind=True)
- conn.search("dc=%s" % request.args['dc'], "(&(objectClass=*)(uid=%s))" %
- escape_filter_chars(request.args['username']))
- return conn.response
-
-
-@ app.route("/with_")
-def with_():
- sanitized_dn = request.args['dc']
- sanitized_filter = escape_filter_chars(request.args['username'])
-
- dn = "dc=%s" % sanitized_dn
- search_filter = "(&(objectClass=*)(uid=%s))" % sanitized_filter
-
- srv = ldap3.Server('localhost', port=1337)
- with ldap3.Connection(server, auto_bind=True) as conn:
- conn.search(dn, search_filter)
- return conn.response
-
-# if __name__ == "__main__":
-# app.run(debug=True)
diff --git a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_bad.py b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_bad.py
new file mode 100644
index 000000000000..76b7f309bd17
--- /dev/null
+++ b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_bad.py
@@ -0,0 +1,38 @@
+from flask import request, Flask
+import ldap3
+
+app = Flask(__name__)
+
+
+@app.route("/normal")
+def normal():
+ unsafe_dn = "dc=%s" % request.args['dc']
+ unsafe_filter = "(user=%s)" % request.args['username']
+
+ srv = ldap3.Server('ldap://127.0.0.1', port=1337)
+ conn = ldap3.Connection(srv, user=unsafe_dn, auto_bind=True)
+ conn.search(unsafe_dn, unsafe_filter, attributes=[
+ "testAttr1", "testAttr2"])
+
+
+@app.route("/normal_noAttrs")
+def normal_noAttrs():
+ unsafe_dn = "dc=%s" % request.args['dc']
+ unsafe_filter = "(user=%s)" % request.args['username']
+
+ srv = ldap3.Server('ldap://127.0.0.1', port=1337)
+ conn = ldap3.Connection(srv, user=unsafe_dn, auto_bind=True)
+ conn.search(unsafe_dn, unsafe_filter)
+
+
+@app.route("/direct")
+def direct():
+ unsafe_dn = "dc=%s" % request.args['dc']
+ unsafe_filter = "(user=%s)" % request.args['username']
+
+ srv = ldap3.Server('ldap://127.0.0.1', port=1337)
+ conn = ldap3.Connection(srv, user=unsafe_dn, auto_bind=True).search(unsafe_dn, unsafe_filter, attributes=[
+ "testAttr1", "testAttr2"])
+
+# if __name__ == "__main__":
+# app.run(debug=True)
diff --git a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_good.py b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_good.py
new file mode 100644
index 000000000000..c3cc1272ea77
--- /dev/null
+++ b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_good.py
@@ -0,0 +1,49 @@
+from flask import request, Flask
+import ldap3
+from ldap3.utils.dn import escape_rdn
+from ldap3.utils.conv import escape_filter_chars
+
+app = Flask(__name__)
+
+
+@app.route("/normal")
+def normal():
+ unsafe_dn = "dc=%s" % request.args['dc']
+ unsafe_filter = "(user=%s)" % request.args['username']
+
+ safe_dn = escape_rdn(unsafe_dn)
+ safe_filter = escape_filter_chars(unsafe_filter)
+
+ srv = ldap3.Server('ldap://127.0.0.1', port=1337)
+ conn = ldap3.Connection(srv, user=unsafe_dn, auto_bind=True)
+ conn.search(safe_dn, safe_filter, attributes=[
+ "testAttr1", "testAttr2"])
+
+
+@app.route("/normal_noAttrs")
+def normal_noAttrs():
+ unsafe_dn = "dc=%s" % request.args['dc']
+ unsafe_filter = "(user=%s)" % request.args['username']
+
+ safe_dn = escape_rdn(unsafe_dn)
+ safe_filter = escape_filter_chars(unsafe_filter)
+
+ srv = ldap3.Server('ldap://127.0.0.1', port=1337)
+ conn = ldap3.Connection(srv, user=unsafe_dn, auto_bind=True)
+ conn.search(safe_dn, safe_filter)
+
+
+@app.route("/direct")
+def direct():
+ unsafe_dn = "dc=%s" % request.args['dc']
+ unsafe_filter = "(user=%s)" % request.args['username']
+
+ safe_dn = escape_rdn(unsafe_dn)
+ safe_filter = escape_filter_chars(unsafe_filter)
+
+ srv = ldap3.Server('ldap://127.0.0.1', port=1337)
+ conn = ldap3.Connection(srv, user=unsafe_dn, auto_bind=True).search(safe_dn, safe_filter, attributes=[
+ "testAttr1", "testAttr2"])
+
+# if __name__ == "__main__":
+# app.run(debug=True)
From 8faafb6961d21e652ff3c7223dab498cec9f8332 Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Tue, 30 Mar 2021 16:58:02 +0200
Subject: [PATCH 08/34] Update Sink
---
.../Security/CWE-090/LDAPInjection.ql | 9 +++----
.../semmle/python/frameworks/Stdlib.qll | 6 ++---
.../security/injection/LDAPInjection.qll | 26 +++++++++++++++++--
3 files changed, 31 insertions(+), 10 deletions(-)
diff --git a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
index 7ce3a118918c..a803b39260ea 100644
--- a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
+++ b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
@@ -16,11 +16,10 @@ import DataFlow::PathGraph
from
LDAPInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink,
- LDAPQuery castedSink
+ LDAPInjectionSink castedSink
where
config.hasFlowPath(source, sink) and
- castedSink = sink.getNode() //and
+ castedSink.getLDAPNode() = sink.getNode() //and
// if exists(castedSink.getAttrs()) then
-select sink.getNode(), source, sink, "$@ LDAP query executes $@ as a $@ probably leaking $@.",
- sink.getNode(), "This", source.getNode(), "a user-provided value", castedSink.getLDAPNode(),
- castedSink.getLDAPPart(), castedSink.getAttrs(), "this attribute(s)"
+select sink.getNode(), source, sink, "$@ LDAP query executes $@ as a $@.", castedSink, "This",
+ source.getNode(), "a user-provided value", castedSink.getLDAPNode(), castedSink.getLDAPPart() //, castedSink.getAttrs(), "probably leaking this attribute(s)"
diff --git a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
index aa9013a76c06..348367d03707 100644
--- a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
+++ b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
@@ -95,10 +95,10 @@ private module LDAP {
(
ldapNode = this.getArg(0) and
ldapPart = "DN"
- or
- ldapNode = this.getArg(1) and
- ldapPart = "search_filter"
)
+ or
+ ldapNode = this.getArg(1) and
+ ldapPart = "search_filter"
)
}
diff --git a/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll b/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll
index db8bdda7db61..4814097ff8e5 100644
--- a/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll
+++ b/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll
@@ -8,6 +8,26 @@ import semmle.python.dataflow.new.DataFlow
import semmle.python.dataflow.new.TaintTracking
import semmle.python.dataflow.new.RemoteFlowSources
+class LDAPInjectionSink extends DataFlow::Node {
+ // DataFlow::Node attrs;
+ DataFlow::Node ldapNode;
+ string ldapPart;
+
+ LDAPInjectionSink() {
+ exists(LDAPQuery ldapQuery |
+ this = ldapQuery and
+ ldapNode = ldapQuery.getLDAPNode() and
+ ldapPart = ldapQuery.getLDAPPart() // and
+ // if exists(ldapQuery.getAttrs()) then attrs = ldapQuery.getAttrs()
+ )
+ }
+
+ DataFlow::Node getLDAPNode() { result = ldapNode }
+
+ string getLDAPPart() { result = ldapPart }
+ // DataFlow::Node getAttrs() { result = attrs }
+}
+
/**
* A taint-tracking configuration for detecting regular expression injections.
*/
@@ -16,9 +36,11 @@ class LDAPInjectionFlowConfig extends TaintTracking::Configuration {
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
- override predicate isSink(DataFlow::Node sink) { sink = any(LDAPQuery lQ).getLDAPNode() }
+ override predicate isSink(DataFlow::Node sink) {
+ sink = any(LDAPInjectionSink ldapInjSink).getLDAPNode()
+ }
override predicate isSanitizer(DataFlow::Node sanitizer) {
- sanitizer = any(LDAPEscape lE).getEscapeNode()
+ sanitizer = any(LDAPEscape ldapEsc).getEscapeNode()
}
}
From 4328ff398121d12f800a67a3edc0cd72ea2bf77e Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Wed, 31 Mar 2021 22:26:08 +0200
Subject: [PATCH 09/34] Remove attrs feature
---
.../experimental/Security/CWE-090/LDAPInjection.ql | 5 ++---
python/ql/src/experimental/semmle/python/Concepts.qll | 4 ----
.../experimental/semmle/python/frameworks/Stdlib.qll | 11 -----------
.../python/security/injection/LDAPInjection.qll | 5 +----
4 files changed, 3 insertions(+), 22 deletions(-)
diff --git a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
index a803b39260ea..8d01fc173d4a 100644
--- a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
+++ b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
@@ -19,7 +19,6 @@ from
LDAPInjectionSink castedSink
where
config.hasFlowPath(source, sink) and
- castedSink.getLDAPNode() = sink.getNode() //and
-// if exists(castedSink.getAttrs()) then
+ castedSink.getLDAPNode() = sink.getNode()
select sink.getNode(), source, sink, "$@ LDAP query executes $@ as a $@.", castedSink, "This",
- source.getNode(), "a user-provided value", castedSink.getLDAPNode(), castedSink.getLDAPPart() //, castedSink.getAttrs(), "probably leaking this attribute(s)"
+ source.getNode(), "a user-provided value", castedSink.getLDAPNode(), castedSink.getLDAPPart()
diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll
index e0f5c1bdec34..e62d9680665b 100644
--- a/python/ql/src/experimental/semmle/python/Concepts.qll
+++ b/python/ql/src/experimental/semmle/python/Concepts.qll
@@ -20,8 +20,6 @@ module LDAPQuery {
abstract DataFlow::Node getLDAPNode();
abstract string getLDAPPart();
-
- abstract DataFlow::Node getAttrs();
}
}
@@ -33,8 +31,6 @@ class LDAPQuery extends DataFlow::Node {
DataFlow::Node getLDAPNode() { result = range.getLDAPNode() }
string getLDAPPart() { result = range.getLDAPPart() }
-
- DataFlow::Node getAttrs() { result = range.getAttrs() }
}
module LDAPEscape {
diff --git a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
index 348367d03707..f56a6603ec76 100644
--- a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
+++ b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
@@ -21,7 +21,6 @@ private module LDAP {
private class LDAP2Query extends DataFlow::CallCfgNode, LDAPQuery::Range {
DataFlow::Node ldapNode;
string ldapPart;
- DataFlow::Node attrs;
LDAP2Query() {
exists(DataFlow::AttrRead searchMethod, DataFlow::CallCfgNode initCall |
@@ -45,10 +44,6 @@ private module LDAP {
override DataFlow::Node getLDAPNode() { result = ldapNode }
override string getLDAPPart() { result = ldapPart }
-
- override DataFlow::Node getAttrs() {
- result = this.getArg(3) or result = this.getArgByName("attrlist")
- }
}
private class LDAP2EscapeDN extends DataFlow::CallCfgNode, LDAPEscape::Range {
@@ -77,14 +72,12 @@ private module LDAP {
private module LDAP3 {
private class LDAP3QueryMethods extends string {
- // pending to dig into this although https://github.com/cannatag/ldap3/blob/21001d9087c0d24c399eec433a261c455b7bc97f/ldap3/core/connection.py#L760
LDAP3QueryMethods() { this in ["search"] }
}
private class LDAP3Query extends DataFlow::CallCfgNode, LDAPQuery::Range {
DataFlow::Node ldapNode;
string ldapPart;
- DataFlow::Node attrs;
LDAP3Query() {
exists(DataFlow::AttrRead searchMethod, DataFlow::CallCfgNode connCall |
@@ -105,10 +98,6 @@ private module LDAP {
override DataFlow::Node getLDAPNode() { result = ldapNode }
override string getLDAPPart() { result = ldapPart }
-
- override DataFlow::Node getAttrs() {
- result = this.getArg(3) or result = this.getArgByName("attributes")
- }
}
private class LDAP3EscapeDN extends DataFlow::CallCfgNode, LDAPEscape::Range {
diff --git a/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll b/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll
index 4814097ff8e5..da71f38457af 100644
--- a/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll
+++ b/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll
@@ -9,7 +9,6 @@ import semmle.python.dataflow.new.TaintTracking
import semmle.python.dataflow.new.RemoteFlowSources
class LDAPInjectionSink extends DataFlow::Node {
- // DataFlow::Node attrs;
DataFlow::Node ldapNode;
string ldapPart;
@@ -17,15 +16,13 @@ class LDAPInjectionSink extends DataFlow::Node {
exists(LDAPQuery ldapQuery |
this = ldapQuery and
ldapNode = ldapQuery.getLDAPNode() and
- ldapPart = ldapQuery.getLDAPPart() // and
- // if exists(ldapQuery.getAttrs()) then attrs = ldapQuery.getAttrs()
+ ldapPart = ldapQuery.getLDAPPart()
)
}
DataFlow::Node getLDAPNode() { result = ldapNode }
string getLDAPPart() { result = ldapPart }
- // DataFlow::Node getAttrs() { result = attrs }
}
/**
From 9b430310b4ecd9535191262249d889d0b9306025 Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Wed, 31 Mar 2021 23:19:56 +0200
Subject: [PATCH 10/34] Improve Sanitizer calls
---
.../semmle/python/frameworks/Stdlib.qll | 44 +++++++------------
1 file changed, 16 insertions(+), 28 deletions(-)
diff --git a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
index f56a6603ec76..cfd02b8b5a55 100644
--- a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
+++ b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
@@ -46,27 +46,21 @@ private module LDAP {
override string getLDAPPart() { result = ldapPart }
}
- private class LDAP2EscapeDN extends DataFlow::CallCfgNode, LDAPEscape::Range {
- DataFlow::Node escapeNode;
-
- LDAP2EscapeDN() {
- this = API::moduleImport("ldap").getMember("dn").getMember("escape_dn_chars").getACall() and
- escapeNode = this.getArg(0)
+ private class LDAP2EscapeDNCall extends DataFlow::CallCfgNode, LDAPEscape::Range {
+ LDAP2EscapeDNCall() {
+ this = API::moduleImport("ldap").getMember("dn").getMember("escape_dn_chars").getACall()
}
- override DataFlow::Node getEscapeNode() { result = escapeNode }
+ override DataFlow::Node getEscapeNode() { result = this.getArg(0) }
}
- private class LDAP2EscapeFilter extends DataFlow::CallCfgNode, LDAPEscape::Range {
- DataFlow::Node escapeNode;
-
- LDAP2EscapeFilter() {
+ private class LDAP2EscapeFilterCall extends DataFlow::CallCfgNode, LDAPEscape::Range {
+ LDAP2EscapeFilterCall() {
this =
- API::moduleImport("ldap").getMember("filter").getMember("escape_filter_chars").getACall() and
- escapeNode = this.getArg(0)
+ API::moduleImport("ldap").getMember("filter").getMember("escape_filter_chars").getACall()
}
- override DataFlow::Node getEscapeNode() { result = escapeNode }
+ override DataFlow::Node getEscapeNode() { result = this.getArg(0) }
}
}
@@ -100,36 +94,30 @@ private module LDAP {
override string getLDAPPart() { result = ldapPart }
}
- private class LDAP3EscapeDN extends DataFlow::CallCfgNode, LDAPEscape::Range {
- DataFlow::Node escapeNode;
-
- LDAP3EscapeDN() {
+ private class LDAP3EscapeDNCall extends DataFlow::CallCfgNode, LDAPEscape::Range {
+ LDAP3EscapeDNCall() {
this =
API::moduleImport("ldap3")
.getMember("utils")
.getMember("dn")
.getMember("escape_rdn")
- .getACall() and
- escapeNode = this.getArg(0)
+ .getACall()
}
- override DataFlow::Node getEscapeNode() { result = escapeNode }
+ override DataFlow::Node getEscapeNode() { result = this.getArg(0) }
}
- private class LDAP3EscapeFilter extends DataFlow::CallCfgNode, LDAPEscape::Range {
- DataFlow::Node escapeNode;
-
- LDAP3EscapeFilter() {
+ private class LDAP3EscapeFilterCall extends DataFlow::CallCfgNode, LDAPEscape::Range {
+ LDAP3EscapeFilterCall() {
this =
API::moduleImport("ldap3")
.getMember("utils")
.getMember("conv")
.getMember("escape_filter_chars")
- .getACall() and
- escapeNode = this.getArg(0)
+ .getACall()
}
- override DataFlow::Node getEscapeNode() { result = escapeNode }
+ override DataFlow::Node getEscapeNode() { result = this.getArg(0) }
}
}
}
From 1bcb9cd7c0f9be99c8c30e7bf4d4fea93722efb7 Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Tue, 6 Apr 2021 15:42:56 +0200
Subject: [PATCH 11/34] Simplify query
---
.../Security/CWE-090/LDAPInjection.ql | 12 ++++-------
.../experimental/semmle/python/Concepts.qll | 5 -----
.../semmle/python/frameworks/Stdlib.qll | 19 ++++-------------
.../security/injection/LDAPInjection.qll | 21 +------------------
4 files changed, 9 insertions(+), 48 deletions(-)
diff --git a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
index 8d01fc173d4a..945a7b94473d 100644
--- a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
+++ b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
@@ -14,11 +14,7 @@ import python
import experimental.semmle.python.security.injection.LDAPInjection
import DataFlow::PathGraph
-from
- LDAPInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink,
- LDAPInjectionSink castedSink
-where
- config.hasFlowPath(source, sink) and
- castedSink.getLDAPNode() = sink.getNode()
-select sink.getNode(), source, sink, "$@ LDAP query executes $@ as a $@.", castedSink, "This",
- source.getNode(), "a user-provided value", castedSink.getLDAPNode(), castedSink.getLDAPPart()
+from LDAPInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
+where config.hasFlowPath(source, sink)
+select sink.getNode(), source, sink, "$@ LDAP query parameter comes from $@.", sink.getNode(),
+ "This", source.getNode(), "a user-provided value"
diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll
index e62d9680665b..9022f9c38182 100644
--- a/python/ql/src/experimental/semmle/python/Concepts.qll
+++ b/python/ql/src/experimental/semmle/python/Concepts.qll
@@ -13,13 +13,10 @@ private import semmle.python.dataflow.new.DataFlow
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.dataflow.new.TaintTracking
private import experimental.semmle.python.Frameworks
-private import semmle.python.ApiGraphs
module LDAPQuery {
abstract class Range extends DataFlow::Node {
abstract DataFlow::Node getLDAPNode();
-
- abstract string getLDAPPart();
}
}
@@ -29,8 +26,6 @@ class LDAPQuery extends DataFlow::Node {
LDAPQuery() { this = range }
DataFlow::Node getLDAPNode() { result = range.getLDAPNode() }
-
- string getLDAPPart() { result = range.getLDAPPart() }
}
module LDAPEscape {
diff --git a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
index cfd02b8b5a55..5a7984fab43a 100644
--- a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
+++ b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
@@ -20,7 +20,6 @@ private module LDAP {
private class LDAP2Query extends DataFlow::CallCfgNode, LDAPQuery::Range {
DataFlow::Node ldapNode;
- string ldapPart;
LDAP2Query() {
exists(DataFlow::AttrRead searchMethod, DataFlow::CallCfgNode initCall |
@@ -29,21 +28,17 @@ private module LDAP {
initCall = searchMethod.getObject().getALocalSource() and
searchMethod.getAttributeName() instanceof LDAP2QueryMethods and
(
- ldapNode = this.getArg(0) and
- ldapPart = "DN"
+ ldapNode = this.getArg(0)
or
(
ldapNode = this.getArg(2) or
ldapNode = this.getArgByName("filterstr")
- ) and
- ldapPart = "search_filter"
+ )
)
)
}
override DataFlow::Node getLDAPNode() { result = ldapNode }
-
- override string getLDAPPart() { result = ldapPart }
}
private class LDAP2EscapeDNCall extends DataFlow::CallCfgNode, LDAPEscape::Range {
@@ -71,7 +66,6 @@ private module LDAP {
private class LDAP3Query extends DataFlow::CallCfgNode, LDAPQuery::Range {
DataFlow::Node ldapNode;
- string ldapPart;
LDAP3Query() {
exists(DataFlow::AttrRead searchMethod, DataFlow::CallCfgNode connCall |
@@ -80,18 +74,13 @@ private module LDAP {
connCall = searchMethod.getObject().getALocalSource() and
searchMethod.getAttributeName() instanceof LDAP3QueryMethods and
(
- ldapNode = this.getArg(0) and
- ldapPart = "DN"
+ ldapNode = this.getArg(0) or
+ ldapNode = this.getArg(1)
)
- or
- ldapNode = this.getArg(1) and
- ldapPart = "search_filter"
)
}
override DataFlow::Node getLDAPNode() { result = ldapNode }
-
- override string getLDAPPart() { result = ldapPart }
}
private class LDAP3EscapeDNCall extends DataFlow::CallCfgNode, LDAPEscape::Range {
diff --git a/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll b/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll
index da71f38457af..febebe0a8fd8 100644
--- a/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll
+++ b/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll
@@ -8,23 +8,6 @@ import semmle.python.dataflow.new.DataFlow
import semmle.python.dataflow.new.TaintTracking
import semmle.python.dataflow.new.RemoteFlowSources
-class LDAPInjectionSink extends DataFlow::Node {
- DataFlow::Node ldapNode;
- string ldapPart;
-
- LDAPInjectionSink() {
- exists(LDAPQuery ldapQuery |
- this = ldapQuery and
- ldapNode = ldapQuery.getLDAPNode() and
- ldapPart = ldapQuery.getLDAPPart()
- )
- }
-
- DataFlow::Node getLDAPNode() { result = ldapNode }
-
- string getLDAPPart() { result = ldapPart }
-}
-
/**
* A taint-tracking configuration for detecting regular expression injections.
*/
@@ -33,9 +16,7 @@ class LDAPInjectionFlowConfig extends TaintTracking::Configuration {
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
- override predicate isSink(DataFlow::Node sink) {
- sink = any(LDAPInjectionSink ldapInjSink).getLDAPNode()
- }
+ override predicate isSink(DataFlow::Node sink) { sink = any(LDAPQuery ldapQuery).getLDAPNode() }
override predicate isSanitizer(DataFlow::Node sanitizer) {
sanitizer = any(LDAPEscape ldapEsc).getEscapeNode()
From 33423eaef3386b3e6e782c35fb2fb99c8a4cf150 Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Thu, 8 Apr 2021 00:31:53 +0200
Subject: [PATCH 12/34] Optimize calls
---
.../experimental/semmle/python/frameworks/Stdlib.qll | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
index 5a7984fab43a..24def6be2773 100644
--- a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
+++ b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
@@ -22,10 +22,10 @@ private module LDAP {
DataFlow::Node ldapNode;
LDAP2Query() {
- exists(DataFlow::AttrRead searchMethod, DataFlow::CallCfgNode initCall |
+ exists(DataFlow::AttrRead searchMethod |
this.getFunction() = searchMethod and
- initCall = API::moduleImport("ldap").getMember("initialize").getACall() and
- initCall = searchMethod.getObject().getALocalSource() and
+ API::moduleImport("ldap").getMember("initialize").getACall() =
+ searchMethod.getObject().getALocalSource() and
searchMethod.getAttributeName() instanceof LDAP2QueryMethods and
(
ldapNode = this.getArg(0)
@@ -68,10 +68,10 @@ private module LDAP {
DataFlow::Node ldapNode;
LDAP3Query() {
- exists(DataFlow::AttrRead searchMethod, DataFlow::CallCfgNode connCall |
+ exists(DataFlow::AttrRead searchMethod |
this.getFunction() = searchMethod and
- connCall = API::moduleImport("ldap3").getMember("Connection").getACall() and
- connCall = searchMethod.getObject().getALocalSource() and
+ API::moduleImport("ldap3").getMember("Connection").getACall() =
+ searchMethod.getObject().getALocalSource() and
searchMethod.getAttributeName() instanceof LDAP3QueryMethods and
(
ldapNode = this.getArg(0) or
From a1850ddad47a07e93e9c2f94b10db2827fc5d863 Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Thu, 8 Apr 2021 22:55:48 +0200
Subject: [PATCH 13/34] Change LDAP config (qll) filename
---
python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql | 2 +-
.../python/security/injection/{LDAPInjection.qll => LDAP.qll} | 0
2 files changed, 1 insertion(+), 1 deletion(-)
rename python/ql/src/experimental/semmle/python/security/injection/{LDAPInjection.qll => LDAP.qll} (100%)
diff --git a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
index 945a7b94473d..28748595c465 100644
--- a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
+++ b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
@@ -11,7 +11,7 @@
// Determine precision above
import python
-import experimental.semmle.python.security.injection.LDAPInjection
+import experimental.semmle.python.security.injection.LDAP
import DataFlow::PathGraph
from LDAPInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
diff --git a/python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll b/python/ql/src/experimental/semmle/python/security/injection/LDAP.qll
similarity index 100%
rename from python/ql/src/experimental/semmle/python/security/injection/LDAPInjection.qll
rename to python/ql/src/experimental/semmle/python/security/injection/LDAP.qll
From 8661cb071948d0311660ff720842f4e42d85d399 Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Thu, 8 Apr 2021 22:56:12 +0200
Subject: [PATCH 14/34] Polish LDAP3Query
---
.../ql/src/experimental/semmle/python/frameworks/Stdlib.qll | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
index 24def6be2773..76d4f3268788 100644
--- a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
+++ b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
@@ -60,10 +60,6 @@ private module LDAP {
}
private module LDAP3 {
- private class LDAP3QueryMethods extends string {
- LDAP3QueryMethods() { this in ["search"] }
- }
-
private class LDAP3Query extends DataFlow::CallCfgNode, LDAPQuery::Range {
DataFlow::Node ldapNode;
@@ -72,7 +68,7 @@ private module LDAP {
this.getFunction() = searchMethod and
API::moduleImport("ldap3").getMember("Connection").getACall() =
searchMethod.getObject().getALocalSource() and
- searchMethod.getAttributeName() instanceof LDAP3QueryMethods and
+ searchMethod.getAttributeName() = "search" and
(
ldapNode = this.getArg(0) or
ldapNode = this.getArg(1)
From 7296879bc9f1b40a508d69e74c6f047c6395e009 Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Thu, 8 Apr 2021 23:11:20 +0200
Subject: [PATCH 15/34] Polish tests
---
.../Security/CWE-090/unit_tests/ldap3_bad.py | 21 +++++-----
.../Security/CWE-090/unit_tests/ldap3_good.py | 26 +++++--------
.../Security/CWE-090/unit_tests/ldap_bad.py | 34 +++++++----------
.../Security/CWE-090/unit_tests/ldap_good.py | 38 +++++++------------
4 files changed, 46 insertions(+), 73 deletions(-)
diff --git a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_bad.py b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_bad.py
index 76b7f309bd17..a5b4748c1467 100644
--- a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_bad.py
+++ b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_bad.py
@@ -6,17 +6,10 @@
@app.route("/normal")
def normal():
- unsafe_dn = "dc=%s" % request.args['dc']
- unsafe_filter = "(user=%s)" % request.args['username']
+ """
+ A RemoteFlowSource is used directly as DN and search filter
+ """
- srv = ldap3.Server('ldap://127.0.0.1', port=1337)
- conn = ldap3.Connection(srv, user=unsafe_dn, auto_bind=True)
- conn.search(unsafe_dn, unsafe_filter, attributes=[
- "testAttr1", "testAttr2"])
-
-
-@app.route("/normal_noAttrs")
-def normal_noAttrs():
unsafe_dn = "dc=%s" % request.args['dc']
unsafe_filter = "(user=%s)" % request.args['username']
@@ -27,12 +20,16 @@ def normal_noAttrs():
@app.route("/direct")
def direct():
+ """
+ A RemoteFlowSource is used directly as DN and search filter using a oneline call to .search
+ """
+
unsafe_dn = "dc=%s" % request.args['dc']
unsafe_filter = "(user=%s)" % request.args['username']
srv = ldap3.Server('ldap://127.0.0.1', port=1337)
- conn = ldap3.Connection(srv, user=unsafe_dn, auto_bind=True).search(unsafe_dn, unsafe_filter, attributes=[
- "testAttr1", "testAttr2"])
+ conn = ldap3.Connection(srv, user=unsafe_dn, auto_bind=True).search(
+ unsafe_dn, unsafe_filter)
# if __name__ == "__main__":
# app.run(debug=True)
diff --git a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_good.py b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_good.py
index c3cc1272ea77..a4fbb718c5b7 100644
--- a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_good.py
+++ b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_good.py
@@ -8,20 +8,10 @@
@app.route("/normal")
def normal():
- unsafe_dn = "dc=%s" % request.args['dc']
- unsafe_filter = "(user=%s)" % request.args['username']
-
- safe_dn = escape_rdn(unsafe_dn)
- safe_filter = escape_filter_chars(unsafe_filter)
+ """
+ A RemoteFlowSource is sanitized and used as DN and search filter
+ """
- srv = ldap3.Server('ldap://127.0.0.1', port=1337)
- conn = ldap3.Connection(srv, user=unsafe_dn, auto_bind=True)
- conn.search(safe_dn, safe_filter, attributes=[
- "testAttr1", "testAttr2"])
-
-
-@app.route("/normal_noAttrs")
-def normal_noAttrs():
unsafe_dn = "dc=%s" % request.args['dc']
unsafe_filter = "(user=%s)" % request.args['username']
@@ -29,12 +19,16 @@ def normal_noAttrs():
safe_filter = escape_filter_chars(unsafe_filter)
srv = ldap3.Server('ldap://127.0.0.1', port=1337)
- conn = ldap3.Connection(srv, user=unsafe_dn, auto_bind=True)
+ conn = ldap3.Connection(srv, user=safe_dn, auto_bind=True)
conn.search(safe_dn, safe_filter)
@app.route("/direct")
def direct():
+ """
+ A RemoteFlowSource is sanitized and used as DN and search filter using a oneline call to .search
+ """
+
unsafe_dn = "dc=%s" % request.args['dc']
unsafe_filter = "(user=%s)" % request.args['username']
@@ -42,8 +36,8 @@ def direct():
safe_filter = escape_filter_chars(unsafe_filter)
srv = ldap3.Server('ldap://127.0.0.1', port=1337)
- conn = ldap3.Connection(srv, user=unsafe_dn, auto_bind=True).search(safe_dn, safe_filter, attributes=[
- "testAttr1", "testAttr2"])
+ conn = ldap3.Connection(srv, user=safe_dn, auto_bind=True).search(
+ safe_dn, safe_filter)
# if __name__ == "__main__":
# app.run(debug=True)
diff --git a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_bad.py b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_bad.py
index 011f3b828654..728a9179c7a0 100644
--- a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_bad.py
+++ b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_bad.py
@@ -6,16 +6,10 @@
@app.route("/normal")
def normal():
- unsafe_dn = "dc=%s" % request.args['dc']
- unsafe_filter = "(user=%s)" % request.args['username']
-
- ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
- user = ldap_connection.search_s(
- unsafe_dn, ldap.SCOPE_SUBTREE, unsafe_filter, ["testAttr1", "testAttr2"])
-
+ """
+ A RemoteFlowSource is used directly as DN and search filter
+ """
-@app.route("/normal_noAttrs")
-def normal_noAttrs():
unsafe_dn = "dc=%s" % request.args['dc']
unsafe_filter = "(user=%s)" % request.args['username']
@@ -26,30 +20,30 @@ def normal_noAttrs():
@app.route("/direct")
def direct():
+ """
+ A RemoteFlowSource is used directly as DN and search filter using a oneline call to .search_s
+ """
+
unsafe_dn = "dc=%s" % request.args['dc']
unsafe_filter = "(user=%s)" % request.args['username']
user = ldap.initialize("ldap://127.0.0.1:1337").search_s(
- unsafe_dn, ldap.SCOPE_SUBTREE, unsafe_filter, ["testAttr1", "testAttr2"])
+ unsafe_dn, ldap.SCOPE_SUBTREE, unsafe_filter)
@app.route("/normal_argbyname")
def normal_argbyname():
+ """
+ A RemoteFlowSource is used directly as DN and search filter, while the search filter is specified as
+ an argument by name
+ """
+
unsafe_dn = "dc=%s" % request.args['dc']
unsafe_filter = "(user=%s)" % request.args['username']
ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
user = ldap_connection.search_s(
- unsafe_dn, ldap.SCOPE_SUBTREE, attrlist=["testAttr1", "testAttr2"], filterstr=unsafe_filter)
-
-
-@app.route("/direct_argbyname")
-def direct_argbyname():
- unsafe_dn = "dc=%s" % request.args['dc']
- unsafe_filter = "(user=%s)" % request.args['username']
-
- user = ldap.initialize("ldap://127.0.0.1:1337").search_s(
- unsafe_dn, ldap.SCOPE_SUBTREE, attrlist=["testAttr1", "testAttr2"], filterstr=unsafe_filter)
+ unsafe_dn, ldap.SCOPE_SUBTREE, filterstr=unsafe_filter)
# if __name__ == "__main__":
diff --git a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_good.py b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_good.py
index eda3883da6dc..b2c00cc04cb3 100644
--- a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_good.py
+++ b/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_good.py
@@ -8,19 +8,10 @@
@app.route("/normal")
def normal():
- unsafe_dn = "dc=%s" % request.args['dc']
- unsafe_filter = "(user=%s)" % request.args['username']
-
- safe_dn = ldap.dn.escape_dn_chars(unsafe_dn)
- safe_filter = ldap.filter.escape_filter_chars(unsafe_filter)
+ """
+ A RemoteFlowSource is sanitized and used as DN and search filter
+ """
- ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
- user = ldap_connection.search_s(
- safe_dn, ldap.SCOPE_SUBTREE, safe_filter, ["testAttr1", "testAttr2"])
-
-
-@app.route("/normal_noAttrs")
-def normal_noAttrs():
unsafe_dn = "dc=%s" % request.args['dc']
unsafe_filter = "(user=%s)" % request.args['username']
@@ -34,6 +25,10 @@ def normal_noAttrs():
@app.route("/direct")
def direct():
+ """
+ A RemoteFlowSource is sanitized and used as DN and search filter using a oneline call to .search_s
+ """
+
unsafe_dn = "dc=%s" % request.args['dc']
unsafe_filter = "(user=%s)" % request.args['username']
@@ -46,6 +41,11 @@ def direct():
@app.route("/normal_argbyname")
def normal_argbyname():
+ """
+ A RemoteFlowSource is sanitized and used as DN and search filter, while the search filter is specified as
+ an argument by name
+ """
+
unsafe_dn = "dc=%s" % request.args['dc']
unsafe_filter = "(user=%s)" % request.args['username']
@@ -54,19 +54,7 @@ def normal_argbyname():
ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
user = ldap_connection.search_s(
- safe_dn, ldap.SCOPE_SUBTREE, attrlist=["testAttr1", "testAttr2"], filterstr=safe_filter)
-
-
-@app.route("/direct_argbyname")
-def direct_argbyname():
- unsafe_dn = "dc=%s" % request.args['dc']
- unsafe_filter = "(user=%s)" % request.args['username']
-
- safe_dn = ldap.dn.escape_dn_chars(unsafe_dn)
- safe_filter = ldap.filter.escape_filter_chars(unsafe_filter)
-
- user = ldap.initialize("ldap://127.0.0.1:1337").search_s(
- safe_dn, ldap.SCOPE_SUBTREE, attrlist=["testAttr1", "testAttr2"], filterstr=safe_filter)
+ safe_dn, ldap.SCOPE_SUBTREE, filterstr=safe_filter)
# if __name__ == "__main__":
From 3c1ca7232469c71e96732e0fbbc4b8a825e3884a Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Thu, 8 Apr 2021 23:44:30 +0200
Subject: [PATCH 16/34] Improve qhelp
---
.../Security/CWE-090/LDAPInjection.qhelp | 66 ++++++++++++-------
1 file changed, 44 insertions(+), 22 deletions(-)
diff --git a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp
index a570549abfc4..0c6c4b9ca205 100644
--- a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp
+++ b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp
@@ -1,28 +1,50 @@
-
-
+
-
- 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.
-
+
+If an LDAP query or DN is built using string concatenation or string formatting, and the
+components of the concatenation include user input without any proper sanitization, a user
+is likely to be able to run malicious LDAP queries.
+
+
+
+If user input must be included in an LDAP query or DN, it should be escaped to
+avoid a malicious user providing special characters that change the meaning
+of the query. In Python2, user input should be escaped with ldap.dn.escape_dn_chars
+or ldap.filter.escape_filter_chars, while in Python3, user input should be escaped with
+ldap3.utils.dn.escape_rdn or ldap3.utils.conv.escape_filter_chars
+depending on the component tainted by the user. A good practice is to escape filter characters
+that could change the meaning of the query (https://tools.ietf.org/search/rfc4515#section-3).
+
+
+
+In the following examples, the code accepts both username and dc from the user,
+which it then uses to build a LDAP query and DN.
+
+The first and the second example uses the unsanitized user input directly
+in the search filter and DN for the LDAP query.
+A malicious user could provide special characters to change the meaning of these
+components, and search for a completely different set of values.
-
- 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 escape_filter_chars should be applied to that user input.
-
+
+
+In the third and four example, the input provided by the user is sanitized before it is included in the search filter or DN.
+This ensures the meaning of the query cannot be changed by a malicious user.
-
-
- OWASP
- LDAP Injection
-
-
- SonarSource
- RSPEC-2078
-
-
- Python
- LDAP Documentation
-
-
+
+
+
+
+OWASP: LDAP Injection Prevention Cheat Sheet.
+OWASP: LDAP Injection.
+SonarSource: RSPEC-2078.
+Python2: LDAP Documentation.
+Python3: LDAP Documentation.
+Wikipedia: LDAP injection.
+BlackHat: LDAP Injection and Blind LDAP Injection.
+LDAP: Understanding and Defending Against LDAP Injection Attacks.
+
\ No newline at end of file
From 1554f4f48d04e89c78783f2ec54c4f878fff60fe Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Thu, 8 Apr 2021 23:44:46 +0200
Subject: [PATCH 17/34] Create qhelp examples
---
.../Security/CWE-090/examples/example_bad1.py | 12 ++++++++++++
.../Security/CWE-090/examples/example_bad2.py | 12 ++++++++++++
.../Security/CWE-090/examples/example_good1.py | 17 +++++++++++++++++
.../Security/CWE-090/examples/example_good2.py | 17 +++++++++++++++++
4 files changed, 58 insertions(+)
create mode 100644 python/ql/src/experimental/Security/CWE-090/examples/example_bad1.py
create mode 100644 python/ql/src/experimental/Security/CWE-090/examples/example_bad2.py
create mode 100644 python/ql/src/experimental/Security/CWE-090/examples/example_good1.py
create mode 100644 python/ql/src/experimental/Security/CWE-090/examples/example_good2.py
diff --git a/python/ql/src/experimental/Security/CWE-090/examples/example_bad1.py b/python/ql/src/experimental/Security/CWE-090/examples/example_bad1.py
new file mode 100644
index 000000000000..4a1f86fd9811
--- /dev/null
+++ b/python/ql/src/experimental/Security/CWE-090/examples/example_bad1.py
@@ -0,0 +1,12 @@
+from flask import request, Flask
+import ldap
+
+
+@app.route("/normal")
+def normal():
+ unsafe_dn = "dc=%s" % request.args['dc']
+ unsafe_filter = "(user=%s)" % request.args['username']
+
+ ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
+ user = ldap_connection.search_s(
+ unsafe_dn, ldap.SCOPE_SUBTREE, unsafe_filter)
diff --git a/python/ql/src/experimental/Security/CWE-090/examples/example_bad2.py b/python/ql/src/experimental/Security/CWE-090/examples/example_bad2.py
new file mode 100644
index 000000000000..87cb4a65b8b0
--- /dev/null
+++ b/python/ql/src/experimental/Security/CWE-090/examples/example_bad2.py
@@ -0,0 +1,12 @@
+from flask import request, Flask
+import ldap3
+
+
+@app.route("/normal")
+def normal():
+ unsafe_dn = "dc=%s" % request.args['dc']
+ unsafe_filter = "(user=%s)" % request.args['username']
+
+ srv = ldap3.Server('ldap://127.0.0.1', port=1337)
+ conn = ldap3.Connection(srv, user=unsafe_dn, auto_bind=True)
+ conn.search(unsafe_dn, unsafe_filter)
diff --git a/python/ql/src/experimental/Security/CWE-090/examples/example_good1.py b/python/ql/src/experimental/Security/CWE-090/examples/example_good1.py
new file mode 100644
index 000000000000..ad9e9fe84f5d
--- /dev/null
+++ b/python/ql/src/experimental/Security/CWE-090/examples/example_good1.py
@@ -0,0 +1,17 @@
+from flask import request, Flask
+import ldap
+import ldap.filter
+import ldap.dn
+
+
+@app.route("/normal")
+def normal():
+ unsafe_dn = "dc=%s" % request.args['dc']
+ unsafe_filter = "(user=%s)" % request.args['username']
+
+ safe_dn = ldap.dn.escape_dn_chars(unsafe_dn)
+ safe_filter = ldap.filter.escape_filter_chars(unsafe_filter)
+
+ ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
+ user = ldap_connection.search_s(
+ safe_dn, ldap.SCOPE_SUBTREE, safe_filter)
diff --git a/python/ql/src/experimental/Security/CWE-090/examples/example_good2.py b/python/ql/src/experimental/Security/CWE-090/examples/example_good2.py
new file mode 100644
index 000000000000..c86db1780406
--- /dev/null
+++ b/python/ql/src/experimental/Security/CWE-090/examples/example_good2.py
@@ -0,0 +1,17 @@
+from flask import request, Flask
+import ldap3
+from ldap3.utils.dn import escape_rdn
+from ldap3.utils.conv import escape_filter_chars
+
+
+@app.route("/normal")
+def normal():
+ unsafe_dn = "dc=%s" % request.args['dc']
+ unsafe_filter = "(user=%s)" % request.args['username']
+
+ safe_dn = escape_rdn(unsafe_dn)
+ safe_filter = escape_filter_chars(unsafe_filter)
+
+ srv = ldap3.Server('ldap://127.0.0.1', port=1337)
+ conn = ldap3.Connection(srv, user=safe_dn, auto_bind=True)
+ conn.search(safe_dn, safe_filter)
From 95bfdc49558f3fdee343fd9b018e45922a24678f Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Thu, 8 Apr 2021 23:45:03 +0200
Subject: [PATCH 18/34] Move tests to /test
---
.../experimental/query-tests/Security/CWE-090}/ldap3_bad.py | 0
.../experimental/query-tests/Security/CWE-090}/ldap3_good.py | 0
.../experimental/query-tests/Security/CWE-090}/ldap_bad.py | 0
.../experimental/query-tests/Security/CWE-090}/ldap_good.py | 0
4 files changed, 0 insertions(+), 0 deletions(-)
rename python/ql/{src/experimental/Security/CWE-090/unit_tests => test/experimental/query-tests/Security/CWE-090}/ldap3_bad.py (100%)
rename python/ql/{src/experimental/Security/CWE-090/unit_tests => test/experimental/query-tests/Security/CWE-090}/ldap3_good.py (100%)
rename python/ql/{src/experimental/Security/CWE-090/unit_tests => test/experimental/query-tests/Security/CWE-090}/ldap_bad.py (100%)
rename python/ql/{src/experimental/Security/CWE-090/unit_tests => test/experimental/query-tests/Security/CWE-090}/ldap_good.py (100%)
diff --git a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_bad.py b/python/ql/test/experimental/query-tests/Security/CWE-090/ldap3_bad.py
similarity index 100%
rename from python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_bad.py
rename to python/ql/test/experimental/query-tests/Security/CWE-090/ldap3_bad.py
diff --git a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_good.py b/python/ql/test/experimental/query-tests/Security/CWE-090/ldap3_good.py
similarity index 100%
rename from python/ql/src/experimental/Security/CWE-090/unit_tests/ldap3_good.py
rename to python/ql/test/experimental/query-tests/Security/CWE-090/ldap3_good.py
diff --git a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_bad.py b/python/ql/test/experimental/query-tests/Security/CWE-090/ldap_bad.py
similarity index 100%
rename from python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_bad.py
rename to python/ql/test/experimental/query-tests/Security/CWE-090/ldap_bad.py
diff --git a/python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_good.py b/python/ql/test/experimental/query-tests/Security/CWE-090/ldap_good.py
similarity index 100%
rename from python/ql/src/experimental/Security/CWE-090/unit_tests/ldap_good.py
rename to python/ql/test/experimental/query-tests/Security/CWE-090/ldap_good.py
From 4f85de87debf91ace22bdea6cffc4c5f9788c4c0 Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Thu, 8 Apr 2021 23:45:12 +0200
Subject: [PATCH 19/34] Add qlref
---
.../query-tests/Security/CWE-090/LDAPInjection.qlref | 1 +
1 file changed, 1 insertion(+)
create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-090/LDAPInjection.qlref
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-090/LDAPInjection.qlref b/python/ql/test/experimental/query-tests/Security/CWE-090/LDAPInjection.qlref
new file mode 100644
index 000000000000..98b37bfdcf6e
--- /dev/null
+++ b/python/ql/test/experimental/query-tests/Security/CWE-090/LDAPInjection.qlref
@@ -0,0 +1 @@
+experimental/Security/CWE-090/LDAPInjection.ql
From 7819d1a30b856d15c1ec3d7c051a0ec890d80b9f Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Thu, 8 Apr 2021 23:45:26 +0200
Subject: [PATCH 20/34] Generate .expected
---
.../Security/CWE-090/LDAPInjection.expected | 98 +++++++++++++++++++
1 file changed, 98 insertions(+)
create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-090/LDAPInjection.expected
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-090/LDAPInjection.expected b/python/ql/test/experimental/query-tests/Security/CWE-090/LDAPInjection.expected
new file mode 100644
index 000000000000..07f46fbecdc3
--- /dev/null
+++ b/python/ql/test/experimental/query-tests/Security/CWE-090/LDAPInjection.expected
@@ -0,0 +1,98 @@
+edges
+| ldap3_bad.py:13:27:13:33 | ControlFlowNode for request | ldap3_bad.py:13:27:13:38 | ControlFlowNode for Attribute |
+| ldap3_bad.py:13:27:13:33 | ControlFlowNode for request | ldap3_bad.py:14:35:14:41 | ControlFlowNode for request |
+| ldap3_bad.py:13:27:13:33 | ControlFlowNode for request | ldap3_bad.py:14:35:14:46 | ControlFlowNode for Attribute |
+| ldap3_bad.py:13:27:13:38 | ControlFlowNode for Attribute | ldap3_bad.py:13:27:13:44 | ControlFlowNode for Subscript |
+| ldap3_bad.py:13:27:13:44 | ControlFlowNode for Subscript | ldap3_bad.py:18:17:18:25 | ControlFlowNode for unsafe_dn |
+| ldap3_bad.py:14:35:14:41 | ControlFlowNode for request | ldap3_bad.py:14:35:14:46 | ControlFlowNode for Attribute |
+| ldap3_bad.py:14:35:14:46 | ControlFlowNode for Attribute | ldap3_bad.py:14:35:14:58 | ControlFlowNode for Subscript |
+| ldap3_bad.py:14:35:14:58 | ControlFlowNode for Subscript | ldap3_bad.py:18:28:18:40 | ControlFlowNode for unsafe_filter |
+| ldap3_bad.py:27:27:27:33 | ControlFlowNode for request | ldap3_bad.py:27:27:27:38 | ControlFlowNode for Attribute |
+| ldap3_bad.py:27:27:27:33 | ControlFlowNode for request | ldap3_bad.py:28:35:28:41 | ControlFlowNode for request |
+| ldap3_bad.py:27:27:27:33 | ControlFlowNode for request | ldap3_bad.py:28:35:28:46 | ControlFlowNode for Attribute |
+| ldap3_bad.py:27:27:27:38 | ControlFlowNode for Attribute | ldap3_bad.py:27:27:27:44 | ControlFlowNode for Subscript |
+| ldap3_bad.py:27:27:27:44 | ControlFlowNode for Subscript | ldap3_bad.py:32:9:32:17 | ControlFlowNode for unsafe_dn |
+| ldap3_bad.py:28:35:28:41 | ControlFlowNode for request | ldap3_bad.py:28:35:28:46 | ControlFlowNode for Attribute |
+| ldap3_bad.py:28:35:28:46 | ControlFlowNode for Attribute | ldap3_bad.py:28:35:28:58 | ControlFlowNode for Subscript |
+| ldap3_bad.py:28:35:28:58 | ControlFlowNode for Subscript | ldap3_bad.py:32:20:32:32 | ControlFlowNode for unsafe_filter |
+| ldap_bad.py:13:27:13:33 | ControlFlowNode for request | ldap_bad.py:13:27:13:38 | ControlFlowNode for Attribute |
+| ldap_bad.py:13:27:13:33 | ControlFlowNode for request | ldap_bad.py:14:35:14:41 | ControlFlowNode for request |
+| ldap_bad.py:13:27:13:33 | ControlFlowNode for request | ldap_bad.py:14:35:14:46 | ControlFlowNode for Attribute |
+| ldap_bad.py:13:27:13:38 | ControlFlowNode for Attribute | ldap_bad.py:13:27:13:44 | ControlFlowNode for Subscript |
+| ldap_bad.py:13:27:13:44 | ControlFlowNode for Subscript | ldap_bad.py:18:9:18:17 | ControlFlowNode for unsafe_dn |
+| ldap_bad.py:14:35:14:41 | ControlFlowNode for request | ldap_bad.py:14:35:14:46 | ControlFlowNode for Attribute |
+| ldap_bad.py:14:35:14:46 | ControlFlowNode for Attribute | ldap_bad.py:14:35:14:58 | ControlFlowNode for Subscript |
+| ldap_bad.py:14:35:14:58 | ControlFlowNode for Subscript | ldap_bad.py:18:40:18:52 | ControlFlowNode for unsafe_filter |
+| ldap_bad.py:27:27:27:33 | ControlFlowNode for request | ldap_bad.py:27:27:27:38 | ControlFlowNode for Attribute |
+| ldap_bad.py:27:27:27:33 | ControlFlowNode for request | ldap_bad.py:28:35:28:41 | ControlFlowNode for request |
+| ldap_bad.py:27:27:27:33 | ControlFlowNode for request | ldap_bad.py:28:35:28:46 | ControlFlowNode for Attribute |
+| ldap_bad.py:27:27:27:38 | ControlFlowNode for Attribute | ldap_bad.py:27:27:27:44 | ControlFlowNode for Subscript |
+| ldap_bad.py:27:27:27:44 | ControlFlowNode for Subscript | ldap_bad.py:31:9:31:17 | ControlFlowNode for unsafe_dn |
+| ldap_bad.py:28:35:28:41 | ControlFlowNode for request | ldap_bad.py:28:35:28:46 | ControlFlowNode for Attribute |
+| ldap_bad.py:28:35:28:46 | ControlFlowNode for Attribute | ldap_bad.py:28:35:28:58 | ControlFlowNode for Subscript |
+| ldap_bad.py:28:35:28:58 | ControlFlowNode for Subscript | ldap_bad.py:31:40:31:52 | ControlFlowNode for unsafe_filter |
+| ldap_bad.py:41:27:41:33 | ControlFlowNode for request | ldap_bad.py:41:27:41:38 | ControlFlowNode for Attribute |
+| ldap_bad.py:41:27:41:33 | ControlFlowNode for request | ldap_bad.py:42:35:42:41 | ControlFlowNode for request |
+| ldap_bad.py:41:27:41:33 | ControlFlowNode for request | ldap_bad.py:42:35:42:46 | ControlFlowNode for Attribute |
+| ldap_bad.py:41:27:41:38 | ControlFlowNode for Attribute | ldap_bad.py:41:27:41:44 | ControlFlowNode for Subscript |
+| ldap_bad.py:41:27:41:44 | ControlFlowNode for Subscript | ldap_bad.py:46:9:46:17 | ControlFlowNode for unsafe_dn |
+| ldap_bad.py:42:35:42:41 | ControlFlowNode for request | ldap_bad.py:42:35:42:46 | ControlFlowNode for Attribute |
+| ldap_bad.py:42:35:42:46 | ControlFlowNode for Attribute | ldap_bad.py:42:35:42:58 | ControlFlowNode for Subscript |
+| ldap_bad.py:42:35:42:58 | ControlFlowNode for Subscript | ldap_bad.py:46:50:46:62 | ControlFlowNode for unsafe_filter |
+nodes
+| ldap3_bad.py:13:27:13:33 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| ldap3_bad.py:13:27:13:38 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| ldap3_bad.py:13:27:13:44 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
+| ldap3_bad.py:14:35:14:41 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| ldap3_bad.py:14:35:14:46 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| ldap3_bad.py:14:35:14:58 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
+| ldap3_bad.py:18:17:18:25 | ControlFlowNode for unsafe_dn | semmle.label | ControlFlowNode for unsafe_dn |
+| ldap3_bad.py:18:28:18:40 | ControlFlowNode for unsafe_filter | semmle.label | ControlFlowNode for unsafe_filter |
+| ldap3_bad.py:27:27:27:33 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| ldap3_bad.py:27:27:27:38 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| ldap3_bad.py:27:27:27:44 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
+| ldap3_bad.py:28:35:28:41 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| ldap3_bad.py:28:35:28:46 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| ldap3_bad.py:28:35:28:58 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
+| ldap3_bad.py:32:9:32:17 | ControlFlowNode for unsafe_dn | semmle.label | ControlFlowNode for unsafe_dn |
+| ldap3_bad.py:32:20:32:32 | ControlFlowNode for unsafe_filter | semmle.label | ControlFlowNode for unsafe_filter |
+| ldap_bad.py:13:27:13:33 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| ldap_bad.py:13:27:13:38 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| ldap_bad.py:13:27:13:44 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
+| ldap_bad.py:14:35:14:41 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| ldap_bad.py:14:35:14:46 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| ldap_bad.py:14:35:14:58 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
+| ldap_bad.py:18:9:18:17 | ControlFlowNode for unsafe_dn | semmle.label | ControlFlowNode for unsafe_dn |
+| ldap_bad.py:18:40:18:52 | ControlFlowNode for unsafe_filter | semmle.label | ControlFlowNode for unsafe_filter |
+| ldap_bad.py:27:27:27:33 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| ldap_bad.py:27:27:27:38 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| ldap_bad.py:27:27:27:44 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
+| ldap_bad.py:28:35:28:41 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| ldap_bad.py:28:35:28:46 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| ldap_bad.py:28:35:28:58 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
+| ldap_bad.py:31:9:31:17 | ControlFlowNode for unsafe_dn | semmle.label | ControlFlowNode for unsafe_dn |
+| ldap_bad.py:31:40:31:52 | ControlFlowNode for unsafe_filter | semmle.label | ControlFlowNode for unsafe_filter |
+| ldap_bad.py:41:27:41:33 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| ldap_bad.py:41:27:41:38 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| ldap_bad.py:41:27:41:44 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
+| ldap_bad.py:42:35:42:41 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| ldap_bad.py:42:35:42:46 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| ldap_bad.py:42:35:42:58 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
+| ldap_bad.py:46:9:46:17 | ControlFlowNode for unsafe_dn | semmle.label | ControlFlowNode for unsafe_dn |
+| ldap_bad.py:46:50:46:62 | ControlFlowNode for unsafe_filter | semmle.label | ControlFlowNode for unsafe_filter |
+#select
+| ldap3_bad.py:18:17:18:25 | ControlFlowNode for unsafe_dn | ldap3_bad.py:13:27:13:33 | ControlFlowNode for request | ldap3_bad.py:18:17:18:25 | ControlFlowNode for unsafe_dn | $@ LDAP query parameter comes from $@. | ldap3_bad.py:18:17:18:25 | ControlFlowNode for unsafe_dn | This | ldap3_bad.py:13:27:13:33 | ControlFlowNode for request | a user-provided value |
+| ldap3_bad.py:18:28:18:40 | ControlFlowNode for unsafe_filter | ldap3_bad.py:13:27:13:33 | ControlFlowNode for request | ldap3_bad.py:18:28:18:40 | ControlFlowNode for unsafe_filter | $@ LDAP query parameter comes from $@. | ldap3_bad.py:18:28:18:40 | ControlFlowNode for unsafe_filter | This | ldap3_bad.py:13:27:13:33 | ControlFlowNode for request | a user-provided value |
+| ldap3_bad.py:18:28:18:40 | ControlFlowNode for unsafe_filter | ldap3_bad.py:14:35:14:41 | ControlFlowNode for request | ldap3_bad.py:18:28:18:40 | ControlFlowNode for unsafe_filter | $@ LDAP query parameter comes from $@. | ldap3_bad.py:18:28:18:40 | ControlFlowNode for unsafe_filter | This | ldap3_bad.py:14:35:14:41 | ControlFlowNode for request | a user-provided value |
+| ldap3_bad.py:32:9:32:17 | ControlFlowNode for unsafe_dn | ldap3_bad.py:27:27:27:33 | ControlFlowNode for request | ldap3_bad.py:32:9:32:17 | ControlFlowNode for unsafe_dn | $@ LDAP query parameter comes from $@. | ldap3_bad.py:32:9:32:17 | ControlFlowNode for unsafe_dn | This | ldap3_bad.py:27:27:27:33 | ControlFlowNode for request | a user-provided value |
+| ldap3_bad.py:32:20:32:32 | ControlFlowNode for unsafe_filter | ldap3_bad.py:27:27:27:33 | ControlFlowNode for request | ldap3_bad.py:32:20:32:32 | ControlFlowNode for unsafe_filter | $@ LDAP query parameter comes from $@. | ldap3_bad.py:32:20:32:32 | ControlFlowNode for unsafe_filter | This | ldap3_bad.py:27:27:27:33 | ControlFlowNode for request | a user-provided value |
+| ldap3_bad.py:32:20:32:32 | ControlFlowNode for unsafe_filter | ldap3_bad.py:28:35:28:41 | ControlFlowNode for request | ldap3_bad.py:32:20:32:32 | ControlFlowNode for unsafe_filter | $@ LDAP query parameter comes from $@. | ldap3_bad.py:32:20:32:32 | ControlFlowNode for unsafe_filter | This | ldap3_bad.py:28:35:28:41 | ControlFlowNode for request | a user-provided value |
+| ldap_bad.py:18:9:18:17 | ControlFlowNode for unsafe_dn | ldap_bad.py:13:27:13:33 | ControlFlowNode for request | ldap_bad.py:18:9:18:17 | ControlFlowNode for unsafe_dn | $@ LDAP query parameter comes from $@. | ldap_bad.py:18:9:18:17 | ControlFlowNode for unsafe_dn | This | ldap_bad.py:13:27:13:33 | ControlFlowNode for request | a user-provided value |
+| ldap_bad.py:18:40:18:52 | ControlFlowNode for unsafe_filter | ldap_bad.py:13:27:13:33 | ControlFlowNode for request | ldap_bad.py:18:40:18:52 | ControlFlowNode for unsafe_filter | $@ LDAP query parameter comes from $@. | ldap_bad.py:18:40:18:52 | ControlFlowNode for unsafe_filter | This | ldap_bad.py:13:27:13:33 | ControlFlowNode for request | a user-provided value |
+| ldap_bad.py:18:40:18:52 | ControlFlowNode for unsafe_filter | ldap_bad.py:14:35:14:41 | ControlFlowNode for request | ldap_bad.py:18:40:18:52 | ControlFlowNode for unsafe_filter | $@ LDAP query parameter comes from $@. | ldap_bad.py:18:40:18:52 | ControlFlowNode for unsafe_filter | This | ldap_bad.py:14:35:14:41 | ControlFlowNode for request | a user-provided value |
+| ldap_bad.py:31:9:31:17 | ControlFlowNode for unsafe_dn | ldap_bad.py:27:27:27:33 | ControlFlowNode for request | ldap_bad.py:31:9:31:17 | ControlFlowNode for unsafe_dn | $@ LDAP query parameter comes from $@. | ldap_bad.py:31:9:31:17 | ControlFlowNode for unsafe_dn | This | ldap_bad.py:27:27:27:33 | ControlFlowNode for request | a user-provided value |
+| ldap_bad.py:31:40:31:52 | ControlFlowNode for unsafe_filter | ldap_bad.py:27:27:27:33 | ControlFlowNode for request | ldap_bad.py:31:40:31:52 | ControlFlowNode for unsafe_filter | $@ LDAP query parameter comes from $@. | ldap_bad.py:31:40:31:52 | ControlFlowNode for unsafe_filter | This | ldap_bad.py:27:27:27:33 | ControlFlowNode for request | a user-provided value |
+| ldap_bad.py:31:40:31:52 | ControlFlowNode for unsafe_filter | ldap_bad.py:28:35:28:41 | ControlFlowNode for request | ldap_bad.py:31:40:31:52 | ControlFlowNode for unsafe_filter | $@ LDAP query parameter comes from $@. | ldap_bad.py:31:40:31:52 | ControlFlowNode for unsafe_filter | This | ldap_bad.py:28:35:28:41 | ControlFlowNode for request | a user-provided value |
+| ldap_bad.py:46:9:46:17 | ControlFlowNode for unsafe_dn | ldap_bad.py:41:27:41:33 | ControlFlowNode for request | ldap_bad.py:46:9:46:17 | ControlFlowNode for unsafe_dn | $@ LDAP query parameter comes from $@. | ldap_bad.py:46:9:46:17 | ControlFlowNode for unsafe_dn | This | ldap_bad.py:41:27:41:33 | ControlFlowNode for request | a user-provided value |
+| ldap_bad.py:46:50:46:62 | ControlFlowNode for unsafe_filter | ldap_bad.py:41:27:41:33 | ControlFlowNode for request | ldap_bad.py:46:50:46:62 | ControlFlowNode for unsafe_filter | $@ LDAP query parameter comes from $@. | ldap_bad.py:46:50:46:62 | ControlFlowNode for unsafe_filter | This | ldap_bad.py:41:27:41:33 | ControlFlowNode for request | a user-provided value |
+| ldap_bad.py:46:50:46:62 | ControlFlowNode for unsafe_filter | ldap_bad.py:42:35:42:41 | ControlFlowNode for request | ldap_bad.py:46:50:46:62 | ControlFlowNode for unsafe_filter | $@ LDAP query parameter comes from $@. | ldap_bad.py:46:50:46:62 | ControlFlowNode for unsafe_filter | This | ldap_bad.py:42:35:42:41 | ControlFlowNode for request | a user-provided value |
From b405c675c2bb7a62faac778b968b827ef9b8b9f7 Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Thu, 8 Apr 2021 23:49:33 +0200
Subject: [PATCH 21/34] Add qhelp last newline
---
python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp
index 0c6c4b9ca205..e3b47f23b0f3 100644
--- a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp
+++ b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp
@@ -47,4 +47,4 @@ This ensures the meaning of the query cannot be changed by a malicious user.
BlackHat: LDAP Injection and Blind LDAP Injection.
LDAP: Understanding and Defending Against LDAP Injection Attacks.
-
\ No newline at end of file
+
From 82f47f85715f8d92839b820ccb5bd4b8cc48b41c Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Thu, 8 Apr 2021 23:55:34 +0200
Subject: [PATCH 22/34] Polish metadata
---
.../ql/src/experimental/Security/CWE-090/LDAPInjection.ql | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
index 28748595c465..50c892483181 100644
--- a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
+++ b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
@@ -1,9 +1,10 @@
/**
- * @name Python LDAP Injection
- * @description Python LDAP Injection through search filter
+ * @name LDAP query built from user-controlled sources
+ * @description Building an LDAP query from user-controlled sources is vulnerable to insertion of
+ * malicious LDAP code by the user.
* @kind path-problem
* @problem.severity error
- * @id python/ldap-injection
+ * @id py/ldap-injection
* @tags experimental
* security
* external/cwe/cwe-090
From cd75433e3945fd6ffa4166d4c09108b489e37587 Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Fri, 9 Apr 2021 00:52:50 +0200
Subject: [PATCH 23/34] Fix qhelp examples extension
---
.../src/experimental/Security/CWE-090/LDAPInjection.qhelp | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp
index e3b47f23b0f3..bda9b75da99d 100644
--- a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp
+++ b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp
@@ -27,14 +27,14 @@ in the search filter and DN for the LDAP query.
A malicious user could provide special characters to change the meaning of these
components, and search for a completely different set of values.
-
-
+
+
In the third and four example, the input provided by the user is sanitized before it is included in the search filter or DN.
This ensures the meaning of the query cannot be changed by a malicious user.
-
-
+
+
From a2e8d88a07df14cf22bbca4e3039b54107f8983a Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Fri, 9 Apr 2021 01:47:44 +0200
Subject: [PATCH 24/34] Write documentation
---
.../experimental/semmle/python/Concepts.qll | 32 +++++++++++++
.../semmle/python/frameworks/Stdlib.qll | 46 +++++++++++++++++++
.../semmle/python/security/injection/LDAP.qll | 2 +-
3 files changed, 79 insertions(+), 1 deletion(-)
diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll
index 9022f9c38182..9d0dddc3f143 100644
--- a/python/ql/src/experimental/semmle/python/Concepts.qll
+++ b/python/ql/src/experimental/semmle/python/Concepts.qll
@@ -14,12 +14,28 @@ private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.dataflow.new.TaintTracking
private import experimental.semmle.python.Frameworks
+/** Provides classes for modeling LDAP-related APIs. */
module LDAPQuery {
+ /**
+ * A data-flow node that collects methods executing a LDAP query.
+ *
+ * Extend this class to model new APIs. If you want to refine existing API models,
+ * extend `LDAPQuery` instead.
+ */
abstract class Range extends DataFlow::Node {
+ /**
+ * Gets the argument containing the executed expression.
+ */
abstract DataFlow::Node getLDAPNode();
}
}
+/**
+ * A data-flow node that collect methods executing a LDAP query.
+ *
+ * Extend this class to refine existing API models. If you want to model new APIs,
+ * extend `LDAPQuery::Range` instead.
+ */
class LDAPQuery extends DataFlow::Node {
LDAPQuery::Range range;
@@ -28,12 +44,28 @@ class LDAPQuery extends DataFlow::Node {
DataFlow::Node getLDAPNode() { result = range.getLDAPNode() }
}
+/** Provides classes for modeling LDAP components escape-related APIs. */
module LDAPEscape {
+ /**
+ * A data-flow node that collects functions escaping LDAP components.
+ *
+ * Extend this class to model new APIs. If you want to refine existing API models,
+ * extend `LDAPEscape` instead.
+ */
abstract class Range extends DataFlow::Node {
+ /**
+ * Gets the argument containing the escaped expression.
+ */
abstract DataFlow::Node getEscapeNode();
}
}
+/**
+ * A data-flow node that collects functions escaping LDAP components.
+ *
+ * Extend this class to refine existing API models. If you want to model new APIs,
+ * extend `RegexEscape::Range` instead.
+ */
class LDAPEscape extends DataFlow::Node {
LDAPEscape::Range range;
diff --git a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
index 76d4f3268788..2bbca55f820c 100644
--- a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
+++ b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
@@ -10,14 +10,32 @@ private import semmle.python.dataflow.new.RemoteFlowSources
private import experimental.semmle.python.Concepts
private import semmle.python.ApiGraphs
+/**
+ * Provides models for Python's ldap-related libraries.
+ */
private module LDAP {
+ /**
+ * Provides models for Python's `ldap` library.
+ *
+ * See https://www.python-ldap.org/en/python-ldap-3.3.0/index.html
+ */
private module LDAP2 {
+ /**
+ * List of `ldap` methods used to execute a query.
+ *
+ * See https://www.python-ldap.org/en/python-ldap-3.3.0/reference/ldap.html#functions
+ */
private class LDAP2QueryMethods extends string {
LDAP2QueryMethods() {
this in ["search", "search_s", "search_st", "search_ext", "search_ext_s"]
}
}
+ /**
+ * A class to find `ldap` methods executing a query.
+ *
+ * See `LDAP2QueryMethods`
+ */
private class LDAP2Query extends DataFlow::CallCfgNode, LDAPQuery::Range {
DataFlow::Node ldapNode;
@@ -41,6 +59,11 @@ private module LDAP {
override DataFlow::Node getLDAPNode() { result = ldapNode }
}
+ /**
+ * A class to find calls to `ldap.dn.escape_dn_chars`.
+ *
+ * See https://github.com/python-ldap/python-ldap/blob/7ce471e238cdd9a4dd8d17baccd1c9e05e6f894a/Lib/ldap/dn.py#L17
+ */
private class LDAP2EscapeDNCall extends DataFlow::CallCfgNode, LDAPEscape::Range {
LDAP2EscapeDNCall() {
this = API::moduleImport("ldap").getMember("dn").getMember("escape_dn_chars").getACall()
@@ -49,6 +72,11 @@ private module LDAP {
override DataFlow::Node getEscapeNode() { result = this.getArg(0) }
}
+ /**
+ * A class to find calls to `ldap.filter.escape_filter_chars`.
+ *
+ * See https://www.python-ldap.org/en/python-ldap-3.3.0/reference/ldap-filter.html#ldap.filter.escape_filter_chars
+ */
private class LDAP2EscapeFilterCall extends DataFlow::CallCfgNode, LDAPEscape::Range {
LDAP2EscapeFilterCall() {
this =
@@ -59,7 +87,15 @@ private module LDAP {
}
}
+ /**
+ * Provides models for Python's `ldap3` library.
+ *
+ * See https://pypi.org/project/ldap3/
+ */
private module LDAP3 {
+ /**
+ * A class to find `ldap3` methods executing a query.
+ */
private class LDAP3Query extends DataFlow::CallCfgNode, LDAPQuery::Range {
DataFlow::Node ldapNode;
@@ -79,6 +115,11 @@ private module LDAP {
override DataFlow::Node getLDAPNode() { result = ldapNode }
}
+ /**
+ * A class to find calls to `ldap3.utils.dn.escape_rdn`.
+ *
+ * See https://github.com/cannatag/ldap3/blob/4d33166f0869b929f59c6e6825a1b9505eb99967/ldap3/utils/dn.py#L390
+ */
private class LDAP3EscapeDNCall extends DataFlow::CallCfgNode, LDAPEscape::Range {
LDAP3EscapeDNCall() {
this =
@@ -92,6 +133,11 @@ private module LDAP {
override DataFlow::Node getEscapeNode() { result = this.getArg(0) }
}
+ /**
+ * A class to find calls to `ldap3.utils.conv.escape_filter_chars`.
+ *
+ * See https://github.com/cannatag/ldap3/blob/4d33166f0869b929f59c6e6825a1b9505eb99967/ldap3/utils/conv.py#L91
+ */
private class LDAP3EscapeFilterCall extends DataFlow::CallCfgNode, LDAPEscape::Range {
LDAP3EscapeFilterCall() {
this =
diff --git a/python/ql/src/experimental/semmle/python/security/injection/LDAP.qll b/python/ql/src/experimental/semmle/python/security/injection/LDAP.qll
index febebe0a8fd8..9f91f91b3214 100644
--- a/python/ql/src/experimental/semmle/python/security/injection/LDAP.qll
+++ b/python/ql/src/experimental/semmle/python/security/injection/LDAP.qll
@@ -9,7 +9,7 @@ import semmle.python.dataflow.new.TaintTracking
import semmle.python.dataflow.new.RemoteFlowSources
/**
- * A taint-tracking configuration for detecting regular expression injections.
+ * A taint-tracking configuration for detecting LDAP injections.
*/
class LDAPInjectionFlowConfig extends TaintTracking::Configuration {
LDAPInjectionFlowConfig() { this = "LDAPInjectionFlowConfig" }
From b020ea6e3aef0c0716cb931d9e22bd4f48c5bd18 Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Fri, 9 Apr 2021 01:50:23 +0200
Subject: [PATCH 25/34] Polish documentation
---
python/ql/src/experimental/semmle/python/Concepts.qll | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll
index 9d0dddc3f143..89a82fff87c4 100644
--- a/python/ql/src/experimental/semmle/python/Concepts.qll
+++ b/python/ql/src/experimental/semmle/python/Concepts.qll
@@ -14,7 +14,7 @@ private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.dataflow.new.TaintTracking
private import experimental.semmle.python.Frameworks
-/** Provides classes for modeling LDAP-related APIs. */
+/** Provides classes for modeling LDAP query execution-related APIs. */
module LDAPQuery {
/**
* A data-flow node that collects methods executing a LDAP query.
From 1c34230efbab719f332e1feb35ef7460edc3ae39 Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Fri, 9 Apr 2021 01:58:18 +0200
Subject: [PATCH 26/34] Fix documentation typo
---
python/ql/src/experimental/semmle/python/Concepts.qll | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll
index 89a82fff87c4..cb90a465a547 100644
--- a/python/ql/src/experimental/semmle/python/Concepts.qll
+++ b/python/ql/src/experimental/semmle/python/Concepts.qll
@@ -64,7 +64,7 @@ module LDAPEscape {
* A data-flow node that collects functions escaping LDAP components.
*
* Extend this class to refine existing API models. If you want to model new APIs,
- * extend `RegexEscape::Range` instead.
+ * extend `LDAPEscape::Range` instead.
*/
class LDAPEscape extends DataFlow::Node {
LDAPEscape::Range range;
From c2b96b3a5e8e8b0c7ff0a0f352e8e014233b29cf Mon Sep 17 00:00:00 2001
From: Jorge <46056498+jorgectf@users.noreply.github.com>
Date: Fri, 7 May 2021 21:51:10 +0200
Subject: [PATCH 27/34] Add documentation to main classes' functions.
Co-authored-by: Rasmus Wriedt Larsen
---
python/ql/src/experimental/semmle/python/Concepts.qll | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll
index cb90a465a547..18fd0b992eb0 100644
--- a/python/ql/src/experimental/semmle/python/Concepts.qll
+++ b/python/ql/src/experimental/semmle/python/Concepts.qll
@@ -41,6 +41,9 @@ class LDAPQuery extends DataFlow::Node {
LDAPQuery() { this = range }
+ /**
+ * Gets the argument containing the executed expression.
+ */
DataFlow::Node getLDAPNode() { result = range.getLDAPNode() }
}
@@ -71,5 +74,8 @@ class LDAPEscape extends DataFlow::Node {
LDAPEscape() { this = range }
+ /**
+ * Gets the argument containing the escaped expression.
+ */
DataFlow::Node getEscapeNode() { result = range.getEscapeNode() }
}
From 34b8af30acd8d28b5f5980169ed0d0d4ec826455 Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Fri, 7 May 2021 22:09:57 +0200
Subject: [PATCH 28/34] Move structure to LDAP.qll
---
.../semmle/python/frameworks/LDAP.qll | 153 ++++++++++++++++++
.../semmle/python/frameworks/Stdlib.qll | 143 ----------------
2 files changed, 153 insertions(+), 143 deletions(-)
create mode 100644 python/ql/src/experimental/semmle/python/frameworks/LDAP.qll
diff --git a/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll b/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll
new file mode 100644
index 000000000000..2153f0704575
--- /dev/null
+++ b/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll
@@ -0,0 +1,153 @@
+/**
+ * Provides classes modeling security-relevant aspects of the LDAP libraries.
+ */
+
+private import python
+private import semmle.python.dataflow.new.DataFlow
+private import semmle.python.dataflow.new.TaintTracking
+private import semmle.python.dataflow.new.RemoteFlowSources
+private import experimental.semmle.python.Concepts
+private import semmle.python.ApiGraphs
+
+/**
+ * Provides models for Python's ldap-related libraries.
+ */
+private module LDAP {
+ /**
+ * Provides models for Python's `ldap` library.
+ *
+ * See https://www.python-ldap.org/en/python-ldap-3.3.0/index.html
+ */
+ private module LDAP2 {
+ /**
+ * List of `ldap` methods used to execute a query.
+ *
+ * See https://www.python-ldap.org/en/python-ldap-3.3.0/reference/ldap.html#functions
+ */
+ private class LDAP2QueryMethods extends string {
+ LDAP2QueryMethods() {
+ this in ["search", "search_s", "search_st", "search_ext", "search_ext_s"]
+ }
+ }
+
+ /**
+ * A class to find `ldap` methods executing a query.
+ *
+ * See `LDAP2QueryMethods`
+ */
+ private class LDAP2Query extends DataFlow::CallCfgNode, LDAPQuery::Range {
+ DataFlow::Node ldapNode;
+
+ LDAP2Query() {
+ exists(DataFlow::AttrRead searchMethod |
+ this.getFunction() = searchMethod and
+ API::moduleImport("ldap").getMember("initialize").getACall() =
+ searchMethod.getObject().getALocalSource() and
+ searchMethod.getAttributeName() instanceof LDAP2QueryMethods and
+ (
+ ldapNode = this.getArg(0)
+ or
+ (
+ ldapNode = this.getArg(2) or
+ ldapNode = this.getArgByName("filterstr")
+ )
+ )
+ )
+ }
+
+ override DataFlow::Node getLDAPNode() { result = ldapNode }
+ }
+
+ /**
+ * A class to find calls to `ldap.dn.escape_dn_chars`.
+ *
+ * See https://github.com/python-ldap/python-ldap/blob/7ce471e238cdd9a4dd8d17baccd1c9e05e6f894a/Lib/ldap/dn.py#L17
+ */
+ private class LDAP2EscapeDNCall extends DataFlow::CallCfgNode, LDAPEscape::Range {
+ LDAP2EscapeDNCall() {
+ this = API::moduleImport("ldap").getMember("dn").getMember("escape_dn_chars").getACall()
+ }
+
+ override DataFlow::Node getEscapeNode() { result = this.getArg(0) }
+ }
+
+ /**
+ * A class to find calls to `ldap.filter.escape_filter_chars`.
+ *
+ * See https://www.python-ldap.org/en/python-ldap-3.3.0/reference/ldap-filter.html#ldap.filter.escape_filter_chars
+ */
+ private class LDAP2EscapeFilterCall extends DataFlow::CallCfgNode, LDAPEscape::Range {
+ LDAP2EscapeFilterCall() {
+ this =
+ API::moduleImport("ldap").getMember("filter").getMember("escape_filter_chars").getACall()
+ }
+
+ override DataFlow::Node getEscapeNode() { result = this.getArg(0) }
+ }
+ }
+
+ /**
+ * Provides models for Python's `ldap3` library.
+ *
+ * See https://pypi.org/project/ldap3/
+ */
+ private module LDAP3 {
+ /**
+ * A class to find `ldap3` methods executing a query.
+ */
+ private class LDAP3Query extends DataFlow::CallCfgNode, LDAPQuery::Range {
+ DataFlow::Node ldapNode;
+
+ LDAP3Query() {
+ exists(DataFlow::AttrRead searchMethod |
+ this.getFunction() = searchMethod and
+ API::moduleImport("ldap3").getMember("Connection").getACall() =
+ searchMethod.getObject().getALocalSource() and
+ searchMethod.getAttributeName() = "search" and
+ (
+ ldapNode = this.getArg(0) or
+ ldapNode = this.getArg(1)
+ )
+ )
+ }
+
+ override DataFlow::Node getLDAPNode() { result = ldapNode }
+ }
+
+ /**
+ * A class to find calls to `ldap3.utils.dn.escape_rdn`.
+ *
+ * See https://github.com/cannatag/ldap3/blob/4d33166f0869b929f59c6e6825a1b9505eb99967/ldap3/utils/dn.py#L390
+ */
+ private class LDAP3EscapeDNCall extends DataFlow::CallCfgNode, LDAPEscape::Range {
+ LDAP3EscapeDNCall() {
+ this =
+ API::moduleImport("ldap3")
+ .getMember("utils")
+ .getMember("dn")
+ .getMember("escape_rdn")
+ .getACall()
+ }
+
+ override DataFlow::Node getEscapeNode() { result = this.getArg(0) }
+ }
+
+ /**
+ * A class to find calls to `ldap3.utils.conv.escape_filter_chars`.
+ *
+ * See https://github.com/cannatag/ldap3/blob/4d33166f0869b929f59c6e6825a1b9505eb99967/ldap3/utils/conv.py#L91
+ */
+ private class LDAP3EscapeFilterCall extends DataFlow::CallCfgNode, LDAPEscape::Range {
+ LDAP3EscapeFilterCall() {
+ this =
+ API::moduleImport("ldap3")
+ .getMember("utils")
+ .getMember("conv")
+ .getMember("escape_filter_chars")
+ .getACall()
+ }
+
+ override DataFlow::Node getEscapeNode() { result = this.getArg(0) }
+ }
+ }
+}
diff --git a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
index 2bbca55f820c..420caf0d73bb 100644
--- a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
+++ b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll
@@ -9,146 +9,3 @@ private import semmle.python.dataflow.new.TaintTracking
private import semmle.python.dataflow.new.RemoteFlowSources
private import experimental.semmle.python.Concepts
private import semmle.python.ApiGraphs
-
-/**
- * Provides models for Python's ldap-related libraries.
- */
-private module LDAP {
- /**
- * Provides models for Python's `ldap` library.
- *
- * See https://www.python-ldap.org/en/python-ldap-3.3.0/index.html
- */
- private module LDAP2 {
- /**
- * List of `ldap` methods used to execute a query.
- *
- * See https://www.python-ldap.org/en/python-ldap-3.3.0/reference/ldap.html#functions
- */
- private class LDAP2QueryMethods extends string {
- LDAP2QueryMethods() {
- this in ["search", "search_s", "search_st", "search_ext", "search_ext_s"]
- }
- }
-
- /**
- * A class to find `ldap` methods executing a query.
- *
- * See `LDAP2QueryMethods`
- */
- private class LDAP2Query extends DataFlow::CallCfgNode, LDAPQuery::Range {
- DataFlow::Node ldapNode;
-
- LDAP2Query() {
- exists(DataFlow::AttrRead searchMethod |
- this.getFunction() = searchMethod and
- API::moduleImport("ldap").getMember("initialize").getACall() =
- searchMethod.getObject().getALocalSource() and
- searchMethod.getAttributeName() instanceof LDAP2QueryMethods and
- (
- ldapNode = this.getArg(0)
- or
- (
- ldapNode = this.getArg(2) or
- ldapNode = this.getArgByName("filterstr")
- )
- )
- )
- }
-
- override DataFlow::Node getLDAPNode() { result = ldapNode }
- }
-
- /**
- * A class to find calls to `ldap.dn.escape_dn_chars`.
- *
- * See https://github.com/python-ldap/python-ldap/blob/7ce471e238cdd9a4dd8d17baccd1c9e05e6f894a/Lib/ldap/dn.py#L17
- */
- private class LDAP2EscapeDNCall extends DataFlow::CallCfgNode, LDAPEscape::Range {
- LDAP2EscapeDNCall() {
- this = API::moduleImport("ldap").getMember("dn").getMember("escape_dn_chars").getACall()
- }
-
- override DataFlow::Node getEscapeNode() { result = this.getArg(0) }
- }
-
- /**
- * A class to find calls to `ldap.filter.escape_filter_chars`.
- *
- * See https://www.python-ldap.org/en/python-ldap-3.3.0/reference/ldap-filter.html#ldap.filter.escape_filter_chars
- */
- private class LDAP2EscapeFilterCall extends DataFlow::CallCfgNode, LDAPEscape::Range {
- LDAP2EscapeFilterCall() {
- this =
- API::moduleImport("ldap").getMember("filter").getMember("escape_filter_chars").getACall()
- }
-
- override DataFlow::Node getEscapeNode() { result = this.getArg(0) }
- }
- }
-
- /**
- * Provides models for Python's `ldap3` library.
- *
- * See https://pypi.org/project/ldap3/
- */
- private module LDAP3 {
- /**
- * A class to find `ldap3` methods executing a query.
- */
- private class LDAP3Query extends DataFlow::CallCfgNode, LDAPQuery::Range {
- DataFlow::Node ldapNode;
-
- LDAP3Query() {
- exists(DataFlow::AttrRead searchMethod |
- this.getFunction() = searchMethod and
- API::moduleImport("ldap3").getMember("Connection").getACall() =
- searchMethod.getObject().getALocalSource() and
- searchMethod.getAttributeName() = "search" and
- (
- ldapNode = this.getArg(0) or
- ldapNode = this.getArg(1)
- )
- )
- }
-
- override DataFlow::Node getLDAPNode() { result = ldapNode }
- }
-
- /**
- * A class to find calls to `ldap3.utils.dn.escape_rdn`.
- *
- * See https://github.com/cannatag/ldap3/blob/4d33166f0869b929f59c6e6825a1b9505eb99967/ldap3/utils/dn.py#L390
- */
- private class LDAP3EscapeDNCall extends DataFlow::CallCfgNode, LDAPEscape::Range {
- LDAP3EscapeDNCall() {
- this =
- API::moduleImport("ldap3")
- .getMember("utils")
- .getMember("dn")
- .getMember("escape_rdn")
- .getACall()
- }
-
- override DataFlow::Node getEscapeNode() { result = this.getArg(0) }
- }
-
- /**
- * A class to find calls to `ldap3.utils.conv.escape_filter_chars`.
- *
- * See https://github.com/cannatag/ldap3/blob/4d33166f0869b929f59c6e6825a1b9505eb99967/ldap3/utils/conv.py#L91
- */
- private class LDAP3EscapeFilterCall extends DataFlow::CallCfgNode, LDAPEscape::Range {
- LDAP3EscapeFilterCall() {
- this =
- API::moduleImport("ldap3")
- .getMember("utils")
- .getMember("conv")
- .getMember("escape_filter_chars")
- .getACall()
- }
-
- override DataFlow::Node getEscapeNode() { result = this.getArg(0) }
- }
- }
-}
From 6159fbea2bc8f4fdacdcf76829f23c7124e44d38 Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Fri, 7 May 2021 22:15:51 +0200
Subject: [PATCH 29/34] Update functions naming
---
.../experimental/semmle/python/Concepts.qll | 8 +++---
.../semmle/python/frameworks/LDAP.qll | 26 +++++++++----------
2 files changed, 17 insertions(+), 17 deletions(-)
diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll
index 18fd0b992eb0..0d7c351974be 100644
--- a/python/ql/src/experimental/semmle/python/Concepts.qll
+++ b/python/ql/src/experimental/semmle/python/Concepts.qll
@@ -26,7 +26,7 @@ module LDAPQuery {
/**
* Gets the argument containing the executed expression.
*/
- abstract DataFlow::Node getLDAPNode();
+ abstract DataFlow::Node getQuery();
}
}
@@ -44,7 +44,7 @@ class LDAPQuery extends DataFlow::Node {
/**
* Gets the argument containing the executed expression.
*/
- DataFlow::Node getLDAPNode() { result = range.getLDAPNode() }
+ DataFlow::Node getQuery() { result = range.getQuery() }
}
/** Provides classes for modeling LDAP components escape-related APIs. */
@@ -59,7 +59,7 @@ module LDAPEscape {
/**
* Gets the argument containing the escaped expression.
*/
- abstract DataFlow::Node getEscapeNode();
+ abstract DataFlow::Node getAnInput();
}
}
@@ -77,5 +77,5 @@ class LDAPEscape extends DataFlow::Node {
/**
* Gets the argument containing the escaped expression.
*/
- DataFlow::Node getEscapeNode() { result = range.getEscapeNode() }
+ DataFlow::Node getAnInput() { result = range.getAnInput() }
}
diff --git a/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll b/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll
index 2153f0704575..6c3e03cb267a 100644
--- a/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll
+++ b/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll
@@ -36,7 +36,7 @@ private module LDAP {
* See `LDAP2QueryMethods`
*/
private class LDAP2Query extends DataFlow::CallCfgNode, LDAPQuery::Range {
- DataFlow::Node ldapNode;
+ DataFlow::Node ldapQuery;
LDAP2Query() {
exists(DataFlow::AttrRead searchMethod |
@@ -45,17 +45,17 @@ private module LDAP {
searchMethod.getObject().getALocalSource() and
searchMethod.getAttributeName() instanceof LDAP2QueryMethods and
(
- ldapNode = this.getArg(0)
+ ldapQuery = this.getArg(0)
or
(
- ldapNode = this.getArg(2) or
- ldapNode = this.getArgByName("filterstr")
+ ldapQuery = this.getArg(2) or
+ ldapQuery = this.getArgByName("filterstr")
)
)
)
}
- override DataFlow::Node getLDAPNode() { result = ldapNode }
+ override DataFlow::Node getQuery() { result = ldapQuery }
}
/**
@@ -68,7 +68,7 @@ private module LDAP {
this = API::moduleImport("ldap").getMember("dn").getMember("escape_dn_chars").getACall()
}
- override DataFlow::Node getEscapeNode() { result = this.getArg(0) }
+ override DataFlow::Node getAnInput() { result = this.getArg(0) }
}
/**
@@ -82,7 +82,7 @@ private module LDAP {
API::moduleImport("ldap").getMember("filter").getMember("escape_filter_chars").getACall()
}
- override DataFlow::Node getEscapeNode() { result = this.getArg(0) }
+ override DataFlow::Node getAnInput() { result = this.getArg(0) }
}
}
@@ -96,7 +96,7 @@ private module LDAP {
* A class to find `ldap3` methods executing a query.
*/
private class LDAP3Query extends DataFlow::CallCfgNode, LDAPQuery::Range {
- DataFlow::Node ldapNode;
+ DataFlow::Node ldapQuery;
LDAP3Query() {
exists(DataFlow::AttrRead searchMethod |
@@ -105,13 +105,13 @@ private module LDAP {
searchMethod.getObject().getALocalSource() and
searchMethod.getAttributeName() = "search" and
(
- ldapNode = this.getArg(0) or
- ldapNode = this.getArg(1)
+ ldapQuery = this.getArg(0) or
+ ldapQuery = this.getArg(1)
)
)
}
- override DataFlow::Node getLDAPNode() { result = ldapNode }
+ override DataFlow::Node getQuery() { result = ldapQuery }
}
/**
@@ -129,7 +129,7 @@ private module LDAP {
.getACall()
}
- override DataFlow::Node getEscapeNode() { result = this.getArg(0) }
+ override DataFlow::Node getAnInput() { result = this.getArg(0) }
}
/**
@@ -147,7 +147,7 @@ private module LDAP {
.getACall()
}
- override DataFlow::Node getEscapeNode() { result = this.getArg(0) }
+ override DataFlow::Node getAnInput() { result = this.getArg(0) }
}
}
}
From 2ad72ad69390652dc7e68b831e71ecaf020bc14f Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Fri, 7 May 2021 22:16:12 +0200
Subject: [PATCH 30/34] Add LDAP framework entry in Frameworks.qll
---
python/ql/src/experimental/semmle/python/Frameworks.qll | 1 +
1 file changed, 1 insertion(+)
diff --git a/python/ql/src/experimental/semmle/python/Frameworks.qll b/python/ql/src/experimental/semmle/python/Frameworks.qll
index ca1dd04e57d6..b64532dda7c5 100644
--- a/python/ql/src/experimental/semmle/python/Frameworks.qll
+++ b/python/ql/src/experimental/semmle/python/Frameworks.qll
@@ -3,3 +3,4 @@
*/
private import experimental.semmle.python.frameworks.Stdlib
+private import experimental.semmle.python.frameworks.LDAP
\ No newline at end of file
From 8665747316064806f09a0018f48c7ce439bf7ee6 Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Sat, 8 May 2021 18:08:50 +0200
Subject: [PATCH 31/34] Update sink and sanitizer to match new naming
---
.../experimental/semmle/python/security/injection/LDAP.qll | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/python/ql/src/experimental/semmle/python/security/injection/LDAP.qll b/python/ql/src/experimental/semmle/python/security/injection/LDAP.qll
index 9f91f91b3214..1927e7a95d32 100644
--- a/python/ql/src/experimental/semmle/python/security/injection/LDAP.qll
+++ b/python/ql/src/experimental/semmle/python/security/injection/LDAP.qll
@@ -16,9 +16,9 @@ class LDAPInjectionFlowConfig extends TaintTracking::Configuration {
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
- override predicate isSink(DataFlow::Node sink) { sink = any(LDAPQuery ldapQuery).getLDAPNode() }
+ override predicate isSink(DataFlow::Node sink) { sink = any(LDAPQuery ldapQuery).getQuery() }
override predicate isSanitizer(DataFlow::Node sanitizer) {
- sanitizer = any(LDAPEscape ldapEsc).getEscapeNode()
+ sanitizer = any(LDAPEscape ldapEsc).getAnInput()
}
}
From 9e9678b3ca7c2f84cd1b0941cf8deb8e72a88193 Mon Sep 17 00:00:00 2001
From: Jorge <46056498+jorgectf@users.noreply.github.com>
Date: Fri, 21 May 2021 16:17:39 +0200
Subject: [PATCH 32/34] Apply documentation suggestions
Co-authored-by: Rasmus Wriedt Larsen
---
python/ql/src/experimental/semmle/python/frameworks/LDAP.qll | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll b/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll
index 6c3e03cb267a..2843a695b189 100644
--- a/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll
+++ b/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll
@@ -14,7 +14,7 @@ private import semmle.python.ApiGraphs
*/
private module LDAP {
/**
- * Provides models for Python's `ldap` library.
+ * Provides models for the `python-ldap` PyPI package (imported as `ldap`).
*
* See https://www.python-ldap.org/en/python-ldap-3.3.0/index.html
*/
@@ -87,7 +87,7 @@ private module LDAP {
}
/**
- * Provides models for Python's `ldap3` library.
+ * Provides models for the `ldap3` PyPI package
*
* See https://pypi.org/project/ldap3/
*/
From 37d6ff76a38433cd5c05bd39eec3cce086c3ec22 Mon Sep 17 00:00:00 2001
From: jorgectf
Date: Fri, 21 May 2021 17:47:53 +0200
Subject: [PATCH 33/34] Update tests and .expected
---
.../Security/CWE-090/examples/example_bad1.py | 11 +-
.../Security/CWE-090/examples/example_bad2.py | 13 +-
.../CWE-090/examples/example_good1.py | 13 +-
.../CWE-090/examples/example_good2.py | 15 +-
.../Security/CWE-090/LDAPInjection.expected | 190 +++++++++---------
.../query-tests/Security/CWE-090/ldap3_bad.py | 26 ++-
.../Security/CWE-090/ldap3_good.py | 30 +--
.../query-tests/Security/CWE-090/ldap_bad.py | 33 +--
.../query-tests/Security/CWE-090/ldap_good.py | 39 ++--
9 files changed, 206 insertions(+), 164 deletions(-)
diff --git a/python/ql/src/experimental/Security/CWE-090/examples/example_bad1.py b/python/ql/src/experimental/Security/CWE-090/examples/example_bad1.py
index 4a1f86fd9811..580f2aea5551 100644
--- a/python/ql/src/experimental/Security/CWE-090/examples/example_bad1.py
+++ b/python/ql/src/experimental/Security/CWE-090/examples/example_bad1.py
@@ -4,9 +4,12 @@
@app.route("/normal")
def normal():
- unsafe_dn = "dc=%s" % request.args['dc']
- unsafe_filter = "(user=%s)" % request.args['username']
+ unsafe_dc = request.args['dc']
+ unsafe_filter = request.args['username']
- ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
+ dn = "dc={}".format(unsafe_dc)
+ search_filter = "(user={})".format(unsafe_filter)
+
+ ldap_connection = ldap.initialize("ldap://127.0.0.1")
user = ldap_connection.search_s(
- unsafe_dn, ldap.SCOPE_SUBTREE, unsafe_filter)
+ dn, ldap.SCOPE_SUBTREE, search_filter)
diff --git a/python/ql/src/experimental/Security/CWE-090/examples/example_bad2.py b/python/ql/src/experimental/Security/CWE-090/examples/example_bad2.py
index 87cb4a65b8b0..5f43236e5c5d 100644
--- a/python/ql/src/experimental/Security/CWE-090/examples/example_bad2.py
+++ b/python/ql/src/experimental/Security/CWE-090/examples/example_bad2.py
@@ -4,9 +4,12 @@
@app.route("/normal")
def normal():
- unsafe_dn = "dc=%s" % request.args['dc']
- unsafe_filter = "(user=%s)" % request.args['username']
+ unsafe_dc = request.args['dc']
+ unsafe_filter = request.args['username']
- srv = ldap3.Server('ldap://127.0.0.1', port=1337)
- conn = ldap3.Connection(srv, user=unsafe_dn, auto_bind=True)
- conn.search(unsafe_dn, unsafe_filter)
+ dn = "dc={}".format(unsafe_dc)
+ search_filter = "(user={})".format(unsafe_filter)
+
+ srv = ldap3.Server('ldap://127.0.0.1')
+ conn = ldap3.Connection(srv, user=dn, auto_bind=True)
+ conn.search(dn, search_filter)
diff --git a/python/ql/src/experimental/Security/CWE-090/examples/example_good1.py b/python/ql/src/experimental/Security/CWE-090/examples/example_good1.py
index ad9e9fe84f5d..4bf73839c445 100644
--- a/python/ql/src/experimental/Security/CWE-090/examples/example_good1.py
+++ b/python/ql/src/experimental/Security/CWE-090/examples/example_good1.py
@@ -6,12 +6,15 @@
@app.route("/normal")
def normal():
- unsafe_dn = "dc=%s" % request.args['dc']
- unsafe_filter = "(user=%s)" % request.args['username']
+ unsafe_dc = request.args['dc']
+ unsafe_filter = request.args['username']
- safe_dn = ldap.dn.escape_dn_chars(unsafe_dn)
+ safe_dc = ldap.dn.escape_dn_chars(unsafe_dc)
safe_filter = ldap.filter.escape_filter_chars(unsafe_filter)
- ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
+ dn = "dc={}".format(safe_dc)
+ search_filter = "(user={})".format(safe_filter)
+
+ ldap_connection = ldap.initialize("ldap://127.0.0.1")
user = ldap_connection.search_s(
- safe_dn, ldap.SCOPE_SUBTREE, safe_filter)
+ dn, ldap.SCOPE_SUBTREE, search_filter)
diff --git a/python/ql/src/experimental/Security/CWE-090/examples/example_good2.py b/python/ql/src/experimental/Security/CWE-090/examples/example_good2.py
index c86db1780406..dced25873e89 100644
--- a/python/ql/src/experimental/Security/CWE-090/examples/example_good2.py
+++ b/python/ql/src/experimental/Security/CWE-090/examples/example_good2.py
@@ -6,12 +6,15 @@
@app.route("/normal")
def normal():
- unsafe_dn = "dc=%s" % request.args['dc']
- unsafe_filter = "(user=%s)" % request.args['username']
+ unsafe_dc = request.args['dc']
+ unsafe_filter = request.args['username']
- safe_dn = escape_rdn(unsafe_dn)
+ safe_dc = escape_rdn(unsafe_dc)
safe_filter = escape_filter_chars(unsafe_filter)
- srv = ldap3.Server('ldap://127.0.0.1', port=1337)
- conn = ldap3.Connection(srv, user=safe_dn, auto_bind=True)
- conn.search(safe_dn, safe_filter)
+ dn = "dc={}".format(safe_dc)
+ search_filter = "(user={})".format(safe_filter)
+
+ srv = ldap3.Server('ldap://127.0.0.1')
+ conn = ldap3.Connection(srv, user=dn, auto_bind=True)
+ conn.search(dn, search_filter)
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-090/LDAPInjection.expected b/python/ql/test/experimental/query-tests/Security/CWE-090/LDAPInjection.expected
index 07f46fbecdc3..403cc05c30ee 100644
--- a/python/ql/test/experimental/query-tests/Security/CWE-090/LDAPInjection.expected
+++ b/python/ql/test/experimental/query-tests/Security/CWE-090/LDAPInjection.expected
@@ -1,98 +1,98 @@
edges
-| ldap3_bad.py:13:27:13:33 | ControlFlowNode for request | ldap3_bad.py:13:27:13:38 | ControlFlowNode for Attribute |
-| ldap3_bad.py:13:27:13:33 | ControlFlowNode for request | ldap3_bad.py:14:35:14:41 | ControlFlowNode for request |
-| ldap3_bad.py:13:27:13:33 | ControlFlowNode for request | ldap3_bad.py:14:35:14:46 | ControlFlowNode for Attribute |
-| ldap3_bad.py:13:27:13:38 | ControlFlowNode for Attribute | ldap3_bad.py:13:27:13:44 | ControlFlowNode for Subscript |
-| ldap3_bad.py:13:27:13:44 | ControlFlowNode for Subscript | ldap3_bad.py:18:17:18:25 | ControlFlowNode for unsafe_dn |
-| ldap3_bad.py:14:35:14:41 | ControlFlowNode for request | ldap3_bad.py:14:35:14:46 | ControlFlowNode for Attribute |
-| ldap3_bad.py:14:35:14:46 | ControlFlowNode for Attribute | ldap3_bad.py:14:35:14:58 | ControlFlowNode for Subscript |
-| ldap3_bad.py:14:35:14:58 | ControlFlowNode for Subscript | ldap3_bad.py:18:28:18:40 | ControlFlowNode for unsafe_filter |
-| ldap3_bad.py:27:27:27:33 | ControlFlowNode for request | ldap3_bad.py:27:27:27:38 | ControlFlowNode for Attribute |
-| ldap3_bad.py:27:27:27:33 | ControlFlowNode for request | ldap3_bad.py:28:35:28:41 | ControlFlowNode for request |
-| ldap3_bad.py:27:27:27:33 | ControlFlowNode for request | ldap3_bad.py:28:35:28:46 | ControlFlowNode for Attribute |
-| ldap3_bad.py:27:27:27:38 | ControlFlowNode for Attribute | ldap3_bad.py:27:27:27:44 | ControlFlowNode for Subscript |
-| ldap3_bad.py:27:27:27:44 | ControlFlowNode for Subscript | ldap3_bad.py:32:9:32:17 | ControlFlowNode for unsafe_dn |
-| ldap3_bad.py:28:35:28:41 | ControlFlowNode for request | ldap3_bad.py:28:35:28:46 | ControlFlowNode for Attribute |
-| ldap3_bad.py:28:35:28:46 | ControlFlowNode for Attribute | ldap3_bad.py:28:35:28:58 | ControlFlowNode for Subscript |
-| ldap3_bad.py:28:35:28:58 | ControlFlowNode for Subscript | ldap3_bad.py:32:20:32:32 | ControlFlowNode for unsafe_filter |
-| ldap_bad.py:13:27:13:33 | ControlFlowNode for request | ldap_bad.py:13:27:13:38 | ControlFlowNode for Attribute |
-| ldap_bad.py:13:27:13:33 | ControlFlowNode for request | ldap_bad.py:14:35:14:41 | ControlFlowNode for request |
-| ldap_bad.py:13:27:13:33 | ControlFlowNode for request | ldap_bad.py:14:35:14:46 | ControlFlowNode for Attribute |
-| ldap_bad.py:13:27:13:38 | ControlFlowNode for Attribute | ldap_bad.py:13:27:13:44 | ControlFlowNode for Subscript |
-| ldap_bad.py:13:27:13:44 | ControlFlowNode for Subscript | ldap_bad.py:18:9:18:17 | ControlFlowNode for unsafe_dn |
-| ldap_bad.py:14:35:14:41 | ControlFlowNode for request | ldap_bad.py:14:35:14:46 | ControlFlowNode for Attribute |
-| ldap_bad.py:14:35:14:46 | ControlFlowNode for Attribute | ldap_bad.py:14:35:14:58 | ControlFlowNode for Subscript |
-| ldap_bad.py:14:35:14:58 | ControlFlowNode for Subscript | ldap_bad.py:18:40:18:52 | ControlFlowNode for unsafe_filter |
-| ldap_bad.py:27:27:27:33 | ControlFlowNode for request | ldap_bad.py:27:27:27:38 | ControlFlowNode for Attribute |
-| ldap_bad.py:27:27:27:33 | ControlFlowNode for request | ldap_bad.py:28:35:28:41 | ControlFlowNode for request |
-| ldap_bad.py:27:27:27:33 | ControlFlowNode for request | ldap_bad.py:28:35:28:46 | ControlFlowNode for Attribute |
-| ldap_bad.py:27:27:27:38 | ControlFlowNode for Attribute | ldap_bad.py:27:27:27:44 | ControlFlowNode for Subscript |
-| ldap_bad.py:27:27:27:44 | ControlFlowNode for Subscript | ldap_bad.py:31:9:31:17 | ControlFlowNode for unsafe_dn |
-| ldap_bad.py:28:35:28:41 | ControlFlowNode for request | ldap_bad.py:28:35:28:46 | ControlFlowNode for Attribute |
-| ldap_bad.py:28:35:28:46 | ControlFlowNode for Attribute | ldap_bad.py:28:35:28:58 | ControlFlowNode for Subscript |
-| ldap_bad.py:28:35:28:58 | ControlFlowNode for Subscript | ldap_bad.py:31:40:31:52 | ControlFlowNode for unsafe_filter |
-| ldap_bad.py:41:27:41:33 | ControlFlowNode for request | ldap_bad.py:41:27:41:38 | ControlFlowNode for Attribute |
-| ldap_bad.py:41:27:41:33 | ControlFlowNode for request | ldap_bad.py:42:35:42:41 | ControlFlowNode for request |
-| ldap_bad.py:41:27:41:33 | ControlFlowNode for request | ldap_bad.py:42:35:42:46 | ControlFlowNode for Attribute |
-| ldap_bad.py:41:27:41:38 | ControlFlowNode for Attribute | ldap_bad.py:41:27:41:44 | ControlFlowNode for Subscript |
-| ldap_bad.py:41:27:41:44 | ControlFlowNode for Subscript | ldap_bad.py:46:9:46:17 | ControlFlowNode for unsafe_dn |
-| ldap_bad.py:42:35:42:41 | ControlFlowNode for request | ldap_bad.py:42:35:42:46 | ControlFlowNode for Attribute |
-| ldap_bad.py:42:35:42:46 | ControlFlowNode for Attribute | ldap_bad.py:42:35:42:58 | ControlFlowNode for Subscript |
-| ldap_bad.py:42:35:42:58 | ControlFlowNode for Subscript | ldap_bad.py:46:50:46:62 | ControlFlowNode for unsafe_filter |
+| ldap3_bad.py:13:17:13:23 | ControlFlowNode for request | ldap3_bad.py:13:17:13:28 | ControlFlowNode for Attribute |
+| ldap3_bad.py:13:17:13:23 | ControlFlowNode for request | ldap3_bad.py:14:21:14:27 | ControlFlowNode for request |
+| ldap3_bad.py:13:17:13:23 | ControlFlowNode for request | ldap3_bad.py:14:21:14:32 | ControlFlowNode for Attribute |
+| ldap3_bad.py:13:17:13:28 | ControlFlowNode for Attribute | ldap3_bad.py:13:17:13:34 | ControlFlowNode for Subscript |
+| ldap3_bad.py:13:17:13:34 | ControlFlowNode for Subscript | ldap3_bad.py:21:17:21:18 | ControlFlowNode for dn |
+| ldap3_bad.py:14:21:14:27 | ControlFlowNode for request | ldap3_bad.py:14:21:14:32 | ControlFlowNode for Attribute |
+| ldap3_bad.py:14:21:14:32 | ControlFlowNode for Attribute | ldap3_bad.py:14:21:14:44 | ControlFlowNode for Subscript |
+| ldap3_bad.py:14:21:14:44 | ControlFlowNode for Subscript | ldap3_bad.py:21:21:21:33 | ControlFlowNode for search_filter |
+| ldap3_bad.py:30:17:30:23 | ControlFlowNode for request | ldap3_bad.py:30:17:30:28 | ControlFlowNode for Attribute |
+| ldap3_bad.py:30:17:30:23 | ControlFlowNode for request | ldap3_bad.py:31:21:31:27 | ControlFlowNode for request |
+| ldap3_bad.py:30:17:30:23 | ControlFlowNode for request | ldap3_bad.py:31:21:31:32 | ControlFlowNode for Attribute |
+| ldap3_bad.py:30:17:30:28 | ControlFlowNode for Attribute | ldap3_bad.py:30:17:30:34 | ControlFlowNode for Subscript |
+| ldap3_bad.py:30:17:30:34 | ControlFlowNode for Subscript | ldap3_bad.py:38:9:38:10 | ControlFlowNode for dn |
+| ldap3_bad.py:31:21:31:27 | ControlFlowNode for request | ldap3_bad.py:31:21:31:32 | ControlFlowNode for Attribute |
+| ldap3_bad.py:31:21:31:32 | ControlFlowNode for Attribute | ldap3_bad.py:31:21:31:44 | ControlFlowNode for Subscript |
+| ldap3_bad.py:31:21:31:44 | ControlFlowNode for Subscript | ldap3_bad.py:38:13:38:25 | ControlFlowNode for search_filter |
+| ldap_bad.py:13:17:13:23 | ControlFlowNode for request | ldap_bad.py:13:17:13:28 | ControlFlowNode for Attribute |
+| ldap_bad.py:13:17:13:23 | ControlFlowNode for request | ldap_bad.py:14:21:14:27 | ControlFlowNode for request |
+| ldap_bad.py:13:17:13:23 | ControlFlowNode for request | ldap_bad.py:14:21:14:32 | ControlFlowNode for Attribute |
+| ldap_bad.py:13:17:13:28 | ControlFlowNode for Attribute | ldap_bad.py:13:17:13:34 | ControlFlowNode for Subscript |
+| ldap_bad.py:13:17:13:34 | ControlFlowNode for Subscript | ldap_bad.py:21:9:21:10 | ControlFlowNode for dn |
+| ldap_bad.py:14:21:14:27 | ControlFlowNode for request | ldap_bad.py:14:21:14:32 | ControlFlowNode for Attribute |
+| ldap_bad.py:14:21:14:32 | ControlFlowNode for Attribute | ldap_bad.py:14:21:14:44 | ControlFlowNode for Subscript |
+| ldap_bad.py:14:21:14:44 | ControlFlowNode for Subscript | ldap_bad.py:21:33:21:45 | ControlFlowNode for search_filter |
+| ldap_bad.py:30:17:30:23 | ControlFlowNode for request | ldap_bad.py:30:17:30:28 | ControlFlowNode for Attribute |
+| ldap_bad.py:30:17:30:23 | ControlFlowNode for request | ldap_bad.py:31:21:31:27 | ControlFlowNode for request |
+| ldap_bad.py:30:17:30:23 | ControlFlowNode for request | ldap_bad.py:31:21:31:32 | ControlFlowNode for Attribute |
+| ldap_bad.py:30:17:30:28 | ControlFlowNode for Attribute | ldap_bad.py:30:17:30:34 | ControlFlowNode for Subscript |
+| ldap_bad.py:30:17:30:34 | ControlFlowNode for Subscript | ldap_bad.py:37:9:37:10 | ControlFlowNode for dn |
+| ldap_bad.py:31:21:31:27 | ControlFlowNode for request | ldap_bad.py:31:21:31:32 | ControlFlowNode for Attribute |
+| ldap_bad.py:31:21:31:32 | ControlFlowNode for Attribute | ldap_bad.py:31:21:31:44 | ControlFlowNode for Subscript |
+| ldap_bad.py:31:21:31:44 | ControlFlowNode for Subscript | ldap_bad.py:37:33:37:45 | ControlFlowNode for search_filter |
+| ldap_bad.py:47:17:47:23 | ControlFlowNode for request | ldap_bad.py:47:17:47:28 | ControlFlowNode for Attribute |
+| ldap_bad.py:47:17:47:23 | ControlFlowNode for request | ldap_bad.py:48:21:48:27 | ControlFlowNode for request |
+| ldap_bad.py:47:17:47:23 | ControlFlowNode for request | ldap_bad.py:48:21:48:32 | ControlFlowNode for Attribute |
+| ldap_bad.py:47:17:47:28 | ControlFlowNode for Attribute | ldap_bad.py:47:17:47:34 | ControlFlowNode for Subscript |
+| ldap_bad.py:47:17:47:34 | ControlFlowNode for Subscript | ldap_bad.py:55:9:55:10 | ControlFlowNode for dn |
+| ldap_bad.py:48:21:48:27 | ControlFlowNode for request | ldap_bad.py:48:21:48:32 | ControlFlowNode for Attribute |
+| ldap_bad.py:48:21:48:32 | ControlFlowNode for Attribute | ldap_bad.py:48:21:48:44 | ControlFlowNode for Subscript |
+| ldap_bad.py:48:21:48:44 | ControlFlowNode for Subscript | ldap_bad.py:55:43:55:55 | ControlFlowNode for search_filter |
nodes
-| ldap3_bad.py:13:27:13:33 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| ldap3_bad.py:13:27:13:38 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
-| ldap3_bad.py:13:27:13:44 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
-| ldap3_bad.py:14:35:14:41 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| ldap3_bad.py:14:35:14:46 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
-| ldap3_bad.py:14:35:14:58 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
-| ldap3_bad.py:18:17:18:25 | ControlFlowNode for unsafe_dn | semmle.label | ControlFlowNode for unsafe_dn |
-| ldap3_bad.py:18:28:18:40 | ControlFlowNode for unsafe_filter | semmle.label | ControlFlowNode for unsafe_filter |
-| ldap3_bad.py:27:27:27:33 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| ldap3_bad.py:27:27:27:38 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
-| ldap3_bad.py:27:27:27:44 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
-| ldap3_bad.py:28:35:28:41 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| ldap3_bad.py:28:35:28:46 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
-| ldap3_bad.py:28:35:28:58 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
-| ldap3_bad.py:32:9:32:17 | ControlFlowNode for unsafe_dn | semmle.label | ControlFlowNode for unsafe_dn |
-| ldap3_bad.py:32:20:32:32 | ControlFlowNode for unsafe_filter | semmle.label | ControlFlowNode for unsafe_filter |
-| ldap_bad.py:13:27:13:33 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| ldap_bad.py:13:27:13:38 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
-| ldap_bad.py:13:27:13:44 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
-| ldap_bad.py:14:35:14:41 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| ldap_bad.py:14:35:14:46 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
-| ldap_bad.py:14:35:14:58 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
-| ldap_bad.py:18:9:18:17 | ControlFlowNode for unsafe_dn | semmle.label | ControlFlowNode for unsafe_dn |
-| ldap_bad.py:18:40:18:52 | ControlFlowNode for unsafe_filter | semmle.label | ControlFlowNode for unsafe_filter |
-| ldap_bad.py:27:27:27:33 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| ldap_bad.py:27:27:27:38 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
-| ldap_bad.py:27:27:27:44 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
-| ldap_bad.py:28:35:28:41 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| ldap_bad.py:28:35:28:46 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
-| ldap_bad.py:28:35:28:58 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
-| ldap_bad.py:31:9:31:17 | ControlFlowNode for unsafe_dn | semmle.label | ControlFlowNode for unsafe_dn |
-| ldap_bad.py:31:40:31:52 | ControlFlowNode for unsafe_filter | semmle.label | ControlFlowNode for unsafe_filter |
-| ldap_bad.py:41:27:41:33 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| ldap_bad.py:41:27:41:38 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
-| ldap_bad.py:41:27:41:44 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
-| ldap_bad.py:42:35:42:41 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
-| ldap_bad.py:42:35:42:46 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
-| ldap_bad.py:42:35:42:58 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
-| ldap_bad.py:46:9:46:17 | ControlFlowNode for unsafe_dn | semmle.label | ControlFlowNode for unsafe_dn |
-| ldap_bad.py:46:50:46:62 | ControlFlowNode for unsafe_filter | semmle.label | ControlFlowNode for unsafe_filter |
+| ldap3_bad.py:13:17:13:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| ldap3_bad.py:13:17:13:28 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| ldap3_bad.py:13:17:13:34 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
+| ldap3_bad.py:14:21:14:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| ldap3_bad.py:14:21:14:32 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| ldap3_bad.py:14:21:14:44 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
+| ldap3_bad.py:21:17:21:18 | ControlFlowNode for dn | semmle.label | ControlFlowNode for dn |
+| ldap3_bad.py:21:21:21:33 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter |
+| ldap3_bad.py:30:17:30:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| ldap3_bad.py:30:17:30:28 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| ldap3_bad.py:30:17:30:34 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
+| ldap3_bad.py:31:21:31:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| ldap3_bad.py:31:21:31:32 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| ldap3_bad.py:31:21:31:44 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
+| ldap3_bad.py:38:9:38:10 | ControlFlowNode for dn | semmle.label | ControlFlowNode for dn |
+| ldap3_bad.py:38:13:38:25 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter |
+| ldap_bad.py:13:17:13:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| ldap_bad.py:13:17:13:28 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| ldap_bad.py:13:17:13:34 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
+| ldap_bad.py:14:21:14:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| ldap_bad.py:14:21:14:32 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| ldap_bad.py:14:21:14:44 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
+| ldap_bad.py:21:9:21:10 | ControlFlowNode for dn | semmle.label | ControlFlowNode for dn |
+| ldap_bad.py:21:33:21:45 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter |
+| ldap_bad.py:30:17:30:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| ldap_bad.py:30:17:30:28 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| ldap_bad.py:30:17:30:34 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
+| ldap_bad.py:31:21:31:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| ldap_bad.py:31:21:31:32 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| ldap_bad.py:31:21:31:44 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
+| ldap_bad.py:37:9:37:10 | ControlFlowNode for dn | semmle.label | ControlFlowNode for dn |
+| ldap_bad.py:37:33:37:45 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter |
+| ldap_bad.py:47:17:47:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| ldap_bad.py:47:17:47:28 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| ldap_bad.py:47:17:47:34 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
+| ldap_bad.py:48:21:48:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
+| ldap_bad.py:48:21:48:32 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
+| ldap_bad.py:48:21:48:44 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
+| ldap_bad.py:55:9:55:10 | ControlFlowNode for dn | semmle.label | ControlFlowNode for dn |
+| ldap_bad.py:55:43:55:55 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter |
#select
-| ldap3_bad.py:18:17:18:25 | ControlFlowNode for unsafe_dn | ldap3_bad.py:13:27:13:33 | ControlFlowNode for request | ldap3_bad.py:18:17:18:25 | ControlFlowNode for unsafe_dn | $@ LDAP query parameter comes from $@. | ldap3_bad.py:18:17:18:25 | ControlFlowNode for unsafe_dn | This | ldap3_bad.py:13:27:13:33 | ControlFlowNode for request | a user-provided value |
-| ldap3_bad.py:18:28:18:40 | ControlFlowNode for unsafe_filter | ldap3_bad.py:13:27:13:33 | ControlFlowNode for request | ldap3_bad.py:18:28:18:40 | ControlFlowNode for unsafe_filter | $@ LDAP query parameter comes from $@. | ldap3_bad.py:18:28:18:40 | ControlFlowNode for unsafe_filter | This | ldap3_bad.py:13:27:13:33 | ControlFlowNode for request | a user-provided value |
-| ldap3_bad.py:18:28:18:40 | ControlFlowNode for unsafe_filter | ldap3_bad.py:14:35:14:41 | ControlFlowNode for request | ldap3_bad.py:18:28:18:40 | ControlFlowNode for unsafe_filter | $@ LDAP query parameter comes from $@. | ldap3_bad.py:18:28:18:40 | ControlFlowNode for unsafe_filter | This | ldap3_bad.py:14:35:14:41 | ControlFlowNode for request | a user-provided value |
-| ldap3_bad.py:32:9:32:17 | ControlFlowNode for unsafe_dn | ldap3_bad.py:27:27:27:33 | ControlFlowNode for request | ldap3_bad.py:32:9:32:17 | ControlFlowNode for unsafe_dn | $@ LDAP query parameter comes from $@. | ldap3_bad.py:32:9:32:17 | ControlFlowNode for unsafe_dn | This | ldap3_bad.py:27:27:27:33 | ControlFlowNode for request | a user-provided value |
-| ldap3_bad.py:32:20:32:32 | ControlFlowNode for unsafe_filter | ldap3_bad.py:27:27:27:33 | ControlFlowNode for request | ldap3_bad.py:32:20:32:32 | ControlFlowNode for unsafe_filter | $@ LDAP query parameter comes from $@. | ldap3_bad.py:32:20:32:32 | ControlFlowNode for unsafe_filter | This | ldap3_bad.py:27:27:27:33 | ControlFlowNode for request | a user-provided value |
-| ldap3_bad.py:32:20:32:32 | ControlFlowNode for unsafe_filter | ldap3_bad.py:28:35:28:41 | ControlFlowNode for request | ldap3_bad.py:32:20:32:32 | ControlFlowNode for unsafe_filter | $@ LDAP query parameter comes from $@. | ldap3_bad.py:32:20:32:32 | ControlFlowNode for unsafe_filter | This | ldap3_bad.py:28:35:28:41 | ControlFlowNode for request | a user-provided value |
-| ldap_bad.py:18:9:18:17 | ControlFlowNode for unsafe_dn | ldap_bad.py:13:27:13:33 | ControlFlowNode for request | ldap_bad.py:18:9:18:17 | ControlFlowNode for unsafe_dn | $@ LDAP query parameter comes from $@. | ldap_bad.py:18:9:18:17 | ControlFlowNode for unsafe_dn | This | ldap_bad.py:13:27:13:33 | ControlFlowNode for request | a user-provided value |
-| ldap_bad.py:18:40:18:52 | ControlFlowNode for unsafe_filter | ldap_bad.py:13:27:13:33 | ControlFlowNode for request | ldap_bad.py:18:40:18:52 | ControlFlowNode for unsafe_filter | $@ LDAP query parameter comes from $@. | ldap_bad.py:18:40:18:52 | ControlFlowNode for unsafe_filter | This | ldap_bad.py:13:27:13:33 | ControlFlowNode for request | a user-provided value |
-| ldap_bad.py:18:40:18:52 | ControlFlowNode for unsafe_filter | ldap_bad.py:14:35:14:41 | ControlFlowNode for request | ldap_bad.py:18:40:18:52 | ControlFlowNode for unsafe_filter | $@ LDAP query parameter comes from $@. | ldap_bad.py:18:40:18:52 | ControlFlowNode for unsafe_filter | This | ldap_bad.py:14:35:14:41 | ControlFlowNode for request | a user-provided value |
-| ldap_bad.py:31:9:31:17 | ControlFlowNode for unsafe_dn | ldap_bad.py:27:27:27:33 | ControlFlowNode for request | ldap_bad.py:31:9:31:17 | ControlFlowNode for unsafe_dn | $@ LDAP query parameter comes from $@. | ldap_bad.py:31:9:31:17 | ControlFlowNode for unsafe_dn | This | ldap_bad.py:27:27:27:33 | ControlFlowNode for request | a user-provided value |
-| ldap_bad.py:31:40:31:52 | ControlFlowNode for unsafe_filter | ldap_bad.py:27:27:27:33 | ControlFlowNode for request | ldap_bad.py:31:40:31:52 | ControlFlowNode for unsafe_filter | $@ LDAP query parameter comes from $@. | ldap_bad.py:31:40:31:52 | ControlFlowNode for unsafe_filter | This | ldap_bad.py:27:27:27:33 | ControlFlowNode for request | a user-provided value |
-| ldap_bad.py:31:40:31:52 | ControlFlowNode for unsafe_filter | ldap_bad.py:28:35:28:41 | ControlFlowNode for request | ldap_bad.py:31:40:31:52 | ControlFlowNode for unsafe_filter | $@ LDAP query parameter comes from $@. | ldap_bad.py:31:40:31:52 | ControlFlowNode for unsafe_filter | This | ldap_bad.py:28:35:28:41 | ControlFlowNode for request | a user-provided value |
-| ldap_bad.py:46:9:46:17 | ControlFlowNode for unsafe_dn | ldap_bad.py:41:27:41:33 | ControlFlowNode for request | ldap_bad.py:46:9:46:17 | ControlFlowNode for unsafe_dn | $@ LDAP query parameter comes from $@. | ldap_bad.py:46:9:46:17 | ControlFlowNode for unsafe_dn | This | ldap_bad.py:41:27:41:33 | ControlFlowNode for request | a user-provided value |
-| ldap_bad.py:46:50:46:62 | ControlFlowNode for unsafe_filter | ldap_bad.py:41:27:41:33 | ControlFlowNode for request | ldap_bad.py:46:50:46:62 | ControlFlowNode for unsafe_filter | $@ LDAP query parameter comes from $@. | ldap_bad.py:46:50:46:62 | ControlFlowNode for unsafe_filter | This | ldap_bad.py:41:27:41:33 | ControlFlowNode for request | a user-provided value |
-| ldap_bad.py:46:50:46:62 | ControlFlowNode for unsafe_filter | ldap_bad.py:42:35:42:41 | ControlFlowNode for request | ldap_bad.py:46:50:46:62 | ControlFlowNode for unsafe_filter | $@ LDAP query parameter comes from $@. | ldap_bad.py:46:50:46:62 | ControlFlowNode for unsafe_filter | This | ldap_bad.py:42:35:42:41 | ControlFlowNode for request | a user-provided value |
+| ldap3_bad.py:21:17:21:18 | ControlFlowNode for dn | ldap3_bad.py:13:17:13:23 | ControlFlowNode for request | ldap3_bad.py:21:17:21:18 | ControlFlowNode for dn | $@ LDAP query parameter comes from $@. | ldap3_bad.py:21:17:21:18 | ControlFlowNode for dn | This | ldap3_bad.py:13:17:13:23 | ControlFlowNode for request | a user-provided value |
+| ldap3_bad.py:21:21:21:33 | ControlFlowNode for search_filter | ldap3_bad.py:13:17:13:23 | ControlFlowNode for request | ldap3_bad.py:21:21:21:33 | ControlFlowNode for search_filter | $@ LDAP query parameter comes from $@. | ldap3_bad.py:21:21:21:33 | ControlFlowNode for search_filter | This | ldap3_bad.py:13:17:13:23 | ControlFlowNode for request | a user-provided value |
+| ldap3_bad.py:21:21:21:33 | ControlFlowNode for search_filter | ldap3_bad.py:14:21:14:27 | ControlFlowNode for request | ldap3_bad.py:21:21:21:33 | ControlFlowNode for search_filter | $@ LDAP query parameter comes from $@. | ldap3_bad.py:21:21:21:33 | ControlFlowNode for search_filter | This | ldap3_bad.py:14:21:14:27 | ControlFlowNode for request | a user-provided value |
+| ldap3_bad.py:38:9:38:10 | ControlFlowNode for dn | ldap3_bad.py:30:17:30:23 | ControlFlowNode for request | ldap3_bad.py:38:9:38:10 | ControlFlowNode for dn | $@ LDAP query parameter comes from $@. | ldap3_bad.py:38:9:38:10 | ControlFlowNode for dn | This | ldap3_bad.py:30:17:30:23 | ControlFlowNode for request | a user-provided value |
+| ldap3_bad.py:38:13:38:25 | ControlFlowNode for search_filter | ldap3_bad.py:30:17:30:23 | ControlFlowNode for request | ldap3_bad.py:38:13:38:25 | ControlFlowNode for search_filter | $@ LDAP query parameter comes from $@. | ldap3_bad.py:38:13:38:25 | ControlFlowNode for search_filter | This | ldap3_bad.py:30:17:30:23 | ControlFlowNode for request | a user-provided value |
+| ldap3_bad.py:38:13:38:25 | ControlFlowNode for search_filter | ldap3_bad.py:31:21:31:27 | ControlFlowNode for request | ldap3_bad.py:38:13:38:25 | ControlFlowNode for search_filter | $@ LDAP query parameter comes from $@. | ldap3_bad.py:38:13:38:25 | ControlFlowNode for search_filter | This | ldap3_bad.py:31:21:31:27 | ControlFlowNode for request | a user-provided value |
+| ldap_bad.py:21:9:21:10 | ControlFlowNode for dn | ldap_bad.py:13:17:13:23 | ControlFlowNode for request | ldap_bad.py:21:9:21:10 | ControlFlowNode for dn | $@ LDAP query parameter comes from $@. | ldap_bad.py:21:9:21:10 | ControlFlowNode for dn | This | ldap_bad.py:13:17:13:23 | ControlFlowNode for request | a user-provided value |
+| ldap_bad.py:21:33:21:45 | ControlFlowNode for search_filter | ldap_bad.py:13:17:13:23 | ControlFlowNode for request | ldap_bad.py:21:33:21:45 | ControlFlowNode for search_filter | $@ LDAP query parameter comes from $@. | ldap_bad.py:21:33:21:45 | ControlFlowNode for search_filter | This | ldap_bad.py:13:17:13:23 | ControlFlowNode for request | a user-provided value |
+| ldap_bad.py:21:33:21:45 | ControlFlowNode for search_filter | ldap_bad.py:14:21:14:27 | ControlFlowNode for request | ldap_bad.py:21:33:21:45 | ControlFlowNode for search_filter | $@ LDAP query parameter comes from $@. | ldap_bad.py:21:33:21:45 | ControlFlowNode for search_filter | This | ldap_bad.py:14:21:14:27 | ControlFlowNode for request | a user-provided value |
+| ldap_bad.py:37:9:37:10 | ControlFlowNode for dn | ldap_bad.py:30:17:30:23 | ControlFlowNode for request | ldap_bad.py:37:9:37:10 | ControlFlowNode for dn | $@ LDAP query parameter comes from $@. | ldap_bad.py:37:9:37:10 | ControlFlowNode for dn | This | ldap_bad.py:30:17:30:23 | ControlFlowNode for request | a user-provided value |
+| ldap_bad.py:37:33:37:45 | ControlFlowNode for search_filter | ldap_bad.py:30:17:30:23 | ControlFlowNode for request | ldap_bad.py:37:33:37:45 | ControlFlowNode for search_filter | $@ LDAP query parameter comes from $@. | ldap_bad.py:37:33:37:45 | ControlFlowNode for search_filter | This | ldap_bad.py:30:17:30:23 | ControlFlowNode for request | a user-provided value |
+| ldap_bad.py:37:33:37:45 | ControlFlowNode for search_filter | ldap_bad.py:31:21:31:27 | ControlFlowNode for request | ldap_bad.py:37:33:37:45 | ControlFlowNode for search_filter | $@ LDAP query parameter comes from $@. | ldap_bad.py:37:33:37:45 | ControlFlowNode for search_filter | This | ldap_bad.py:31:21:31:27 | ControlFlowNode for request | a user-provided value |
+| ldap_bad.py:55:9:55:10 | ControlFlowNode for dn | ldap_bad.py:47:17:47:23 | ControlFlowNode for request | ldap_bad.py:55:9:55:10 | ControlFlowNode for dn | $@ LDAP query parameter comes from $@. | ldap_bad.py:55:9:55:10 | ControlFlowNode for dn | This | ldap_bad.py:47:17:47:23 | ControlFlowNode for request | a user-provided value |
+| ldap_bad.py:55:43:55:55 | ControlFlowNode for search_filter | ldap_bad.py:47:17:47:23 | ControlFlowNode for request | ldap_bad.py:55:43:55:55 | ControlFlowNode for search_filter | $@ LDAP query parameter comes from $@. | ldap_bad.py:55:43:55:55 | ControlFlowNode for search_filter | This | ldap_bad.py:47:17:47:23 | ControlFlowNode for request | a user-provided value |
+| ldap_bad.py:55:43:55:55 | ControlFlowNode for search_filter | ldap_bad.py:48:21:48:27 | ControlFlowNode for request | ldap_bad.py:55:43:55:55 | ControlFlowNode for search_filter | $@ LDAP query parameter comes from $@. | ldap_bad.py:55:43:55:55 | ControlFlowNode for search_filter | This | ldap_bad.py:48:21:48:27 | ControlFlowNode for request | a user-provided value |
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-090/ldap3_bad.py b/python/ql/test/experimental/query-tests/Security/CWE-090/ldap3_bad.py
index a5b4748c1467..2edb986fccfc 100644
--- a/python/ql/test/experimental/query-tests/Security/CWE-090/ldap3_bad.py
+++ b/python/ql/test/experimental/query-tests/Security/CWE-090/ldap3_bad.py
@@ -10,12 +10,15 @@ def normal():
A RemoteFlowSource is used directly as DN and search filter
"""
- unsafe_dn = "dc=%s" % request.args['dc']
- unsafe_filter = "(user=%s)" % request.args['username']
+ unsafe_dc = request.args['dc']
+ unsafe_filter = request.args['username']
- srv = ldap3.Server('ldap://127.0.0.1', port=1337)
- conn = ldap3.Connection(srv, user=unsafe_dn, auto_bind=True)
- conn.search(unsafe_dn, unsafe_filter)
+ dn = "dc={}".format(unsafe_dc)
+ search_filter = "(user={})".format(unsafe_filter)
+
+ srv = ldap3.Server('ldap://127.0.0.1')
+ conn = ldap3.Connection(srv, user=dn, auto_bind=True)
+ conn.search(dn, search_filter)
@app.route("/direct")
@@ -24,12 +27,15 @@ def direct():
A RemoteFlowSource is used directly as DN and search filter using a oneline call to .search
"""
- unsafe_dn = "dc=%s" % request.args['dc']
- unsafe_filter = "(user=%s)" % request.args['username']
+ unsafe_dc = request.args['dc']
+ unsafe_filter = request.args['username']
+
+ dn = "dc={}".format(unsafe_dc)
+ search_filter = "(user={})".format(unsafe_filter)
- srv = ldap3.Server('ldap://127.0.0.1', port=1337)
- conn = ldap3.Connection(srv, user=unsafe_dn, auto_bind=True).search(
- unsafe_dn, unsafe_filter)
+ srv = ldap3.Server('ldap://127.0.0.1')
+ conn = ldap3.Connection(srv, user=dn, auto_bind=True).search(
+ dn, search_filter)
# if __name__ == "__main__":
# app.run(debug=True)
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-090/ldap3_good.py b/python/ql/test/experimental/query-tests/Security/CWE-090/ldap3_good.py
index a4fbb718c5b7..bb2e6d7af83e 100644
--- a/python/ql/test/experimental/query-tests/Security/CWE-090/ldap3_good.py
+++ b/python/ql/test/experimental/query-tests/Security/CWE-090/ldap3_good.py
@@ -12,15 +12,18 @@ def normal():
A RemoteFlowSource is sanitized and used as DN and search filter
"""
- unsafe_dn = "dc=%s" % request.args['dc']
- unsafe_filter = "(user=%s)" % request.args['username']
+ unsafe_dc = request.args['dc']
+ unsafe_filter = request.args['username']
- safe_dn = escape_rdn(unsafe_dn)
+ safe_dc = escape_rdn(unsafe_dc)
safe_filter = escape_filter_chars(unsafe_filter)
- srv = ldap3.Server('ldap://127.0.0.1', port=1337)
- conn = ldap3.Connection(srv, user=safe_dn, auto_bind=True)
- conn.search(safe_dn, safe_filter)
+ dn = "dc={}".format(safe_dc)
+ search_filter = "(user={})".format(safe_filter)
+
+ srv = ldap3.Server('ldap://127.0.0.1')
+ conn = ldap3.Connection(srv, user=dn, auto_bind=True)
+ conn.search(dn, search_filter)
@app.route("/direct")
@@ -29,15 +32,18 @@ def direct():
A RemoteFlowSource is sanitized and used as DN and search filter using a oneline call to .search
"""
- unsafe_dn = "dc=%s" % request.args['dc']
- unsafe_filter = "(user=%s)" % request.args['username']
+ unsafe_dc = request.args['dc']
+ unsafe_filter = request.args['username']
- safe_dn = escape_rdn(unsafe_dn)
+ safe_dc = escape_rdn(unsafe_dc)
safe_filter = escape_filter_chars(unsafe_filter)
- srv = ldap3.Server('ldap://127.0.0.1', port=1337)
- conn = ldap3.Connection(srv, user=safe_dn, auto_bind=True).search(
- safe_dn, safe_filter)
+ dn = "dc={}".format(safe_dc)
+ search_filter = "(user={})".format(safe_filter)
+
+ srv = ldap3.Server('ldap://127.0.0.1')
+ conn = ldap3.Connection(srv, user=dn, auto_bind=True).search(
+ dn, search_filter)
# if __name__ == "__main__":
# app.run(debug=True)
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-090/ldap_bad.py b/python/ql/test/experimental/query-tests/Security/CWE-090/ldap_bad.py
index 728a9179c7a0..133b0baaf9c0 100644
--- a/python/ql/test/experimental/query-tests/Security/CWE-090/ldap_bad.py
+++ b/python/ql/test/experimental/query-tests/Security/CWE-090/ldap_bad.py
@@ -10,12 +10,15 @@ def normal():
A RemoteFlowSource is used directly as DN and search filter
"""
- unsafe_dn = "dc=%s" % request.args['dc']
- unsafe_filter = "(user=%s)" % request.args['username']
+ unsafe_dc = request.args['dc']
+ unsafe_filter = request.args['username']
- ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
+ dn = "dc={}".format(unsafe_dc)
+ search_filter = "(user={})".format(unsafe_filter)
+
+ ldap_connection = ldap.initialize("ldap://127.0.0.1")
user = ldap_connection.search_s(
- unsafe_dn, ldap.SCOPE_SUBTREE, unsafe_filter)
+ dn, ldap.SCOPE_SUBTREE, search_filter)
@app.route("/direct")
@@ -24,11 +27,14 @@ def direct():
A RemoteFlowSource is used directly as DN and search filter using a oneline call to .search_s
"""
- unsafe_dn = "dc=%s" % request.args['dc']
- unsafe_filter = "(user=%s)" % request.args['username']
+ unsafe_dc = request.args['dc']
+ unsafe_filter = request.args['username']
+
+ dn = "dc={}".format(unsafe_dc)
+ search_filter = "(user={})".format(unsafe_filter)
- user = ldap.initialize("ldap://127.0.0.1:1337").search_s(
- unsafe_dn, ldap.SCOPE_SUBTREE, unsafe_filter)
+ user = ldap.initialize("ldap://127.0.0.1").search_s(
+ dn, ldap.SCOPE_SUBTREE, search_filter)
@app.route("/normal_argbyname")
@@ -38,12 +44,15 @@ def normal_argbyname():
an argument by name
"""
- unsafe_dn = "dc=%s" % request.args['dc']
- unsafe_filter = "(user=%s)" % request.args['username']
+ unsafe_dc = request.args['dc']
+ unsafe_filter = request.args['username']
+
+ dn = "dc={}".format(unsafe_dc)
+ search_filter = "(user={})".format(unsafe_filter)
- ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
+ ldap_connection = ldap.initialize("ldap://127.0.0.1")
user = ldap_connection.search_s(
- unsafe_dn, ldap.SCOPE_SUBTREE, filterstr=unsafe_filter)
+ dn, ldap.SCOPE_SUBTREE, filterstr=search_filter)
# if __name__ == "__main__":
diff --git a/python/ql/test/experimental/query-tests/Security/CWE-090/ldap_good.py b/python/ql/test/experimental/query-tests/Security/CWE-090/ldap_good.py
index b2c00cc04cb3..dfc6f91d0455 100644
--- a/python/ql/test/experimental/query-tests/Security/CWE-090/ldap_good.py
+++ b/python/ql/test/experimental/query-tests/Security/CWE-090/ldap_good.py
@@ -12,15 +12,18 @@ def normal():
A RemoteFlowSource is sanitized and used as DN and search filter
"""
- unsafe_dn = "dc=%s" % request.args['dc']
- unsafe_filter = "(user=%s)" % request.args['username']
+ unsafe_dc = request.args['dc']
+ unsafe_filter = request.args['username']
- safe_dn = ldap.dn.escape_dn_chars(unsafe_dn)
+ safe_dc = ldap.dn.escape_dn_chars(unsafe_dc)
safe_filter = ldap.filter.escape_filter_chars(unsafe_filter)
- ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
+ dn = "dc={}".format(safe_dc)
+ search_filter = "(user={})".format(safe_filter)
+
+ ldap_connection = ldap.initialize("ldap://127.0.0.1")
user = ldap_connection.search_s(
- safe_dn, ldap.SCOPE_SUBTREE, safe_filter)
+ dn, ldap.SCOPE_SUBTREE, search_filter)
@app.route("/direct")
@@ -29,14 +32,17 @@ def direct():
A RemoteFlowSource is sanitized and used as DN and search filter using a oneline call to .search_s
"""
- unsafe_dn = "dc=%s" % request.args['dc']
- unsafe_filter = "(user=%s)" % request.args['username']
+ unsafe_dc = request.args['dc']
+ unsafe_filter = request.args['username']
- safe_dn = ldap.dn.escape_dn_chars(unsafe_dn)
+ safe_dc = ldap.dn.escape_dn_chars(unsafe_dc)
safe_filter = ldap.filter.escape_filter_chars(unsafe_filter)
- user = ldap.initialize("ldap://127.0.0.1:1337").search_s(
- safe_dn, ldap.SCOPE_SUBTREE, safe_filter, ["testAttr1", "testAttr2"])
+ dn = "dc={}".format(safe_dc)
+ search_filter = "(user={})".format(safe_filter)
+
+ user = ldap.initialize("ldap://127.0.0.1").search_s(
+ dn, ldap.SCOPE_SUBTREE, search_filter, ["testAttr1", "testAttr2"])
@app.route("/normal_argbyname")
@@ -46,15 +52,18 @@ def normal_argbyname():
an argument by name
"""
- unsafe_dn = "dc=%s" % request.args['dc']
- unsafe_filter = "(user=%s)" % request.args['username']
+ unsafe_dc = request.args['dc']
+ unsafe_filter = request.args['username']
- safe_dn = ldap.dn.escape_dn_chars(unsafe_dn)
+ safe_dc = ldap.dn.escape_dn_chars(unsafe_dc)
safe_filter = ldap.filter.escape_filter_chars(unsafe_filter)
- ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
+ dn = "dc={}".format(safe_dc)
+ search_filter = "(user={})".format(safe_filter)
+
+ ldap_connection = ldap.initialize("ldap://127.0.0.1")
user = ldap_connection.search_s(
- safe_dn, ldap.SCOPE_SUBTREE, filterstr=safe_filter)
+ dn, ldap.SCOPE_SUBTREE, filterstr=search_filter)
# if __name__ == "__main__":
From f807c2f52b9ce04ba63831d5dbe8cbdb9cc54dae Mon Sep 17 00:00:00 2001
From: Rasmus Wriedt Larsen
Date: Wed, 26 May 2021 11:07:48 +0200
Subject: [PATCH 34/34] Python: autoformat
---
python/ql/src/experimental/semmle/python/Frameworks.qll | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/python/ql/src/experimental/semmle/python/Frameworks.qll b/python/ql/src/experimental/semmle/python/Frameworks.qll
index b64532dda7c5..5a77fc63a7d3 100644
--- a/python/ql/src/experimental/semmle/python/Frameworks.qll
+++ b/python/ql/src/experimental/semmle/python/Frameworks.qll
@@ -3,4 +3,4 @@
*/
private import experimental.semmle.python.frameworks.Stdlib
-private import experimental.semmle.python.frameworks.LDAP
\ No newline at end of file
+private import experimental.semmle.python.frameworks.LDAP