diff --git a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp new file mode 100644 index 000000000000..bda9b75da99d --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp @@ -0,0 +1,50 @@ + + + +

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 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 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.
  • +
    +
    diff --git a/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql new file mode 100644 index 000000000000..50c892483181 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql @@ -0,0 +1,21 @@ +/** + * @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 py/ldap-injection + * @tags experimental + * security + * external/cwe/cwe-090 + */ + +// Determine precision above +import python +import experimental.semmle.python.security.injection.LDAP +import DataFlow::PathGraph + +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/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..580f2aea5551 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-090/examples/example_bad1.py @@ -0,0 +1,15 @@ +from flask import request, Flask +import ldap + + +@app.route("/normal") +def normal(): + 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") + user = ldap_connection.search_s( + 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 new file mode 100644 index 000000000000..5f43236e5c5d --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-090/examples/example_bad2.py @@ -0,0 +1,15 @@ +from flask import request, Flask +import ldap3 + + +@app.route("/normal") +def normal(): + 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') + 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 new file mode 100644 index 000000000000..4bf73839c445 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-090/examples/example_good1.py @@ -0,0 +1,20 @@ +from flask import request, Flask +import ldap +import ldap.filter +import ldap.dn + + +@app.route("/normal") +def normal(): + unsafe_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + safe_dc = ldap.dn.escape_dn_chars(unsafe_dc) + safe_filter = ldap.filter.escape_filter_chars(unsafe_filter) + + 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( + 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 new file mode 100644 index 000000000000..dced25873e89 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-090/examples/example_good2.py @@ -0,0 +1,20 @@ +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_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + safe_dc = escape_rdn(unsafe_dc) + safe_filter = escape_filter_chars(unsafe_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/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll index 0661ebc5890e..7641ac1becfb 100644 --- a/python/ql/src/experimental/semmle/python/Concepts.qll +++ b/python/ql/src/experimental/semmle/python/Concepts.qll @@ -80,3 +80,69 @@ class RegexEscape extends DataFlow::Node { DataFlow::Node getRegexNode() { result = range.getRegexNode() } } + +/** Provides classes for modeling LDAP query execution-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 getQuery(); + } +} + +/** + * 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; + + LDAPQuery() { this = range } + + /** + * Gets the argument containing the executed expression. + */ + DataFlow::Node getQuery() { result = range.getQuery() } +} + +/** 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 getAnInput(); + } +} + +/** + * 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 `LDAPEscape::Range` instead. + */ +class LDAPEscape extends DataFlow::Node { + LDAPEscape::Range range; + + LDAPEscape() { this = range } + + /** + * Gets the argument containing the escaped expression. + */ + DataFlow::Node getAnInput() { result = range.getAnInput() } +} diff --git a/python/ql/src/experimental/semmle/python/Frameworks.qll b/python/ql/src/experimental/semmle/python/Frameworks.qll index ca1dd04e57d6..5a77fc63a7d3 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 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..2843a695b189 --- /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 the `python-ldap` PyPI package (imported as `ldap`). + * + * 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 ldapQuery; + + LDAP2Query() { + exists(DataFlow::AttrRead searchMethod | + this.getFunction() = searchMethod and + API::moduleImport("ldap").getMember("initialize").getACall() = + searchMethod.getObject().getALocalSource() and + searchMethod.getAttributeName() instanceof LDAP2QueryMethods and + ( + ldapQuery = this.getArg(0) + or + ( + ldapQuery = this.getArg(2) or + ldapQuery = this.getArgByName("filterstr") + ) + ) + ) + } + + override DataFlow::Node getQuery() { result = ldapQuery } + } + + /** + * 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 getAnInput() { 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 getAnInput() { result = this.getArg(0) } + } + } + + /** + * Provides models for the `ldap3` PyPI package + * + * 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 ldapQuery; + + LDAP3Query() { + exists(DataFlow::AttrRead searchMethod | + this.getFunction() = searchMethod and + API::moduleImport("ldap3").getMember("Connection").getACall() = + searchMethod.getObject().getALocalSource() and + searchMethod.getAttributeName() = "search" and + ( + ldapQuery = this.getArg(0) or + ldapQuery = this.getArg(1) + ) + ) + } + + override DataFlow::Node getQuery() { result = ldapQuery } + } + + /** + * 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 getAnInput() { 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 getAnInput() { result = this.getArg(0) } + } + } +} diff --git a/python/ql/src/experimental/semmle/python/security/injection/LDAP.qll b/python/ql/src/experimental/semmle/python/security/injection/LDAP.qll new file mode 100644 index 000000000000..1927e7a95d32 --- /dev/null +++ b/python/ql/src/experimental/semmle/python/security/injection/LDAP.qll @@ -0,0 +1,24 @@ +/** + * 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 LDAP 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 ldapQuery).getQuery() } + + override predicate isSanitizer(DataFlow::Node sanitizer) { + sanitizer = any(LDAPEscape ldapEsc).getAnInput() + } +} 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..403cc05c30ee --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-090/LDAPInjection.expected @@ -0,0 +1,98 @@ +edges +| 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: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: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/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 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 new file mode 100644 index 000000000000..2edb986fccfc --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-090/ldap3_bad.py @@ -0,0 +1,41 @@ +from flask import request, Flask +import ldap3 + +app = Flask(__name__) + + +@app.route("/normal") +def normal(): + """ + A RemoteFlowSource is used directly as DN and search filter + """ + + 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') + conn = ldap3.Connection(srv, user=dn, auto_bind=True) + conn.search(dn, search_filter) + + +@app.route("/direct") +def direct(): + """ + A RemoteFlowSource is used directly as DN and search filter using a oneline call to .search + """ + + 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') + 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 new file mode 100644 index 000000000000..bb2e6d7af83e --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-090/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(): + """ + A RemoteFlowSource is sanitized and used as DN and search filter + """ + + unsafe_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + safe_dc = escape_rdn(unsafe_dc) + safe_filter = escape_filter_chars(unsafe_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") +def direct(): + """ + A RemoteFlowSource is sanitized and used as DN and search filter using a oneline call to .search + """ + + unsafe_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + safe_dc = escape_rdn(unsafe_dc) + safe_filter = escape_filter_chars(unsafe_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 new file mode 100644 index 000000000000..133b0baaf9c0 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-090/ldap_bad.py @@ -0,0 +1,59 @@ +from flask import request, Flask +import ldap + +app = Flask(__name__) + + +@app.route("/normal") +def normal(): + """ + A RemoteFlowSource is used directly as DN and search filter + """ + + 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") + user = ldap_connection.search_s( + dn, ldap.SCOPE_SUBTREE, search_filter) + + +@app.route("/direct") +def direct(): + """ + A RemoteFlowSource is used directly as DN and search filter using a oneline call to .search_s + """ + + 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").search_s( + dn, ldap.SCOPE_SUBTREE, search_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_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") + user = ldap_connection.search_s( + dn, ldap.SCOPE_SUBTREE, filterstr=search_filter) + + +# if __name__ == "__main__": +# app.run(debug=True) 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 new file mode 100644 index 000000000000..dfc6f91d0455 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-090/ldap_good.py @@ -0,0 +1,70 @@ +from flask import request, Flask +import ldap +import ldap.filter +import ldap.dn + +app = Flask(__name__) + + +@app.route("/normal") +def normal(): + """ + A RemoteFlowSource is sanitized and used as DN and search filter + """ + + unsafe_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + safe_dc = ldap.dn.escape_dn_chars(unsafe_dc) + safe_filter = ldap.filter.escape_filter_chars(unsafe_filter) + + 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( + dn, ldap.SCOPE_SUBTREE, search_filter) + + +@app.route("/direct") +def direct(): + """ + A RemoteFlowSource is sanitized and used as DN and search filter using a oneline call to .search_s + """ + + unsafe_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + safe_dc = ldap.dn.escape_dn_chars(unsafe_dc) + safe_filter = ldap.filter.escape_filter_chars(unsafe_filter) + + 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") +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_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + safe_dc = ldap.dn.escape_dn_chars(unsafe_dc) + safe_filter = ldap.filter.escape_filter_chars(unsafe_filter) + + 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( + dn, ldap.SCOPE_SUBTREE, filterstr=search_filter) + + +# if __name__ == "__main__": +# app.run(debug=True)