Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7de9214
Upload LDAP Insecure authentication query and tests
jorgectf Mar 18, 2021
3ce0a9c
Move to experimental folder
jorgectf Mar 18, 2021
957b3e1
Precision warn
jorgectf Mar 18, 2021
edb273a
Merge remote-tracking branch 'origin/jorgectf/python/ldapimproperauth…
jorgectf Jul 22, 2021
a34d6d3
Port to ApiGraphs and finish the query
jorgectf Jul 22, 2021
b03e75e
Extend `ldap3`'s `start_tls` and fix tests
jorgectf Jul 22, 2021
f02b6d6
Merge branch 'github:main' into jorgectf/python/ldapinsecureauth
jorgectf Jul 22, 2021
d458464
Apply suggestions from code review
jorgectf Aug 26, 2021
786edb7
Update `.expected`
jorgectf Aug 26, 2021
64b305c
Add `.qhelp` along with its example
jorgectf Aug 26, 2021
1bc16fb
Apply suggestions from code review
jorgectf Sep 7, 2021
ee98c0c
Add `start_tls_s()` comment and use `DataFlow::MethodCallNode` instead
jorgectf Sep 7, 2021
b802d79
Fix `OPT_X_TLS_` mandatory options
jorgectf Sep 7, 2021
8008011
Fix taint tracking comment
jorgectf Sep 7, 2021
4e261c6
Optimize `concatAndCompareAgainstFullHostRegex`
jorgectf Sep 7, 2021
54012eb
Optimize `getFullHostRegex`
jorgectf Sep 12, 2021
18b05bc
Fix tests and add global option
jorgectf Sep 12, 2021
3cf28ad
Merge remote-tracking branch 'origin/main' into jorgectf/python/ldapi…
jorgectf Sep 12, 2021
353c0a9
Add missing comment
jorgectf Sep 12, 2021
2ccc6dc
Merge branch 'main' into jorgectf/python/ldapinsecureauth
jorgectf Sep 14, 2021
b505662
Fix global test and update `.expected`
jorgectf Sep 14, 2021
70489b2
Merge branch 'main' into jorgectf/python/ldapinsecureauth
RasmusWL Sep 23, 2021
ef6e502
Python: Make LDAP global options test better
RasmusWL Sep 23, 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
20 changes: 20 additions & 0 deletions python/ql/src/experimental/Security/CWE-522/LDAPInsecureAuth.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @name Python Insecure LDAP Authentication
* @description Python LDAP Insecure LDAP Authentication
* @kind path-problem
* @problem.severity error
* @id python/insecure-ldap-auth
Comment thread
jorgectf marked this conversation as resolved.
Outdated
* @tags experimental
* security
* external/cwe/cwe-522
Comment thread
jorgectf marked this conversation as resolved.
*/

// determine precision above
import python
import DataFlow::PathGraph
import experimental.semmle.python.security.LDAPInsecureAuth

from LDAPInsecureAuthConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "$@ is authenticated insecurely.", sink.getNode(),
"This LDAP host"
23 changes: 23 additions & 0 deletions python/ql/src/experimental/semmle/python/Concepts.qll
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,20 @@ module LDAPBind {
* extend `LDAPBind` instead.
*/
abstract class Range extends DataFlow::Node {
/**
* Gets the argument containing the binding host.
*/
abstract DataFlow::Node getHost();

/**
* Gets the argument containing the binding expression.
*/
abstract DataFlow::Node getPassword();

/**
* Checks if the binding process use SSL.
Comment thread
jorgectf marked this conversation as resolved.
Outdated
*/
abstract predicate useSSL();
}
}

Expand All @@ -174,7 +184,20 @@ class LDAPBind extends DataFlow::Node {

LDAPBind() { this = range }

/**
* Gets the argument containing the binding host.
*/
DataFlow::Node getHost() { result = range.getHost() }

/**
* Gets the argument containing the binding expression.
*/
DataFlow::Node getPassword() { result = range.getPassword() }

/**
* Checks if the binding process use SSL.
Comment thread
jorgectf marked this conversation as resolved.
Outdated
*/
predicate useSSL() { range.useSSL() }
}

