Skip to content
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

Python: Add LDAP Improper Authentication query #5444

Merged
merged 32 commits into from
Jul 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8715d29
Upload LDAP Improper authentication query, qhelp and tests
jorgectf Mar 18, 2021
809bf23
Move to experimental folder
jorgectf Mar 18, 2021
2f874c5
Precision warn and Remove CWE (broken) reference
jorgectf Mar 18, 2021
bfd4280
Fix imports and begin refactor
jorgectf Apr 6, 2021
db1f54a
Polish query file
jorgectf Apr 7, 2021
aa7763b
Set up Concepts
jorgectf Apr 7, 2021
8ca6e84
Refactor Calls to use ApiGraphs
jorgectf Apr 7, 2021
7e45649
Set up taint config and custom sink
jorgectf Apr 7, 2021
63bd323
Improve qhelp
jorgectf Apr 8, 2021
20fc5db
Polish query file
jorgectf Apr 8, 2021
2392be0
Improve sink
jorgectf Apr 8, 2021
015d203
Improve tests, move them and create qhelp examples
jorgectf Apr 8, 2021
1320eee
Add qlref
jorgectf Apr 8, 2021
5787406
Add .expected
jorgectf Apr 8, 2021
f140601
Write documentation
jorgectf Apr 8, 2021
ae806cd
Merge branch 'github:main' into jorgectf/python/ldapimproperauth
jorgectf May 7, 2021
1662c5d
resolve merge conflict
jorgectf Jun 14, 2021
d34d2ed
Add .qlref
jorgectf Jun 17, 2021
13cfcec
Change qhelp explanation
jorgectf Jun 17, 2021
5704ac3
Rework LDAP framework modeling
jorgectf Jun 17, 2021
9cbb7e0
Change query objective
jorgectf Jun 17, 2021
1d7ddce
Update .expected
jorgectf Jun 17, 2021
dfe16aa
Python: Handle both positional and keyword args for LDAP bind
RasmusWL Jun 28, 2021
b33f6a3
Python: Fix select for py/improper-ldap-auth
RasmusWL Jun 28, 2021
4a2c99a
Python: Inline `LDAPImproperAuth.qll`
RasmusWL Jun 28, 2021
5477b2e
Python: Minor refactoring cleanup
RasmusWL Jun 28, 2021
b942251
Rephrase .qhelp
jorgectf Jun 28, 2021
1d4d8ab
Fix tests
jorgectf Jun 28, 2021
1d432af
Update `.expected`
jorgectf Jun 28, 2021
2f9e645
Hardcode `ldap2` binding functions
jorgectf Jun 29, 2021
71e6db8
Merge branch 'main' into jorgectf/python/ldapimproperauth
RasmusWL Jul 22, 2021
42a997c
Python: Fix deprecation warning
RasmusWL Jul 22, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>If an LDAP query doesn't carry any kind of authentication, anonymous binds causes an empty or None-set password
to result in a successful authentication.</p>
</overview>

<recommendation>
<p>Use a non-empty password while establishing an LDAP connection.</p>
</recommendation>

<example>
<p>In the following examples, the code builds a LDAP query whose execution carries no authentication or binds anonymously.</p>

<sample src="examples/auth_bad_2.py" />
<sample src="examples/auth_bad_3.py" />

<p>In the third and fourth examples, the authentication is established using a password.</p>

<sample src="examples/auth_good_2.py" />
<sample src="examples/auth_good_3.py" />
</example>

<references>
<li>SonarSource: <a href="https://rules.sonarsource.com/python/type/Vulnerability/RSPEC-4433">RSPEC-4433</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>
</references>
</qhelp>
31 changes: 31 additions & 0 deletions python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* @name Improper LDAP Authentication
* @description A user-controlled query carries no authentication
* @kind problem
* @problem.severity warning
* @id py/improper-ldap-auth
* @tags experimental
* security
* external/cwe/cwe-287
*/

// Determine precision above
import python
import experimental.semmle.python.Concepts
import semmle.python.dataflow.new.DataFlow

predicate authenticatesImproperly(LDAPBind ldapBind) {
(
DataFlow::localFlow(DataFlow::exprNode(any(None noneName)), ldapBind.getPassword()) or
not exists(ldapBind.getPassword())
)
or
exists(StrConst emptyString |
emptyString.getText() = "" and
DataFlow::localFlow(DataFlow::exprNode(emptyString), ldapBind.getPassword())
)
}

from LDAPBind ldapBind
where authenticatesImproperly(ldapBind)
select ldapBind, "The following LDAP bind operation is executed without authentication"
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from flask import request, Flask
import ldap
import ldap.filter
import ldap.dn


