-
Notifications
You must be signed in to change notification settings - Fork 2k
Python: Add LDAP Insecure Authentication query #5445
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
RasmusWL
merged 23 commits into
github:main
from
jorgectf:jorgectf/python/ldapinsecureauth
Sep 23, 2021
Merged
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 3ce0a9c
Move to experimental folder
jorgectf 957b3e1
Precision warn
jorgectf edb273a
Merge remote-tracking branch 'origin/jorgectf/python/ldapimproperauth…
jorgectf a34d6d3
Port to ApiGraphs and finish the query
jorgectf b03e75e
Extend `ldap3`'s `start_tls` and fix tests
jorgectf f02b6d6
Merge branch 'github:main' into jorgectf/python/ldapinsecureauth
jorgectf d458464
Apply suggestions from code review
jorgectf 786edb7
Update `.expected`
jorgectf 64b305c
Add `.qhelp` along with its example
jorgectf 1bc16fb
Apply suggestions from code review
jorgectf ee98c0c
Add `start_tls_s()` comment and use `DataFlow::MethodCallNode` instead
jorgectf b802d79
Fix `OPT_X_TLS_` mandatory options
jorgectf 8008011
Fix taint tracking comment
jorgectf 4e261c6
Optimize `concatAndCompareAgainstFullHostRegex`
jorgectf 54012eb
Optimize `getFullHostRegex`
jorgectf 18b05bc
Fix tests and add global option
jorgectf 3cf28ad
Merge remote-tracking branch 'origin/main' into jorgectf/python/ldapi…
jorgectf 353c0a9
Add missing comment
jorgectf 2ccc6dc
Merge branch 'main' into jorgectf/python/ldapinsecureauth
jorgectf b505662
Fix global test and update `.expected`
jorgectf 70489b2
Merge branch 'main' into jorgectf/python/ldapinsecureauth
RasmusWL ef6e502
Python: Make LDAP global options test better
RasmusWL File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
20 changes: 20 additions & 0 deletions
20
python/ql/src/experimental/Security/CWE-522/LDAPInsecureAuth.ql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| * @tags experimental | ||
| * security | ||
| * external/cwe/cwe-522 | ||
|
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" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
108 changes: 108 additions & 0 deletions
108
python/ql/src/experimental/semmle/python/security/LDAPInsecureAuth.qll
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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].*" } | ||
|
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. | ||
|
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) { | ||
|
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()) | ||
| ) | ||
| } | ||
| } | ||
|
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. | ||
|
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 | ||
|
jorgectf marked this conversation as resolved.
|
||
| } | ||
|
|
||
| override predicate isSink(DataFlow::Node sink) { | ||
| exists(LDAPBind ldapBind | not ldapBind.useSSL() and sink = ldapBind.getHost()) | ||
| } | ||
| } | ||
39 changes: 39 additions & 0 deletions
39
python/ql/test/experimental/query-tests/Security/CWE-522/LDAPInsecureAuth.expected
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
1 change: 1 addition & 0 deletions
1
python/ql/test/experimental/query-tests/Security/CWE-522/LDAPInsecureAuth.qlref
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| experimental/Security/CWE-522/LDAPInsecureAuth.ql |
66 changes: 66 additions & 0 deletions
66
python/ql/test/experimental/query-tests/Security/CWE-522/ldap2_private.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) | ||
|
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) | ||
|
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) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.