Skip to content

Python: Add LDAP Injection query #5443

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 35 commits into from
May 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
799d509
Upload LDAP Injection query, qhelp and tests
jorgectf Mar 18, 2021
719b48c
Move to experimental folder
jorgectf Mar 18, 2021
95a1dae
Precision warn and Remove CWE reference
jorgectf Mar 18, 2021
85ec82a
Refactor in progress
jorgectf Mar 28, 2021
ad36bea
Refactor LDAP3 stuff (untested)
jorgectf Mar 29, 2021
8223539
Add a test without attributes
jorgectf Mar 29, 2021
3cda2e5
Polish up ldap3 tests
jorgectf Mar 29, 2021
8faafb6
Update Sink
jorgectf Mar 30, 2021
4328ff3
Remove attrs feature
jorgectf Mar 31, 2021
9b43031
Improve Sanitizer calls
jorgectf Mar 31, 2021
1bcb9cd
Simplify query
jorgectf Apr 6, 2021
33423ea
Optimize calls
jorgectf Apr 7, 2021
a1850dd
Change LDAP config (qll) filename
jorgectf Apr 8, 2021
8661cb0
Polish LDAP3Query
jorgectf Apr 8, 2021
7296879
Polish tests
jorgectf Apr 8, 2021
3c1ca72
Improve qhelp
jorgectf Apr 8, 2021
1554f4f
Create qhelp examples
jorgectf Apr 8, 2021
95bfdc4
Move tests to /test
jorgectf Apr 8, 2021
4f85de8
Add qlref
jorgectf Apr 8, 2021
7819d1a
Generate .expected
jorgectf Apr 8, 2021
b405c67
Add qhelp last newline
jorgectf Apr 8, 2021
82f47f8
Polish metadata
jorgectf Apr 8, 2021
cd75433
Fix qhelp examples extension
jorgectf Apr 8, 2021
a2e8d88
Write documentation
jorgectf Apr 8, 2021
b020ea6
Polish documentation
jorgectf Apr 8, 2021
1c34230
Fix documentation typo
jorgectf Apr 8, 2021
c2b96b3
Add documentation to main classes' functions.
jorgectf May 7, 2021
34b8af3
Move structure to LDAP.qll
jorgectf May 7, 2021
6159fbe
Update functions naming
jorgectf May 7, 2021
2ad72ad
Add LDAP framework entry in Frameworks.qll
jorgectf May 7, 2021
8665747
Update sink and sanitizer to match new naming
jorgectf May 8, 2021
9e9678b
Apply documentation suggestions
jorgectf May 21, 2021
37d6ff7
Update tests and .expected
jorgectf May 21, 2021
d5f2846
Merge branch 'main' into jorgectf/python/ldapInjection
RasmusWL May 26, 2021
f807c2f
Python: autoformat
RasmusWL May 26, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions python/ql/src/experimental/Security/CWE-090/LDAPInjection.qhelp
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>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.</p>
</overview>

<recommendation>
<p>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 <code>ldap.dn.escape_dn_chars</code>
or <code>ldap.filter.escape_filter_chars</code>, while in Python3, user input should be escaped with
<code>ldap3.utils.dn.escape_rdn</code> or <code>ldap3.utils.conv.escape_filter_chars</code>
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).</p>
</recommendation>

<example>
<p>In the following examples, the code accepts both <code>username</code> and <code>dc</code> from the user,
which it then uses to build a LDAP query and DN.</p>

<p>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.</p>

<sample src="examples/example_bad1.py" />
<sample src="examples/example_bad2.py" />

<p>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.</p>

<sample src="examples/example_good1.py" />
<sample src="examples/example_good2.py" />
</example>

<references>
<li>OWASP: <a href="https://cheatsheetseries.owasp.org/cheatsheets/LDAP_Injection_Prevention_Cheat_Sheet.html">LDAP Injection Prevention Cheat Sheet</a>.</li>
<li>OWASP: <a href="https://owasp.org/www-community/attacks/LDAP_Injection">LDAP Injection</a>.</li>
<li>SonarSource: <a href="https://rules.sonarsource.com/python/RSPEC-2078">RSPEC-2078</a>.</li>
<li>Python2: <a href="https://www.python-ldap.org/en/python-ldap-3.3.0/reference/ldap.html">LDAP Documentation</a>.</li>
<li>Python3: <a href="https://ldap3.readthedocs.io/en/latest/">LDAP Documentation</a>.</li>
<li>Wikipedia: <a href="https://en.wikipedia.org/wiki/LDAP_injection">LDAP injection</a>.</li>
<li>BlackHat: <a href="https://www.blackhat.com/presentations/bh-europe-08/Alonso-Parada/Whitepaper/bh-eu-08-alonso-parada-WP.pdf">LDAP Injection and Blind LDAP Injection</a>.</li>
<li>LDAP: <a href="https://ldap.com/2018/05/04/understanding-and-defending-against-ldap-injection-attacks/">Understanding and Defending Against LDAP Injection Attacks</a>.</li>
</references>
</qhelp>
21 changes: 21 additions & 0 deletions python/ql/src/experimental/Security/CWE-090/LDAPInjection.ql
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)
66 changes: 66 additions & 0 deletions python/ql/src/experimental/semmle/python/Concepts.qll
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
}
1 change: 1 addition & 0 deletions python/ql/src/experimental/semmle/python/Frameworks.qll
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
*/

private import experimental.semmle.python.frameworks.Stdlib
private import experimental.semmle.python.frameworks.LDAP
153 changes: 153 additions & 0 deletions python/ql/src/experimental/semmle/python/frameworks/LDAP.qll
Original file line number Diff line number Diff line change
@@ -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) }
}
}
}
Loading