/** Provides classes for modeling SQL sanitization libraries. */
Expand Down
59 changes: 59 additions & 0 deletions python/ql/src/experimental/semmle/python/frameworks/LDAP.qll
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,42 @@ private module LDAP {
override DataFlow::Node getPassword() {
result in [this.getArg(1), this.getArgByName("cred")]
}

override DataFlow::Node getHost() {
exists(DataFlow::CallCfgNode initialize |
this.getFunction().(DataFlow::AttrRead).getObject().getALocalSource() = initialize and
initialize = ldapInitialize().getACall() and
result = initialize.getArg(0)
)
}

override predicate useSSL() {
// use initialize to correlate `this` and so avoid FP in several instances
exists(DataFlow::CallCfgNode initialize |
this.getFunction().(DataFlow::AttrRead).getObject().getALocalSource() = initialize and
initialize = ldapInitialize().getACall() and
(
// ldap_connection.start_tls_s()
Comment thread
jorgectf marked this conversation as resolved.
exists(DataFlow::AttrRead startTLS |
Comment thread
RasmusWL marked this conversation as resolved.
Outdated
startTLS.getObject().getALocalSource() = initialize and
startTLS.getAttributeName().matches("%start_tls%")
Comment thread
jorgectf marked this conversation as resolved.
Outdated
)
or
// ldap_connection.set_option(ldap.OPT_X_TLS_%s, True)
// ldap_connection.set_option(ldap.OPT_X_TLS_%s)
Comment thread
jorgectf marked this conversation as resolved.
Outdated
exists(DataFlow::CallCfgNode setOption |
setOption.getFunction().(DataFlow::AttrRead).getObject().getALocalSource() =
initialize and
setOption.getFunction().(DataFlow::AttrRead).getAttributeName() = "set_option" and
setOption.getArg(0) =
ldap().getMember("OPT_X_TLS_" + ["ALLOW", "TRY", "DEMAND", "HARD"]).getAUse() and
Comment thread
RasmusWL marked this conversation as resolved.
Outdated
not DataFlow::exprNode(any(False falseExpr))
.(DataFlow::LocalSourceNode)
.flowsTo(setOption.getArg(1))
)
)
)
}
}

/**
Expand Down Expand Up @@ -166,6 +202,29 @@ private module LDAP {
override DataFlow::Node getPassword() {
result in [this.getArg(2), this.getArgByName("password")]
}

override DataFlow::Node getHost() {
exists(DataFlow::CallCfgNode serverCall |
serverCall = ldap3Server().getACall() and
this.getArg(0).getALocalSource() = serverCall and
result = serverCall.getArg(0)
)
}

override predicate useSSL() {
exists(DataFlow::CallCfgNode serverCall |
serverCall = ldap3Server().getACall() and
this.getArg(0).getALocalSource() = serverCall and
DataFlow::exprNode(any(True trueExpr))
.(DataFlow::LocalSourceNode)
.flowsTo([serverCall.getArg(2), serverCall.getArgByName("use_ssl")])
)
or
exists(DataFlow::AttrRead startTLS |
startTLS.getAttributeName().matches("%start_tls%") and
Comment thread
RasmusWL marked this conversation as resolved.
Outdated
startTLS.getObject().getALocalSource() = this
)
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* Provides a taint-tracking configuration for detecting LDAP injection vulnerabilities
*/

import python
import semmle.python.dataflow.new.DataFlow
import semmle.python.dataflow.new.TaintTracking
import semmle.python.dataflow.new.RemoteFlowSources
import experimental.semmle.python.Concepts

string getFullHostRegex() { result = "(?i)ldap://[\\[a-zA-Z0-9].*" }
Comment thread
jorgectf marked this conversation as resolved.
Outdated

string getSchemaRegex() { result = "(?i)ldap(://)?" }