@app.route("/bind_example")
def bind_example():
dn = "dc={}".format(ldap.dn.escape_dn_chars(request.args['dc']))
search_filter = "(user={})".format(ldap.filter.escape_filter_chars(request.args['search']))

ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
ldap_connection.bind('cn=root', "")
user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from ldap3 import Server, Connection, ALL
from flask import request, Flask
from ldap3.utils.dn import escape_rdn
from ldap3.utils.conv import escape_filter_chars

@app.route("/passwordNone")
def passwordNone():
dn = "dc={}".format(escape_rdn(request.args['dc']))
search_filter = "(user={})".format(escape_filter_chars(request.args['search']))

srv = Server('servername', get_info=ALL)
conn = Connection(srv, user='user_dn', password=None)
status, result, response, _ = conn.search(dn, search_filter)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from flask import request, Flask
import ldap
import ldap.filter
import ldap.dn


@app.route("/bind_example")
def bind_example():
dn = "dc={}".format(ldap.dn.escape_dn_chars(request.args['dc']))
search_filter = "(user={})".format(ldap.filter.escape_filter_chars(request.args['search']))

ldap_connection = ldap.initialize("ldap://127.0.0.1:1337")
ldap_connection.bind('cn=root', "SecurePa$$!")
user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from ldap3 import Server, Connection, ALL
from flask import request, Flask
from ldap3.utils.dn import escape_rdn
from ldap3.utils.conv import escape_filter_chars

@app.route("/passwordFromEnv")
def passwordFromEnv():
dn = "dc={}".format(escape_rdn(request.args['dc']))
search_filter = "(user={})".format(escape_filter_chars(request.args['search']))

srv = Server('servername', get_info=ALL)
conn = Connection(srv, user='user_dn',
password="SecurePa$$!")
status, result, response, _ = conn.search(dn, search_filter)
30 changes: 30 additions & 0 deletions python/ql/src/experimental/semmle/python/Concepts.qll
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,36 @@ class LDAPEscape extends DataFlow::Node {
DataFlow::Node getAnInput() { result = range.getAnInput() }
}

/** Provides classes for modeling LDAP bind-related APIs. */
module LDAPBind {
/**
* A data-flow node that collects methods binding a LDAP connection.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `LDAPBind` instead.
*/
abstract class Range extends DataFlow::Node {
/**
* Gets the argument containing the binding expression.
*/
abstract DataFlow::Node getPassword();
}
}

/**
* A data-flow node that collects methods binding a LDAP connection.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `LDAPBind::Range` instead.
*/
class LDAPBind extends DataFlow::Node {
LDAPBind::Range range;

LDAPBind() { this = range }

DataFlow::Node getPassword() { result = range.getPassword() }
}