string getPrivateHostRegex() {
result =
"(?i)localhost(?:[:/?#].*)?|127\\.0\\.0\\.1(?:[:/?#].*)?|10(?:\\.[0-9]+){3}(?:[:/?#].*)?|172\\.16(?:\\.[0-9]+){2}(?:[:/?#].*)?|192.168(?:\\.[0-9]+){2}(?:[:/?#].*)?|\\[?0:0:0:0:0:0:0:1\\]?(?:[:/?#].*)?|\\[?::1\\]?(?:[:/?#].*)?"
}

// "ldap://somethingon.theinternet.com"
class LDAPFullHost extends StrConst {
LDAPFullHost() {
exists(string s |
s = this.getText() and
s.regexpMatch(getFullHostRegex()) and
not s.substring(7, s.length()).regexpMatch(getPrivateHostRegex()) // No need to check for ldaps, it would be SSL by default.
Comment thread
jorgectf marked this conversation as resolved.
Outdated
)
}
}

class LDAPSchema extends StrConst {
LDAPSchema() { this.getText().regexpMatch(getSchemaRegex()) }
}

class LDAPPrivateHost extends StrConst {
LDAPPrivateHost() { this.getText().regexpMatch(getPrivateHostRegex()) }
}

predicate concatAndCompareAgainstFullHostRegex(Expr schema, Expr host) {
Comment thread
jorgectf marked this conversation as resolved.
Outdated
schema instanceof LDAPSchema and
not host instanceof LDAPPrivateHost and
exists(string full_host |
full_host = schema.(StrConst).getText() + host.(StrConst).getText() and
full_host.regexpMatch(getFullHostRegex())
)
}

// "ldap://" + "somethingon.theinternet.com"
class LDAPBothStrings extends BinaryExpr {
LDAPBothStrings() { concatAndCompareAgainstFullHostRegex(this.getLeft(), this.getRight()) }
}

// schema + host
class LDAPBothVar extends BinaryExpr {
LDAPBothVar() {
exists(SsaVariable schemaVar, SsaVariable hostVar |
this.getLeft() = schemaVar.getVariable().getALoad() and // getAUse is incompatible with Expr
this.getRight() = hostVar.getVariable().getALoad() and
concatAndCompareAgainstFullHostRegex(schemaVar
.getDefinition()
.getImmediateDominator()
.getNode(), hostVar.getDefinition().getImmediateDominator().getNode())
)
}
}
Comment thread
jorgectf marked this conversation as resolved.

// schema + "somethingon.theinternet.com"
class LDAPVarString extends BinaryExpr {
LDAPVarString() {
exists(SsaVariable schemaVar |
this.getLeft() = schemaVar.getVariable().getALoad() and
concatAndCompareAgainstFullHostRegex(schemaVar
.getDefinition()
.getImmediateDominator()
.getNode(), this.getRight())
)
}
}

// "ldap://" + host
class LDAPStringVar extends BinaryExpr {
LDAPStringVar() {
exists(SsaVariable hostVar |
this.getRight() = hostVar.getVariable().getALoad() and
concatAndCompareAgainstFullHostRegex(this.getLeft(),
hostVar.getDefinition().getImmediateDominator().getNode())
)
}
}