/** Provides classes for modeling SQL sanitization libraries. */
module SQLEscape {
/**
Expand Down
142 changes: 92 additions & 50 deletions python/ql/src/experimental/semmle/python/frameworks/LDAP.qll
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ private module LDAP {
* See https://www.python-ldap.org/en/python-ldap-3.3.0/index.html
*/
private module LDAP2 {
/** Gets a reference to the `ldap` module. */
API::Node ldap() { result = API::moduleImport("ldap") }

/** Returns a `ldap` module instance */
API::Node ldapInitialize() { result = ldap().getMember("initialize") }

/** Gets a reference to a `ldap` operation. */
private DataFlow::TypeTrackingNode ldapOperation(DataFlow::TypeTracker t) {
t.start() and
result.(DataFlow::AttrRead).getObject().getALocalSource() = ldapInitialize().getACall()
or
exists(DataFlow::TypeTracker t2 | result = ldapOperation(t2).track(t2, t))
}

/**
* List of `ldap` methods used to execute a query.
*
Expand All @@ -30,32 +44,61 @@ private module LDAP {
}
}

/** Gets a reference to a `ldap` operation. */
private DataFlow::Node ldapOperation() {
ldapOperation(DataFlow::TypeTracker::end()).flowsTo(result)
}

/** Gets a reference to a `ldap` query. */
private DataFlow::Node ldapQuery() {
result = ldapOperation() and
result.(DataFlow::AttrRead).getAttributeName() instanceof LDAP2QueryMethods
}

/**
* 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")
)
)
)
LDAP2Query() { this.getFunction() = ldapQuery() }

override DataFlow::Node getQuery() {
result in [this.getArg(0), this.getArg(2), this.getArgByName("filterstr")]
}
}

/**
* List of `ldap` methods used for binding.
*
* See https://www.python-ldap.org/en/python-ldap-3.3.0/reference/ldap.html#functions
*/
private class LDAP2BindMethods extends string {
LDAP2BindMethods() {
this in [
"bind", "bind_s", "simple_bind", "simple_bind_s", "sasl_interactive_bind_s",
"sasl_non_interactive_bind_s", "sasl_external_bind_s", "sasl_gssapi_bind_s"
]
}
}

override DataFlow::Node getQuery() { result = ldapQuery }
/** Gets a reference to a `ldap` bind. */
private DataFlow::Node ldapBind() {
result = ldapOperation() and
result.(DataFlow::AttrRead).getAttributeName() instanceof LDAP2BindMethods
}

/**
* A class to find `ldap` methods binding a connection.
*
* See `LDAP2BindMethods`
*/
private class LDAP2Bind extends DataFlow::CallCfgNode, LDAPBind::Range {
LDAP2Bind() { this.getFunction() = ldapBind() }

override DataFlow::Node getPassword() {
result in [this.getArg(1), this.getArgByName("cred")]
}
}

/**
Expand All @@ -64,9 +107,7 @@ private module LDAP {
* 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()
}
LDAP2EscapeDNCall() { this = ldap().getMember("dn").getMember("escape_dn_chars").getACall() }

override DataFlow::Node getAnInput() { result = this.getArg(0) }
}
Expand All @@ -78,8 +119,7 @@ private module LDAP {
*/
private class LDAP2EscapeFilterCall extends DataFlow::CallCfgNode, LDAPEscape::Range {
LDAP2EscapeFilterCall() {
this =
API::moduleImport("ldap").getMember("filter").getMember("escape_filter_chars").getACall()
this = ldap().getMember("filter").getMember("escape_filter_chars").getACall()
}

override DataFlow::Node getAnInput() { result = this.getArg(0) }
Expand All @@ -92,26 +132,40 @@ private module LDAP {
* See https://pypi.org/project/ldap3/
*/
private module LDAP3 {
/** Gets a reference to the `ldap3` module. */
API::Node ldap3() { result = API::moduleImport("ldap3") }

/** Gets a reference to the `ldap3` `utils` module. */
API::Node ldap3Utils() { result = ldap3().getMember("utils") }

/** Returns a `ldap3` module `Server` instance */
API::Node ldap3Server() { result = ldap3().getMember("Server") }

/** Returns a `ldap3` module `Connection` instance */
API::Node ldap3Connection() { result = ldap3().getMember("Connection") }

/**
* 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)
)
)
this.getFunction().(DataFlow::AttrRead).getObject().getALocalSource() =
ldap3Connection().getACall() and
this.getFunction().(DataFlow::AttrRead).getAttributeName() = "search"
}

override DataFlow::Node getQuery() { result = ldapQuery }
override DataFlow::Node getQuery() { result in [this.getArg(0), this.getArg(1)] }
}

/**
* A class to find `ldap3` methods binding a connection.
*/
class LDAP3Bind extends DataFlow::CallCfgNode, LDAPBind::Range {
LDAP3Bind() { this = ldap3Connection().getACall() }

override DataFlow::Node getPassword() {
result in [this.getArg(2), this.getArgByName("password")]
}
}

/**
Expand All @@ -120,14 +174,7 @@ private module LDAP {
* 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()
}
LDAP3EscapeDNCall() { this = ldap3Utils().getMember("dn").getMember("escape_rdn").getACall() }

override DataFlow::Node getAnInput() { result = this.getArg(0) }
}
Expand All @@ -139,12 +186,7 @@ private module LDAP {
*/
private class LDAP3EscapeFilterCall extends DataFlow::CallCfgNode, LDAPEscape::Range {
LDAP3EscapeFilterCall() {
this =
API::moduleImport("ldap3")
.getMember("utils")
.getMember("conv")
.getMember("escape_filter_chars")
.getACall()
this = ldap3Utils().getMember("conv").getMember("escape_filter_chars").getACall()
}

override DataFlow::Node getAnInput() { result = this.getArg(0) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
| auth_bad_2.py:19:5:19:42 | ControlFlowNode for Attribute() | The following LDAP bind operation is executed without authentication |
| auth_bad_2.py:33:5:33:44 | ControlFlowNode for Attribute() | The following LDAP bind operation is executed without authentication |
| auth_bad_2.py:47:5:47:43 | ControlFlowNode for Attribute() | The following LDAP bind operation is executed without authentication |
| auth_bad_2.py:60:5:60:52 | ControlFlowNode for Attribute() | The following LDAP bind operation is executed without authentication |
| auth_bad_2.py:73:5:73:39 | ControlFlowNode for Attribute() | The following LDAP bind operation is executed without authentication |
| auth_bad_2.py:87:5:87:48 | ControlFlowNode for Attribute() | The following LDAP bind operation is executed without authentication |
| auth_bad_3.py:19:12:19:43 | ControlFlowNode for Connection() | The following LDAP bind operation is executed without authentication |
| auth_bad_3.py:33:12:33:57 | ControlFlowNode for Connection() | The following LDAP bind operation is executed without authentication |
| auth_bad_3.py:46:12:46:55 | ControlFlowNode for Connection() | The following LDAP bind operation is executed without authentication |
| auth_bad_3.py:60:12:60:42 | ControlFlowNode for Connection() | The following LDAP bind operation is executed without authentication |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
experimental/Security/CWE-287/ImproperLdapAuth.ql
Loading