/**
* A taint-tracking configuration for detecting LDAP injections.
Comment thread
RasmusWL marked this conversation as resolved.
Outdated
*/
class LDAPInsecureAuthConfig extends TaintTracking::Configuration {
LDAPInsecureAuthConfig() { this = "LDAPInsecureAuthConfig" }

override predicate isSource(DataFlow::Node source) {
source instanceof RemoteFlowSource or
source.asExpr() instanceof LDAPBothStrings or
source.asExpr() instanceof LDAPBothVar or
source.asExpr() instanceof LDAPVarString or
source.asExpr() instanceof LDAPStringVar
Comment thread
jorgectf marked this conversation as resolved.
}

override predicate isSink(DataFlow::Node sink) {
exists(LDAPBind ldapBind | not ldapBind.useSSL() and sink = ldapBind.getHost())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
edges
| ldap3_remote.py:49:12:49:34 | ControlFlowNode for BinaryExpr | ldap3_remote.py:51:18:51:21 | ControlFlowNode for host |
| ldap3_remote.py:88:21:88:27 | ControlFlowNode for request | ldap3_remote.py:88:21:88:32 | ControlFlowNode for Attribute |
| ldap3_remote.py:88:21:88:32 | ControlFlowNode for Attribute | ldap3_remote.py:88:21:88:40 | ControlFlowNode for Subscript |
| ldap3_remote.py:88:21:88:40 | ControlFlowNode for Subscript | ldap3_remote.py:90:18:90:21 | ControlFlowNode for host |
| ldap3_remote.py:101:12:101:49 | ControlFlowNode for BinaryExpr | ldap3_remote.py:102:18:102:21 | ControlFlowNode for host |
| ldap3_remote.py:114:12:114:49 | ControlFlowNode for BinaryExpr | ldap3_remote.py:115:18:115:21 | ControlFlowNode for host |
| ldap3_remote.py:126:12:126:31 | ControlFlowNode for BinaryExpr | ldap3_remote.py:127:18:127:21 | ControlFlowNode for host |
| ldap3_remote.py:138:21:138:27 | ControlFlowNode for request | ldap3_remote.py:138:21:138:32 | ControlFlowNode for Attribute |
| ldap3_remote.py:138:21:138:32 | ControlFlowNode for Attribute | ldap3_remote.py:138:21:138:40 | ControlFlowNode for Subscript |
| ldap3_remote.py:138:21:138:40 | ControlFlowNode for Subscript | ldap3_remote.py:139:18:139:21 | ControlFlowNode for host |
nodes
| ldap2_remote.py:45:41:45:60 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
| ldap2_remote.py:56:41:56:60 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
| ldap3_remote.py:49:12:49:34 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
| ldap3_remote.py:51:18:51:21 | ControlFlowNode for host | semmle.label | ControlFlowNode for host |
| ldap3_remote.py:88:21:88:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
| ldap3_remote.py:88:21:88:32 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
| ldap3_remote.py:88:21:88:40 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
| ldap3_remote.py:90:18:90:21 | ControlFlowNode for host | semmle.label | ControlFlowNode for host |
| ldap3_remote.py:101:12:101:49 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
| ldap3_remote.py:102:18:102:21 | ControlFlowNode for host | semmle.label | ControlFlowNode for host |
| ldap3_remote.py:114:12:114:49 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
| ldap3_remote.py:115:18:115:21 | ControlFlowNode for host | semmle.label | ControlFlowNode for host |
| ldap3_remote.py:126:12:126:31 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
| ldap3_remote.py:127:18:127:21 | ControlFlowNode for host | semmle.label | ControlFlowNode for host |
| ldap3_remote.py:138:21:138:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
| ldap3_remote.py:138:21:138:32 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
| ldap3_remote.py:138:21:138:40 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
| ldap3_remote.py:139:18:139:21 | ControlFlowNode for host | semmle.label | ControlFlowNode for host |
#select
| ldap2_remote.py:45:41:45:60 | ControlFlowNode for BinaryExpr | ldap2_remote.py:45:41:45:60 | ControlFlowNode for BinaryExpr | ldap2_remote.py:45:41:45:60 | ControlFlowNode for BinaryExpr | $@ is authenticated insecurely. | ldap2_remote.py:45:41:45:60 | ControlFlowNode for BinaryExpr | This LDAP host |
| ldap2_remote.py:56:41:56:60 | ControlFlowNode for BinaryExpr | ldap2_remote.py:56:41:56:60 | ControlFlowNode for BinaryExpr | ldap2_remote.py:56:41:56:60 | ControlFlowNode for BinaryExpr | $@ is authenticated insecurely. | ldap2_remote.py:56:41:56:60 | ControlFlowNode for BinaryExpr | This LDAP host |
| ldap3_remote.py:51:18:51:21 | ControlFlowNode for host | ldap3_remote.py:49:12:49:34 | ControlFlowNode for BinaryExpr | ldap3_remote.py:51:18:51:21 | ControlFlowNode for host | $@ is authenticated insecurely. | ldap3_remote.py:51:18:51:21 | ControlFlowNode for host | This LDAP host |
| ldap3_remote.py:90:18:90:21 | ControlFlowNode for host | ldap3_remote.py:88:21:88:27 | ControlFlowNode for request | ldap3_remote.py:90:18:90:21 | ControlFlowNode for host | $@ is authenticated insecurely. | ldap3_remote.py:90:18:90:21 | ControlFlowNode for host | This LDAP host |
| ldap3_remote.py:102:18:102:21 | ControlFlowNode for host | ldap3_remote.py:101:12:101:49 | ControlFlowNode for BinaryExpr | ldap3_remote.py:102:18:102:21 | ControlFlowNode for host | $@ is authenticated insecurely. | ldap3_remote.py:102:18:102:21 | ControlFlowNode for host | This LDAP host |
| ldap3_remote.py:115:18:115:21 | ControlFlowNode for host | ldap3_remote.py:114:12:114:49 | ControlFlowNode for BinaryExpr | ldap3_remote.py:115:18:115:21 | ControlFlowNode for host | $@ is authenticated insecurely. | ldap3_remote.py:115:18:115:21 | ControlFlowNode for host | This LDAP host |
| ldap3_remote.py:127:18:127:21 | ControlFlowNode for host | ldap3_remote.py:126:12:126:31 | ControlFlowNode for BinaryExpr | ldap3_remote.py:127:18:127:21 | ControlFlowNode for host | $@ is authenticated insecurely. | ldap3_remote.py:127:18:127:21 | ControlFlowNode for host | This LDAP host |
| ldap3_remote.py:139:18:139:21 | ControlFlowNode for host | ldap3_remote.py:138:21:138:27 | ControlFlowNode for request | ldap3_remote.py:139:18:139:21 | ControlFlowNode for host | $@ is authenticated insecurely. | ldap3_remote.py:139:18:139:21 | ControlFlowNode for host | This LDAP host |
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
experimental/Security/CWE-522/LDAPInsecureAuth.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import ldap
from flask import request, Flask

app = Flask(__name__)

schema = "ldap://"
remote_host = "127.0.0.1"
full_host = "ldap://127.0.0.1"


# GOOD

# SSL through ldap variable option
# host from variable + variable

@app.route("/one")
def one():
ldap_connection_1 = ldap.initialize(schema + remote_host)
ldap_connection_1.set_option(ldap.OPT_X_TLS_DEMAND, True)
Comment thread
jorgectf marked this conversation as resolved.
ldap_connection_1.simple_bind_s('', '')
user = ldap_connection_1.search_s(
"dn", ldap.SCOPE_SUBTREE, "search_filter")

return user


# SSL through ldap start_tls_s
# hardcoded host

@app.route("/two")
def two():
ldap_connection_2 = ldap.initialize("ldap://127.0.0.1")
ldap_connection_2.start_tls_s()
ldap_connection_2.simple_bind_s('', '')
user = ldap_connection_2.search_s(
"dn", ldap.SCOPE_SUBTREE, "search_filter")

return user


# BAD (not a sink because it's private)

@app.route("/one_bad")
def one_bad():
ldap_connection_3 = ldap.initialize(schema + remote_host)
ldap_connection_3.set_option(ldap.OPT_X_TLS_DEMAND, False)
ldap_connection_3.simple_bind_s('', '')
user = ldap_connection_3.search_s(
"dn", ldap.SCOPE_SUBTREE, "search_filter")

return user


@app.route("/one_bad_2")
def one_bad_2():
ldap_connection_4 = ldap.initialize(schema + remote_host)
ldap_connection_4.set_option(ldap.OPT_X_TLS_NEVER)
Comment thread
jorgectf marked this conversation as resolved.
Outdated
ldap_connection_4.simple_bind_s('', '')
user = ldap_connection_4.search_s(
"dn", ldap.SCOPE_SUBTREE, "search_filter")

return user


# if __name__ == "__main__":
# app.run(debug=True)
Loading