From 8715d29a440020522753874b8edd08fcad3715f7 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 18 Mar 2021 17:35:05 +0100 Subject: [PATCH 01/29] Upload LDAP Improper authentication query, qhelp and tests --- .../Security/CWE-287/LDAPImproperAuth.qhelp | 31 +++++++ .../src/Security/CWE-287/LDAPImproperAuth.ql | 85 +++++++++++++++++++ .../src/Security/CWE-287/tests/3_auth_bad.py | 30 +++++++ .../src/Security/CWE-287/tests/3_auth_good.py | 20 +++++ .../ql/src/Security/CWE-287/tests/auth_bad.py | 52 ++++++++++++ .../src/Security/CWE-287/tests/auth_good.py | 52 ++++++++++++ 6 files changed, 270 insertions(+) create mode 100644 python/ql/src/Security/CWE-287/LDAPImproperAuth.qhelp create mode 100644 python/ql/src/Security/CWE-287/LDAPImproperAuth.ql create mode 100644 python/ql/src/Security/CWE-287/tests/3_auth_bad.py create mode 100644 python/ql/src/Security/CWE-287/tests/3_auth_good.py create mode 100644 python/ql/src/Security/CWE-287/tests/auth_bad.py create mode 100644 python/ql/src/Security/CWE-287/tests/auth_good.py diff --git a/python/ql/src/Security/CWE-287/LDAPImproperAuth.qhelp b/python/ql/src/Security/CWE-287/LDAPImproperAuth.qhelp new file mode 100644 index 000000000000..7554ae5c50bc --- /dev/null +++ b/python/ql/src/Security/CWE-287/LDAPImproperAuth.qhelp @@ -0,0 +1,31 @@ + + + + + +

If an LDAP connection doesn't carry any kind of authentication, gets produced an access to information + in the LDAP directory.

+ +

Simple authentication in LDAP can be used with three different mechanisms:

+ +

  • Anonymous Authentication Mechanism by performing a bind request with a username and password value of zero length.
  • +
  • Unauthenticated Authentication Mechanism by performing a bind request with a password value of zero length.
  • +
  • Name/Password Authentication Mechanism by performing a bind request with a password value of non-zero length.
  • + +
    + + +

    Every LDAP authentication should be done by using a password taken from a safe place. + + + +

  • + SonarSource + RSPEC-4433 +
  • +
  • + CWE- + 287 + + + \ No newline at end of file diff --git a/python/ql/src/Security/CWE-287/LDAPImproperAuth.ql b/python/ql/src/Security/CWE-287/LDAPImproperAuth.ql new file mode 100644 index 000000000000..912ba3856160 --- /dev/null +++ b/python/ql/src/Security/CWE-287/LDAPImproperAuth.ql @@ -0,0 +1,85 @@ +/** + * @name Python LDAP Improper Authentication + * @description Check if a user-controlled query carry no authentication + * @kind path-problem + * @problem.severity warning + * @id python/ldap-improper-auth + * @tags experimental + * security + * external/cwe/cwe-287 + */ + +import python +import semmle.python.dataflow.new.RemoteFlowSources +import semmle.python.dataflow.new.DataFlow +import semmle.python.dataflow.new.TaintTracking +import semmle.python.dataflow.new.internal.TaintTrackingPublic +import DataFlow::PathGraph + +// LDAP2 +class BindSink extends DataFlow::Node { + BindSink() { + exists(SsaVariable bindVar, CallNode bindCall, CallNode searchCall | + // get variable initializing the connection + bindVar.getDefinition().getImmediateDominator() = Value::named("ldap.initialize").getACall() and + // get a call using that variable + bindVar.getAUse().getImmediateDominator() = bindCall and + // restrict call to any bind method + bindCall.getNode().getFunc().(Attribute).getName().matches("%bind%") and + ( + // check second argument (password) + bindCall.getArg(1).getNode() instanceof None or + count(bindCall.getAnArg()) = 1 + ) and + // get another call using that variable + bindVar.getAUse().getNode() = searchCall.getNode().getFunc().(Attribute).getObject() and + // restrict call to any search method + searchCall.getNode().getFunc().(Attribute).getName().matches("%search%") and + // set the third argument as sink + this.asExpr() = searchCall.getArg(2).getNode() + ) + } +} + +// LDAP3 +class ConnectionSink extends DataFlow::Node { + ConnectionSink() { + exists(SsaVariable connectionVar, CallNode connectionCall, CallNode searchCall | + // get call initializing the connection + connectionCall = Value::named("ldap3.Connection").getACall() and + ( + // check password argument + not exists(connectionCall.getArgByName("password")) or + connectionCall.getArgByName("password").pointsTo(Value::named("None")) + ) and + // get the variable initializing the connection + connectionVar.getDefinition().getImmediateDominator() = connectionCall and + // get a call using that variable + connectionVar.getAUse().getNode() = searchCall.getNode().getFunc().(Attribute).getObject() and + // restrict call to any search method + searchCall.getNode().getFunc().(Attribute).getName().matches("%search%") and + // set the second argument as sink + this.asExpr() = searchCall.getArg(1).getNode() + ) + } +} + +class LDAPImproperAuthSink extends DataFlow::Node { + LDAPImproperAuthSink() { + this instanceof BindSink or + this instanceof ConnectionSink + } +} + +class LDAPImproperAuthConfig extends TaintTracking::Configuration { + LDAPImproperAuthConfig() { this = "LDAPImproperAuthConfig" } + + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { sink instanceof LDAPImproperAuthSink } +} + +from LDAPImproperAuthConfig config, DataFlow::PathNode source, DataFlow::PathNode sink +where config.hasFlowPath(source, sink) +select sink.getNode(), source, sink, "$@ LDAP query executes $@.", sink.getNode(), "This", + source.getNode(), "a user-provided value without authentication." diff --git a/python/ql/src/Security/CWE-287/tests/3_auth_bad.py b/python/ql/src/Security/CWE-287/tests/3_auth_bad.py new file mode 100644 index 000000000000..5726ade823cf --- /dev/null +++ b/python/ql/src/Security/CWE-287/tests/3_auth_bad.py @@ -0,0 +1,30 @@ +from ldap3 import Server, Connection, ALL +from flask import request, Flask + +app = Flask(__name__) + + +@app.route("/passwordNone") +def passwordNone(): + dn = request.args['dc'] + search_filter = 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) + return result + + +@app.route("/notPassword") +def notPassword(): + dn = request.args['dc'] + search_filter = request.args['search'] + + srv = Server('servername', get_info=ALL) + conn = Connection(srv, user='user_dn') + status, result, response, _ = conn.search(dn, search_filter) + return result + + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/src/Security/CWE-287/tests/3_auth_good.py b/python/ql/src/Security/CWE-287/tests/3_auth_good.py new file mode 100644 index 000000000000..9c45ad58e349 --- /dev/null +++ b/python/ql/src/Security/CWE-287/tests/3_auth_good.py @@ -0,0 +1,20 @@ +from ldap3 import Server, Connection, ALL +from flask import request, Flask +import os + +app = Flask(__name__) + + +@app.route("/passwordFromEnv") +def passwordFromEnv(): + dn = request.args['dc'] + search_filter = request.args['search'] + + srv = Server('servername', get_info=ALL) + conn = Connection(srv, user='user_dn', + password=os.environ.get('LDAP_PASSWORD')) + status, result, response, _ = conn.search(dn, search_filter) + return result + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/src/Security/CWE-287/tests/auth_bad.py b/python/ql/src/Security/CWE-287/tests/auth_bad.py new file mode 100644 index 000000000000..36323e71ee3d --- /dev/null +++ b/python/ql/src/Security/CWE-287/tests/auth_bad.py @@ -0,0 +1,52 @@ +from flask import request, Flask +import ldap + +app = Flask(__name__) + + +@app.route("/simple_bind") +def simple_bind(): + dn = request.args['dc'] + search_filter = request.args['search'] + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.simple_bind('cn=root') + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + return user[0] + + +@app.route("/simple_bind_s") +def simple_bind_s(): + dn = request.args['dc'] + search_filter = request.args['search'] + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.simple_bind_s('cn=root') + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + return user[0] + + +@app.route("/bind_s") +def bind_s(): + dn = request.args['dc'] + search_filter = request.args['search'] + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.bind_s('cn=root', None) + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + return user[0] + + +@app.route("/bind") +def bind(): + dn = request.args['dc'] + search_filter = request.args['search'] + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.bind('cn=root', None) + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + return user[0] + + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/src/Security/CWE-287/tests/auth_good.py b/python/ql/src/Security/CWE-287/tests/auth_good.py new file mode 100644 index 000000000000..884bcb7af264 --- /dev/null +++ b/python/ql/src/Security/CWE-287/tests/auth_good.py @@ -0,0 +1,52 @@ +from flask import request, Flask +import ldap +import os + +app = Flask(__name__) + + +@app.route("/simple_bind") +def simple_bind(): + dn = request.args['dc'] + search_filter = request.args['search'] + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.simple_bind('cn=root', os.environ.get('LDAP_PASSWORD')) + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + return user[0] + + +@app.route("/simple_bind_s") +def simple_bind_s(): + dn = request.args['dc'] + search_filter = request.args['search'] + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.simple_bind_s('cn=root', os.environ.get('LDAP_PASSWORD')) + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + return user[0] + + +@app.route("/bind_s") +def bind_s(): + dn = request.args['dc'] + search_filter = request.args['search'] + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.bind_s('cn=root', os.environ.get('LDAP_PASSWORD')) + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + return user[0] + + +@app.route("/bind") +def bind(): + dn = request.args['dc'] + search_filter = request.args['search'] + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.bind('cn=root', os.environ.get('LDAP_PASSWORD')) + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + return user[0] + +# if __name__ == "__main__": +# app.run(debug=True) From 809bf2377e35c0f67e47af536bc7be3ba993299f Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 18 Mar 2021 20:21:00 +0100 Subject: [PATCH 02/29] Move to experimental folder --- .../{ => experimental}/Security/CWE-287/LDAPImproperAuth.qhelp | 0 .../src/{ => experimental}/Security/CWE-287/LDAPImproperAuth.ql | 0 .../src/{ => experimental}/Security/CWE-287/tests/3_auth_bad.py | 0 .../src/{ => experimental}/Security/CWE-287/tests/3_auth_good.py | 0 .../ql/src/{ => experimental}/Security/CWE-287/tests/auth_bad.py | 0 .../ql/src/{ => experimental}/Security/CWE-287/tests/auth_good.py | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename python/ql/src/{ => experimental}/Security/CWE-287/LDAPImproperAuth.qhelp (100%) rename python/ql/src/{ => experimental}/Security/CWE-287/LDAPImproperAuth.ql (100%) rename python/ql/src/{ => experimental}/Security/CWE-287/tests/3_auth_bad.py (100%) rename python/ql/src/{ => experimental}/Security/CWE-287/tests/3_auth_good.py (100%) rename python/ql/src/{ => experimental}/Security/CWE-287/tests/auth_bad.py (100%) rename python/ql/src/{ => experimental}/Security/CWE-287/tests/auth_good.py (100%) diff --git a/python/ql/src/Security/CWE-287/LDAPImproperAuth.qhelp b/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.qhelp similarity index 100% rename from python/ql/src/Security/CWE-287/LDAPImproperAuth.qhelp rename to python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.qhelp diff --git a/python/ql/src/Security/CWE-287/LDAPImproperAuth.ql b/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.ql similarity index 100% rename from python/ql/src/Security/CWE-287/LDAPImproperAuth.ql rename to python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.ql diff --git a/python/ql/src/Security/CWE-287/tests/3_auth_bad.py b/python/ql/src/experimental/Security/CWE-287/tests/3_auth_bad.py similarity index 100% rename from python/ql/src/Security/CWE-287/tests/3_auth_bad.py rename to python/ql/src/experimental/Security/CWE-287/tests/3_auth_bad.py diff --git a/python/ql/src/Security/CWE-287/tests/3_auth_good.py b/python/ql/src/experimental/Security/CWE-287/tests/3_auth_good.py similarity index 100% rename from python/ql/src/Security/CWE-287/tests/3_auth_good.py rename to python/ql/src/experimental/Security/CWE-287/tests/3_auth_good.py diff --git a/python/ql/src/Security/CWE-287/tests/auth_bad.py b/python/ql/src/experimental/Security/CWE-287/tests/auth_bad.py similarity index 100% rename from python/ql/src/Security/CWE-287/tests/auth_bad.py rename to python/ql/src/experimental/Security/CWE-287/tests/auth_bad.py diff --git a/python/ql/src/Security/CWE-287/tests/auth_good.py b/python/ql/src/experimental/Security/CWE-287/tests/auth_good.py similarity index 100% rename from python/ql/src/Security/CWE-287/tests/auth_good.py rename to python/ql/src/experimental/Security/CWE-287/tests/auth_good.py From 2f874c5c0b5e637cae37df1f717437cf9a2fa3f0 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 18 Mar 2021 20:38:46 +0100 Subject: [PATCH 03/29] Precision warn and Remove CWE (broken) reference --- .../src/experimental/Security/CWE-287/LDAPImproperAuth.qhelp | 3 --- .../ql/src/experimental/Security/CWE-287/LDAPImproperAuth.ql | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.qhelp b/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.qhelp index 7554ae5c50bc..64fb94c1c9bf 100644 --- a/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.qhelp +++ b/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.qhelp @@ -23,9 +23,6 @@ SonarSource RSPEC-4433
  • -
  • - CWE- - 287 \ No newline at end of file diff --git a/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.ql b/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.ql index 912ba3856160..19a899accd0b 100644 --- a/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.ql +++ b/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.ql @@ -9,6 +9,7 @@ * external/cwe/cwe-287 */ +// Determine precision above import python import semmle.python.dataflow.new.RemoteFlowSources import semmle.python.dataflow.new.DataFlow From bfd4280d353026ceae7aa0e427516d94708bd3f3 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Tue, 6 Apr 2021 15:51:37 +0200 Subject: [PATCH 04/29] Fix imports and begin refactor --- python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.ql | 1 - .../experimental/semmle/python/security/LDAPImproperAuth.qll | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll diff --git a/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.ql b/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.ql index 19a899accd0b..029dfb313a7b 100644 --- a/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.ql +++ b/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.ql @@ -14,7 +14,6 @@ import python import semmle.python.dataflow.new.RemoteFlowSources import semmle.python.dataflow.new.DataFlow import semmle.python.dataflow.new.TaintTracking -import semmle.python.dataflow.new.internal.TaintTrackingPublic import DataFlow::PathGraph // LDAP2 diff --git a/python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll b/python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll new file mode 100644 index 000000000000..fdf4a7dc9327 --- /dev/null +++ b/python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll @@ -0,0 +1,2 @@ + +// From db1f54a5f34d42139552eb29f60e6dde644f2138 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 8 Apr 2021 00:19:00 +0200 Subject: [PATCH 05/29] Polish query file --- .../Security/CWE-287/LDAPImproperAuth.ql | 78 ++----------------- 1 file changed, 7 insertions(+), 71 deletions(-) diff --git a/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.ql b/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.ql index 029dfb313a7b..72b801bbb3d5 100644 --- a/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.ql +++ b/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.ql @@ -1,9 +1,9 @@ /** * @name Python LDAP Improper Authentication - * @description Check if a user-controlled query carry no authentication + * @description Check if a user-controlled query carries no authentication * @kind path-problem * @problem.severity warning - * @id python/ldap-improper-auth + * @id python/ldap-improper-authentication * @tags experimental * security * external/cwe/cwe-287 @@ -11,75 +11,11 @@ // Determine precision above import python -import semmle.python.dataflow.new.RemoteFlowSources -import semmle.python.dataflow.new.DataFlow -import semmle.python.dataflow.new.TaintTracking +import experimental.semmle.python.security.LDAPImproperAuth import DataFlow::PathGraph -// LDAP2 -class BindSink extends DataFlow::Node { - BindSink() { - exists(SsaVariable bindVar, CallNode bindCall, CallNode searchCall | - // get variable initializing the connection - bindVar.getDefinition().getImmediateDominator() = Value::named("ldap.initialize").getACall() and - // get a call using that variable - bindVar.getAUse().getImmediateDominator() = bindCall and - // restrict call to any bind method - bindCall.getNode().getFunc().(Attribute).getName().matches("%bind%") and - ( - // check second argument (password) - bindCall.getArg(1).getNode() instanceof None or - count(bindCall.getAnArg()) = 1 - ) and - // get another call using that variable - bindVar.getAUse().getNode() = searchCall.getNode().getFunc().(Attribute).getObject() and - // restrict call to any search method - searchCall.getNode().getFunc().(Attribute).getName().matches("%search%") and - // set the third argument as sink - this.asExpr() = searchCall.getArg(2).getNode() - ) - } -} - -// LDAP3 -class ConnectionSink extends DataFlow::Node { - ConnectionSink() { - exists(SsaVariable connectionVar, CallNode connectionCall, CallNode searchCall | - // get call initializing the connection - connectionCall = Value::named("ldap3.Connection").getACall() and - ( - // check password argument - not exists(connectionCall.getArgByName("password")) or - connectionCall.getArgByName("password").pointsTo(Value::named("None")) - ) and - // get the variable initializing the connection - connectionVar.getDefinition().getImmediateDominator() = connectionCall and - // get a call using that variable - connectionVar.getAUse().getNode() = searchCall.getNode().getFunc().(Attribute).getObject() and - // restrict call to any search method - searchCall.getNode().getFunc().(Attribute).getName().matches("%search%") and - // set the second argument as sink - this.asExpr() = searchCall.getArg(1).getNode() - ) - } -} - -class LDAPImproperAuthSink extends DataFlow::Node { - LDAPImproperAuthSink() { - this instanceof BindSink or - this instanceof ConnectionSink - } -} - -class LDAPImproperAuthConfig extends TaintTracking::Configuration { - LDAPImproperAuthConfig() { this = "LDAPImproperAuthConfig" } - - override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } - - override predicate isSink(DataFlow::Node sink) { sink instanceof LDAPImproperAuthSink } -} - -from LDAPImproperAuthConfig config, DataFlow::PathNode source, DataFlow::PathNode sink +from LDAPImproperAuthenticationConfig config, DataFlow::PathNode source, DataFlow::PathNode sink where config.hasFlowPath(source, sink) -select sink.getNode(), source, sink, "$@ LDAP query executes $@.", sink.getNode(), "This", - source.getNode(), "a user-provided value without authentication." +select sink.getNode(), source, sink, + "$@ LDAP query parameter contains $@ and is executed without authentication.", sink.getNode(), + "This", source.getNode(), "a user-provided value" From aa7763b3d2106a70fd378c35a04613600a8b0e50 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 8 Apr 2021 00:19:14 +0200 Subject: [PATCH 06/29] Set up Concepts --- .../experimental/semmle/python/Concepts.qll | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll index 904b7967ee87..6a3b60cfaa10 100644 --- a/python/ql/src/experimental/semmle/python/Concepts.qll +++ b/python/ql/src/experimental/semmle/python/Concepts.qll @@ -13,3 +13,21 @@ private import semmle.python.dataflow.new.DataFlow private import semmle.python.dataflow.new.RemoteFlowSources private import semmle.python.dataflow.new.TaintTracking private import experimental.semmle.python.Frameworks + +module LDAPBind { + abstract class Range extends DataFlow::Node { + abstract DataFlow::Node getPasswordNode(); + + abstract DataFlow::Node getQueryNode(); + } +} + +class LDAPBind extends DataFlow::Node { + LDAPBind::Range range; + + LDAPBind() { this = range } + + DataFlow::Node getPasswordNode() { result = range.getPasswordNode() } + + DataFlow::Node getQueryNode() { result = range.getQueryNode() } +} From 8ca6e84268eba430a909f389bf2e52ac18aad772 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 8 Apr 2021 00:19:46 +0200 Subject: [PATCH 07/29] Refactor Calls to use ApiGraphs --- .../semmle/python/frameworks/Stdlib.qll | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll index 420caf0d73bb..5860151e070c 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll @@ -9,3 +9,60 @@ 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 + +private module LDAP { + private module LDAP2 { + private class LDAP2QueryMethods extends string { + LDAP2QueryMethods() { + this in ["search", "search_s", "search_st", "search_ext", "search_ext_s"] + } + } + + class LDAP2Bind extends DataFlow::CallCfgNode, LDAPBind::Range { + DataFlow::Node queryNode; + + LDAP2Bind() { + exists( + DataFlow::AttrRead bindMethod, DataFlow::CallCfgNode searchCall, + DataFlow::AttrRead searchMethod + | + this.getFunction() = bindMethod and + API::moduleImport("ldap").getMember("initialize").getACall() = + bindMethod.getObject().getALocalSource() and + bindMethod.getAttributeName().matches("%bind%") and + searchCall.getFunction() = searchMethod and + bindMethod.getObject().getALocalSource() = searchMethod.getObject().getALocalSource() and + searchMethod.getAttributeName() instanceof LDAP2QueryMethods and + ( + queryNode = searchCall.getArg(2) or + queryNode = searchCall.getArgByName("filterstr") + ) + ) + } + + override DataFlow::Node getPasswordNode() { result = this.getArg(1) } + + override DataFlow::Node getQueryNode() { result = queryNode } + } + } + + private module LDAP3 { + class LDAP3Bind extends DataFlow::CallCfgNode, LDAPBind::Range { + DataFlow::Node queryNode; + + LDAP3Bind() { + exists(DataFlow::CallCfgNode searchCall, DataFlow::AttrRead searchMethod | + this = API::moduleImport("ldap3").getMember("Connection").getACall() and + searchMethod.getObject().getALocalSource() = this and + searchCall.getFunction() = searchMethod and + searchMethod.getAttributeName() = "search" and + queryNode = searchCall.getArg(1) + ) + } + + override DataFlow::Node getPasswordNode() { result = this.getArgByName("password") } + + override DataFlow::Node getQueryNode() { result = queryNode } + } + } +} From 7e456494ef9fa1ff9d5215b58ce2ef3eaf6e24b0 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 8 Apr 2021 00:20:04 +0200 Subject: [PATCH 08/29] Set up taint config and custom sink --- .../python/security/LDAPImproperAuth.qll | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll b/python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll index fdf4a7dc9327..f481cf75831c 100644 --- a/python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll +++ b/python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll @@ -1,2 +1,25 @@ +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 -// +class LDAPImproperAuthSink extends DataFlow::Node { + LDAPImproperAuthSink() { + exists(LDAPBind ldapBind | + ( + DataFlow::localFlow(DataFlow::exprNode(any(None noneName)), ldapBind.getPasswordNode()) or + not exists(ldapBind.getPasswordNode()) + ) and + this = ldapBind.getQueryNode() + ) + } +} + +class LDAPImproperAuthenticationConfig extends TaintTracking::Configuration { + LDAPImproperAuthenticationConfig() { this = "LDAPImproperAuthenticationConfig" } + + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { sink instanceof LDAPImproperAuthSink } +} From 63bd32359a8bde41c67657d5b754e1cdc7fdd640 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Fri, 9 Apr 2021 00:48:57 +0200 Subject: [PATCH 09/29] Improve qhelp --- .../Security/CWE-287/ImproperLdapAuth.qhelp | 32 +++++++++++++++++++ .../Security/CWE-287/LDAPImproperAuth.qhelp | 28 ---------------- 2 files changed, 32 insertions(+), 28 deletions(-) create mode 100644 python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.qhelp delete mode 100644 python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.qhelp diff --git a/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.qhelp b/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.qhelp new file mode 100644 index 000000000000..43f47927fbad --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.qhelp @@ -0,0 +1,32 @@ + + + +

    If an LDAP query is built using string concatenation or string formatting, and it doesn't carry any kind of authentication, +anonymous binds causes an empty or None-set password to result in a successful authentication.

    +
    + + +

    Use a strong password while establishing a LDAP connection to execute a query a user controls.

    +
    + + +

    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 while the connection carries no authentication or binds anonymously.

    + + + + +

    In the third and fourth examples, the authentication is established using a password from a secure source such as environment variables.

    + + + +
    + + +
  • SonarSource: RSPEC-4433.
  • +
  • Python2: LDAP Documentation.
  • +
  • Python3: LDAP Documentation.
  • + +
    diff --git a/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.qhelp b/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.qhelp deleted file mode 100644 index 64fb94c1c9bf..000000000000 --- a/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.qhelp +++ /dev/null @@ -1,28 +0,0 @@ - - - - - -

    If an LDAP connection doesn't carry any kind of authentication, gets produced an access to information - in the LDAP directory.

    - -

    Simple authentication in LDAP can be used with three different mechanisms:

    - -

  • Anonymous Authentication Mechanism by performing a bind request with a username and password value of zero length.
  • -
  • Unauthenticated Authentication Mechanism by performing a bind request with a password value of zero length.
  • -
  • Name/Password Authentication Mechanism by performing a bind request with a password value of non-zero length.
  • - -
    - - -

    Every LDAP authentication should be done by using a password taken from a safe place. - - - -

  • - SonarSource - RSPEC-4433 -
  • - - -
    \ No newline at end of file From 20fc5db49ec613fc03835a185175c0beb2050eea Mon Sep 17 00:00:00 2001 From: jorgectf Date: Fri, 9 Apr 2021 00:49:30 +0200 Subject: [PATCH 10/29] Polish query file --- .../CWE-287/{LDAPImproperAuth.ql => ImproperLdapAuth.ql} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename python/ql/src/experimental/Security/CWE-287/{LDAPImproperAuth.ql => ImproperLdapAuth.ql} (78%) diff --git a/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.ql b/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.ql similarity index 78% rename from python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.ql rename to python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.ql index 72b801bbb3d5..f5e92a2ee6d2 100644 --- a/python/ql/src/experimental/Security/CWE-287/LDAPImproperAuth.ql +++ b/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.ql @@ -1,9 +1,9 @@ /** - * @name Python LDAP Improper Authentication - * @description Check if a user-controlled query carries no authentication + * @name Improper LDAP Authentication + * @description A user-controlled query carries no authentication * @kind path-problem * @problem.severity warning - * @id python/ldap-improper-authentication + * @id py/improper-ldap-auth * @tags experimental * security * external/cwe/cwe-287 From 2392be08c797f1ed49b39a73fdb39d0062bd0c9a Mon Sep 17 00:00:00 2001 From: jorgectf Date: Fri, 9 Apr 2021 00:50:04 +0200 Subject: [PATCH 11/29] Improve sink --- .../semmle/python/security/LDAPImproperAuth.qll | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll b/python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll index f481cf75831c..dcfd49221c52 100644 --- a/python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll +++ b/python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll @@ -8,8 +8,15 @@ class LDAPImproperAuthSink extends DataFlow::Node { LDAPImproperAuthSink() { exists(LDAPBind ldapBind | ( - DataFlow::localFlow(DataFlow::exprNode(any(None noneName)), ldapBind.getPasswordNode()) or - not exists(ldapBind.getPasswordNode()) + ( + DataFlow::localFlow(DataFlow::exprNode(any(None noneName)), ldapBind.getPasswordNode()) or + not exists(ldapBind.getPasswordNode()) + ) + or + exists(StrConst emptyString | + emptyString.getText() = "" and + DataFlow::localFlow(DataFlow::exprNode(emptyString), ldapBind.getPasswordNode()) + ) ) and this = ldapBind.getQueryNode() ) From 015d203fcb610988ed715b929338a54b878de252 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Fri, 9 Apr 2021 00:50:47 +0200 Subject: [PATCH 12/29] Improve tests, move them and create qhelp examples --- .../Security/CWE-287/examples/auth_bad_2.py | 12 ++++++ .../Security/CWE-287/examples/auth_bad_3.py | 12 ++++++ .../Security/CWE-287/examples/auth_good_2.py | 13 ++++++ .../auth_good_3.py} | 6 --- .../Security/CWE-287/auth_bad_2.py} | 42 +++++++++++++------ .../Security/CWE-287/auth_bad_3.py} | 27 +++++++++++- .../Security/CWE-287/auth_good_2.py} | 40 ++++++++++++------ .../Security/CWE-287/auth_good_3.py | 24 +++++++++++ 8 files changed, 143 insertions(+), 33 deletions(-) create mode 100644 python/ql/src/experimental/Security/CWE-287/examples/auth_bad_2.py create mode 100644 python/ql/src/experimental/Security/CWE-287/examples/auth_bad_3.py create mode 100644 python/ql/src/experimental/Security/CWE-287/examples/auth_good_2.py rename python/ql/src/experimental/Security/CWE-287/{tests/3_auth_good.py => examples/auth_good_3.py} (81%) rename python/ql/{src/experimental/Security/CWE-287/tests/auth_bad.py => test/experimental/query-tests/Security/CWE-287/auth_bad_2.py} (59%) rename python/ql/{src/experimental/Security/CWE-287/tests/3_auth_bad.py => test/experimental/query-tests/Security/CWE-287/auth_bad_3.py} (51%) rename python/ql/{src/experimental/Security/CWE-287/tests/auth_good.py => test/experimental/query-tests/Security/CWE-287/auth_good_2.py} (62%) create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-287/auth_good_3.py diff --git a/python/ql/src/experimental/Security/CWE-287/examples/auth_bad_2.py b/python/ql/src/experimental/Security/CWE-287/examples/auth_bad_2.py new file mode 100644 index 000000000000..c893016ee343 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-287/examples/auth_bad_2.py @@ -0,0 +1,12 @@ +from flask import request, Flask +import ldap + + +@app.route("/bind_example") +def bind_example(): + dn = request.args['dc'] + search_filter = 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) diff --git a/python/ql/src/experimental/Security/CWE-287/examples/auth_bad_3.py b/python/ql/src/experimental/Security/CWE-287/examples/auth_bad_3.py new file mode 100644 index 000000000000..2cce5b73bd5b --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-287/examples/auth_bad_3.py @@ -0,0 +1,12 @@ +from ldap3 import Server, Connection, ALL +from flask import request, Flask + + +@app.route("/passwordNone") +def passwordNone(): + dn = request.args['dc'] + search_filter = 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) diff --git a/python/ql/src/experimental/Security/CWE-287/examples/auth_good_2.py b/python/ql/src/experimental/Security/CWE-287/examples/auth_good_2.py new file mode 100644 index 000000000000..e5fc88f56d1c --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-287/examples/auth_good_2.py @@ -0,0 +1,13 @@ +from flask import request, Flask +import ldap +import os + + +@app.route("/bind_example") +def bind_example(): + dn = request.args['dc'] + search_filter = request.args['search'] + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.bind('cn=root', os.environ.get('LDAP_PASSWORD')) + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) diff --git a/python/ql/src/experimental/Security/CWE-287/tests/3_auth_good.py b/python/ql/src/experimental/Security/CWE-287/examples/auth_good_3.py similarity index 81% rename from python/ql/src/experimental/Security/CWE-287/tests/3_auth_good.py rename to python/ql/src/experimental/Security/CWE-287/examples/auth_good_3.py index 9c45ad58e349..ae554fa9f7d2 100644 --- a/python/ql/src/experimental/Security/CWE-287/tests/3_auth_good.py +++ b/python/ql/src/experimental/Security/CWE-287/examples/auth_good_3.py @@ -2,8 +2,6 @@ from flask import request, Flask import os -app = Flask(__name__) - @app.route("/passwordFromEnv") def passwordFromEnv(): @@ -14,7 +12,3 @@ def passwordFromEnv(): conn = Connection(srv, user='user_dn', password=os.environ.get('LDAP_PASSWORD')) status, result, response, _ = conn.search(dn, search_filter) - return result - -# if __name__ == "__main__": -# app.run(debug=True) diff --git a/python/ql/src/experimental/Security/CWE-287/tests/auth_bad.py b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_2.py similarity index 59% rename from python/ql/src/experimental/Security/CWE-287/tests/auth_bad.py rename to python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_2.py index 36323e71ee3d..cc64623015e3 100644 --- a/python/ql/src/experimental/Security/CWE-287/tests/auth_bad.py +++ b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_2.py @@ -4,48 +4,64 @@ app = Flask(__name__) -@app.route("/simple_bind") -def simple_bind(): +@app.route("/simple_bind_example") +def simple_bind_example(): + """ + A RemoteFlowSource is used directly as DN and search filter while the bind's password + is not set + """ + dn = request.args['dc'] search_filter = request.args['search'] ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") ldap_connection.simple_bind('cn=root') user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) - return user[0] -@app.route("/simple_bind_s") -def simple_bind_s(): +@app.route("/simple_bind_s_example") +def simple_bind_s_example(): + """ + A RemoteFlowSource is used directly as DN and search filter while the bind's password + is not set + """ + dn = request.args['dc'] search_filter = request.args['search'] ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") ldap_connection.simple_bind_s('cn=root') user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) - return user[0] -@app.route("/bind_s") -def bind_s(): +@app.route("/bind_s_example") +def bind_s_example(): + """ + A RemoteFlowSource is used directly as DN and search filter while the bind's password + is set to None + """ + dn = request.args['dc'] search_filter = request.args['search'] ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") ldap_connection.bind_s('cn=root', None) user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) - return user[0] -@app.route("/bind") -def bind(): +@app.route("/bind_example") +def bind_example(): + """ + A RemoteFlowSource is used directly as DN and search filter while the bind's password + is set to None + """ + dn = request.args['dc'] search_filter = request.args['search'] ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") - ldap_connection.bind('cn=root', None) + ldap_connection.bind('cn=root', "") user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) - return user[0] # if __name__ == "__main__": diff --git a/python/ql/src/experimental/Security/CWE-287/tests/3_auth_bad.py b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_3.py similarity index 51% rename from python/ql/src/experimental/Security/CWE-287/tests/3_auth_bad.py rename to python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_3.py index 5726ade823cf..b69722bf589e 100644 --- a/python/ql/src/experimental/Security/CWE-287/tests/3_auth_bad.py +++ b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_3.py @@ -6,24 +6,47 @@ @app.route("/passwordNone") def passwordNone(): + """ + A RemoteFlowSource is used directly as DN and search filter while the connection's password + is set to None + """ + dn = request.args['dc'] search_filter = 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) - return result + + +@app.route("/passwordEmpty") +def passwordEmpty(): + """ + A RemoteFlowSource is used directly as DN and search filter while the connection's password + is empty + """ + + dn = request.args['dc'] + search_filter = request.args['search'] + + srv = Server('servername', get_info=ALL) + conn = Connection(srv, user='user_dn', password="") + status, result, response, _ = conn.search(dn, search_filter) @app.route("/notPassword") def notPassword(): + """ + A RemoteFlowSource is used directly as DN and search filter while the connection's password + is not set + """ + dn = request.args['dc'] search_filter = request.args['search'] srv = Server('servername', get_info=ALL) conn = Connection(srv, user='user_dn') status, result, response, _ = conn.search(dn, search_filter) - return result # if __name__ == "__main__": diff --git a/python/ql/src/experimental/Security/CWE-287/tests/auth_good.py b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_good_2.py similarity index 62% rename from python/ql/src/experimental/Security/CWE-287/tests/auth_good.py rename to python/ql/test/experimental/query-tests/Security/CWE-287/auth_good_2.py index 884bcb7af264..925f80a3e9d9 100644 --- a/python/ql/src/experimental/Security/CWE-287/tests/auth_good.py +++ b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_good_2.py @@ -5,48 +5,64 @@ app = Flask(__name__) -@app.route("/simple_bind") -def simple_bind(): +@app.route("/simple_bind_example") +def simple_bind_example(): + """ + A RemoteFlowSource is used directly as DN and search filter while the bind's password + is an environment variable + """ + dn = request.args['dc'] search_filter = request.args['search'] ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") ldap_connection.simple_bind('cn=root', os.environ.get('LDAP_PASSWORD')) user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) - return user[0] -@app.route("/simple_bind_s") -def simple_bind_s(): +@app.route("/simple_bind_s_example") +def simple_bind_s_example(): + """ + A RemoteFlowSource is used directly as DN and search filter while the bind's password + is an environment variable + """ + dn = request.args['dc'] search_filter = request.args['search'] ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") ldap_connection.simple_bind_s('cn=root', os.environ.get('LDAP_PASSWORD')) user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) - return user[0] -@app.route("/bind_s") -def bind_s(): +@app.route("/bind_s_example") +def bind_s_example(): + """ + A RemoteFlowSource is used directly as DN and search filter while the bind's password + is an environment variable + """ + dn = request.args['dc'] search_filter = request.args['search'] ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") ldap_connection.bind_s('cn=root', os.environ.get('LDAP_PASSWORD')) user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) - return user[0] -@app.route("/bind") -def bind(): +@app.route("/bind_example") +def bind_example(): + """ + A RemoteFlowSource is used directly as DN and search filter while the bind's password + is an environment variable + """ + dn = request.args['dc'] search_filter = request.args['search'] ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") ldap_connection.bind('cn=root', os.environ.get('LDAP_PASSWORD')) user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) - return user[0] # if __name__ == "__main__": # app.run(debug=True) diff --git a/python/ql/test/experimental/query-tests/Security/CWE-287/auth_good_3.py b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_good_3.py new file mode 100644 index 000000000000..fd9174c14286 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_good_3.py @@ -0,0 +1,24 @@ +from ldap3 import Server, Connection, ALL +from flask import request, Flask +import os + +app = Flask(__name__) + + +@app.route("/passwordFromEnv") +def passwordFromEnv(): + """ + A RemoteFlowSource is used directly as DN and search filter while the connection's password + is an environment variable + """ + + dn = request.args['dc'] + search_filter = request.args['search'] + + srv = Server('servername', get_info=ALL) + conn = Connection(srv, user='user_dn', + password=os.environ.get('LDAP_PASSWORD')) + status, result, response, _ = conn.search(dn, search_filter) + +# if __name__ == "__main__": +# app.run(debug=True) From 1320eeee53cf332b5ee9d9ad21f1291b9bd011ad Mon Sep 17 00:00:00 2001 From: jorgectf Date: Fri, 9 Apr 2021 00:51:15 +0200 Subject: [PATCH 13/29] Add qlref --- .../query-tests/Security/CWE-287/ImproperLdapAuth.qlref | 1 + 1 file changed, 1 insertion(+) create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.qlref diff --git a/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.qlref b/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.qlref new file mode 100644 index 000000000000..9f5c6e4c43f4 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.qlref @@ -0,0 +1 @@ +experimental/Security/CWE-287/ImproperLdapAuth.ql From 5787406a0d3f48329ff139af6f09231dde019573 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Fri, 9 Apr 2021 00:51:26 +0200 Subject: [PATCH 14/29] Add .expected --- .../CWE-287/ImproperLdapAuth.expected | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected diff --git a/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected b/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected new file mode 100644 index 000000000000..1712533de774 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected @@ -0,0 +1,87 @@ +edges +| auth_bad_2.py:14:10:14:16 | ControlFlowNode for request | auth_bad_2.py:15:21:15:27 | ControlFlowNode for request | +| auth_bad_2.py:14:10:14:16 | ControlFlowNode for request | auth_bad_2.py:15:21:15:32 | ControlFlowNode for Attribute | +| auth_bad_2.py:15:21:15:27 | ControlFlowNode for request | auth_bad_2.py:15:21:15:32 | ControlFlowNode for Attribute | +| auth_bad_2.py:15:21:15:32 | ControlFlowNode for Attribute | auth_bad_2.py:15:21:15:42 | ControlFlowNode for Subscript | +| auth_bad_2.py:15:21:15:42 | ControlFlowNode for Subscript | auth_bad_2.py:19:61:19:73 | ControlFlowNode for search_filter | +| auth_bad_2.py:29:10:29:16 | ControlFlowNode for request | auth_bad_2.py:30:21:30:27 | ControlFlowNode for request | +| auth_bad_2.py:29:10:29:16 | ControlFlowNode for request | auth_bad_2.py:30:21:30:32 | ControlFlowNode for Attribute | +| auth_bad_2.py:30:21:30:27 | ControlFlowNode for request | auth_bad_2.py:30:21:30:32 | ControlFlowNode for Attribute | +| auth_bad_2.py:30:21:30:32 | ControlFlowNode for Attribute | auth_bad_2.py:30:21:30:42 | ControlFlowNode for Subscript | +| auth_bad_2.py:30:21:30:42 | ControlFlowNode for Subscript | auth_bad_2.py:34:61:34:73 | ControlFlowNode for search_filter | +| auth_bad_2.py:44:10:44:16 | ControlFlowNode for request | auth_bad_2.py:45:21:45:27 | ControlFlowNode for request | +| auth_bad_2.py:44:10:44:16 | ControlFlowNode for request | auth_bad_2.py:45:21:45:32 | ControlFlowNode for Attribute | +| auth_bad_2.py:45:21:45:27 | ControlFlowNode for request | auth_bad_2.py:45:21:45:32 | ControlFlowNode for Attribute | +| auth_bad_2.py:45:21:45:32 | ControlFlowNode for Attribute | auth_bad_2.py:45:21:45:42 | ControlFlowNode for Subscript | +| auth_bad_2.py:45:21:45:42 | ControlFlowNode for Subscript | auth_bad_2.py:49:61:49:73 | ControlFlowNode for search_filter | +| auth_bad_2.py:59:10:59:16 | ControlFlowNode for request | auth_bad_2.py:60:21:60:27 | ControlFlowNode for request | +| auth_bad_2.py:59:10:59:16 | ControlFlowNode for request | auth_bad_2.py:60:21:60:32 | ControlFlowNode for Attribute | +| auth_bad_2.py:60:21:60:27 | ControlFlowNode for request | auth_bad_2.py:60:21:60:32 | ControlFlowNode for Attribute | +| auth_bad_2.py:60:21:60:32 | ControlFlowNode for Attribute | auth_bad_2.py:60:21:60:42 | ControlFlowNode for Subscript | +| auth_bad_2.py:60:21:60:42 | ControlFlowNode for Subscript | auth_bad_2.py:64:61:64:73 | ControlFlowNode for search_filter | +| auth_bad_3.py:14:10:14:16 | ControlFlowNode for request | auth_bad_3.py:15:21:15:27 | ControlFlowNode for request | +| auth_bad_3.py:14:10:14:16 | ControlFlowNode for request | auth_bad_3.py:15:21:15:32 | ControlFlowNode for Attribute | +| auth_bad_3.py:15:21:15:27 | ControlFlowNode for request | auth_bad_3.py:15:21:15:32 | ControlFlowNode for Attribute | +| auth_bad_3.py:15:21:15:32 | ControlFlowNode for Attribute | auth_bad_3.py:15:21:15:42 | ControlFlowNode for Subscript | +| auth_bad_3.py:15:21:15:42 | ControlFlowNode for Subscript | auth_bad_3.py:19:51:19:63 | ControlFlowNode for search_filter | +| auth_bad_3.py:29:10:29:16 | ControlFlowNode for request | auth_bad_3.py:30:21:30:27 | ControlFlowNode for request | +| auth_bad_3.py:29:10:29:16 | ControlFlowNode for request | auth_bad_3.py:30:21:30:32 | ControlFlowNode for Attribute | +| auth_bad_3.py:30:21:30:27 | ControlFlowNode for request | auth_bad_3.py:30:21:30:32 | ControlFlowNode for Attribute | +| auth_bad_3.py:30:21:30:32 | ControlFlowNode for Attribute | auth_bad_3.py:30:21:30:42 | ControlFlowNode for Subscript | +| auth_bad_3.py:30:21:30:42 | ControlFlowNode for Subscript | auth_bad_3.py:34:51:34:63 | ControlFlowNode for search_filter | +| auth_bad_3.py:44:10:44:16 | ControlFlowNode for request | auth_bad_3.py:45:21:45:27 | ControlFlowNode for request | +| auth_bad_3.py:44:10:44:16 | ControlFlowNode for request | auth_bad_3.py:45:21:45:32 | ControlFlowNode for Attribute | +| auth_bad_3.py:45:21:45:27 | ControlFlowNode for request | auth_bad_3.py:45:21:45:32 | ControlFlowNode for Attribute | +| auth_bad_3.py:45:21:45:32 | ControlFlowNode for Attribute | auth_bad_3.py:45:21:45:42 | ControlFlowNode for Subscript | +| auth_bad_3.py:45:21:45:42 | ControlFlowNode for Subscript | auth_bad_3.py:49:51:49:63 | ControlFlowNode for search_filter | +nodes +| auth_bad_2.py:14:10:14:16 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| auth_bad_2.py:15:21:15:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| auth_bad_2.py:15:21:15:32 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| auth_bad_2.py:15:21:15:42 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| auth_bad_2.py:19:61:19:73 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | +| auth_bad_2.py:29:10:29:16 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| auth_bad_2.py:30:21:30:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| auth_bad_2.py:30:21:30:32 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| auth_bad_2.py:30:21:30:42 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| auth_bad_2.py:34:61:34:73 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | +| auth_bad_2.py:44:10:44:16 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| auth_bad_2.py:45:21:45:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| auth_bad_2.py:45:21:45:32 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| auth_bad_2.py:45:21:45:42 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| auth_bad_2.py:49:61:49:73 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | +| auth_bad_2.py:59:10:59:16 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| auth_bad_2.py:60:21:60:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| auth_bad_2.py:60:21:60:32 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| auth_bad_2.py:60:21:60:42 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| auth_bad_2.py:64:61:64:73 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | +| auth_bad_3.py:14:10:14:16 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| auth_bad_3.py:15:21:15:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| auth_bad_3.py:15:21:15:32 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| auth_bad_3.py:15:21:15:42 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| auth_bad_3.py:19:51:19:63 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | +| auth_bad_3.py:29:10:29:16 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| auth_bad_3.py:30:21:30:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| auth_bad_3.py:30:21:30:32 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| auth_bad_3.py:30:21:30:42 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| auth_bad_3.py:34:51:34:63 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | +| auth_bad_3.py:44:10:44:16 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| auth_bad_3.py:45:21:45:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| auth_bad_3.py:45:21:45:32 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| auth_bad_3.py:45:21:45:42 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| auth_bad_3.py:49:51:49:63 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | +#select +| auth_bad_2.py:19:61:19:73 | ControlFlowNode for search_filter | auth_bad_2.py:14:10:14:16 | ControlFlowNode for request | auth_bad_2.py:19:61:19:73 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_2.py:19:61:19:73 | ControlFlowNode for search_filter | This | auth_bad_2.py:14:10:14:16 | ControlFlowNode for request | a user-provided value | +| auth_bad_2.py:19:61:19:73 | ControlFlowNode for search_filter | auth_bad_2.py:15:21:15:27 | ControlFlowNode for request | auth_bad_2.py:19:61:19:73 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_2.py:19:61:19:73 | ControlFlowNode for search_filter | This | auth_bad_2.py:15:21:15:27 | ControlFlowNode for request | a user-provided value | +| auth_bad_2.py:34:61:34:73 | ControlFlowNode for search_filter | auth_bad_2.py:29:10:29:16 | ControlFlowNode for request | auth_bad_2.py:34:61:34:73 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_2.py:34:61:34:73 | ControlFlowNode for search_filter | This | auth_bad_2.py:29:10:29:16 | ControlFlowNode for request | a user-provided value | +| auth_bad_2.py:34:61:34:73 | ControlFlowNode for search_filter | auth_bad_2.py:30:21:30:27 | ControlFlowNode for request | auth_bad_2.py:34:61:34:73 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_2.py:34:61:34:73 | ControlFlowNode for search_filter | This | auth_bad_2.py:30:21:30:27 | ControlFlowNode for request | a user-provided value | +| auth_bad_2.py:49:61:49:73 | ControlFlowNode for search_filter | auth_bad_2.py:44:10:44:16 | ControlFlowNode for request | auth_bad_2.py:49:61:49:73 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_2.py:49:61:49:73 | ControlFlowNode for search_filter | This | auth_bad_2.py:44:10:44:16 | ControlFlowNode for request | a user-provided value | +| auth_bad_2.py:49:61:49:73 | ControlFlowNode for search_filter | auth_bad_2.py:45:21:45:27 | ControlFlowNode for request | auth_bad_2.py:49:61:49:73 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_2.py:49:61:49:73 | ControlFlowNode for search_filter | This | auth_bad_2.py:45:21:45:27 | ControlFlowNode for request | a user-provided value | +| auth_bad_2.py:64:61:64:73 | ControlFlowNode for search_filter | auth_bad_2.py:59:10:59:16 | ControlFlowNode for request | auth_bad_2.py:64:61:64:73 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_2.py:64:61:64:73 | ControlFlowNode for search_filter | This | auth_bad_2.py:59:10:59:16 | ControlFlowNode for request | a user-provided value | +| auth_bad_2.py:64:61:64:73 | ControlFlowNode for search_filter | auth_bad_2.py:60:21:60:27 | ControlFlowNode for request | auth_bad_2.py:64:61:64:73 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_2.py:64:61:64:73 | ControlFlowNode for search_filter | This | auth_bad_2.py:60:21:60:27 | ControlFlowNode for request | a user-provided value | +| auth_bad_3.py:19:51:19:63 | ControlFlowNode for search_filter | auth_bad_3.py:14:10:14:16 | ControlFlowNode for request | auth_bad_3.py:19:51:19:63 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_3.py:19:51:19:63 | ControlFlowNode for search_filter | This | auth_bad_3.py:14:10:14:16 | ControlFlowNode for request | a user-provided value | +| auth_bad_3.py:19:51:19:63 | ControlFlowNode for search_filter | auth_bad_3.py:15:21:15:27 | ControlFlowNode for request | auth_bad_3.py:19:51:19:63 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_3.py:19:51:19:63 | ControlFlowNode for search_filter | This | auth_bad_3.py:15:21:15:27 | ControlFlowNode for request | a user-provided value | +| auth_bad_3.py:34:51:34:63 | ControlFlowNode for search_filter | auth_bad_3.py:29:10:29:16 | ControlFlowNode for request | auth_bad_3.py:34:51:34:63 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_3.py:34:51:34:63 | ControlFlowNode for search_filter | This | auth_bad_3.py:29:10:29:16 | ControlFlowNode for request | a user-provided value | +| auth_bad_3.py:34:51:34:63 | ControlFlowNode for search_filter | auth_bad_3.py:30:21:30:27 | ControlFlowNode for request | auth_bad_3.py:34:51:34:63 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_3.py:34:51:34:63 | ControlFlowNode for search_filter | This | auth_bad_3.py:30:21:30:27 | ControlFlowNode for request | a user-provided value | +| auth_bad_3.py:49:51:49:63 | ControlFlowNode for search_filter | auth_bad_3.py:44:10:44:16 | ControlFlowNode for request | auth_bad_3.py:49:51:49:63 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_3.py:49:51:49:63 | ControlFlowNode for search_filter | This | auth_bad_3.py:44:10:44:16 | ControlFlowNode for request | a user-provided value | +| auth_bad_3.py:49:51:49:63 | ControlFlowNode for search_filter | auth_bad_3.py:45:21:45:27 | ControlFlowNode for request | auth_bad_3.py:49:51:49:63 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_3.py:49:51:49:63 | ControlFlowNode for search_filter | This | auth_bad_3.py:45:21:45:27 | ControlFlowNode for request | a user-provided value | From f140601241498e2b227d49b7637ec7dda9ea0e11 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Fri, 9 Apr 2021 01:57:23 +0200 Subject: [PATCH 15/29] Write documentation --- .../experimental/semmle/python/Concepts.qll | 19 ++++++++++++++ .../semmle/python/frameworks/Stdlib.qll | 26 +++++++++++++++++++ .../python/security/LDAPImproperAuth.qll | 10 +++++++ 3 files changed, 55 insertions(+) diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll index 6a3b60cfaa10..e86afca82ffb 100644 --- a/python/ql/src/experimental/semmle/python/Concepts.qll +++ b/python/ql/src/experimental/semmle/python/Concepts.qll @@ -14,14 +14,33 @@ private import semmle.python.dataflow.new.RemoteFlowSources private import semmle.python.dataflow.new.TaintTracking private import experimental.semmle.python.Frameworks +/** 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 getPasswordNode(); + /** + * Gets the argument containing the executed query. + */ abstract DataFlow::Node getQueryNode(); } } +/** + * 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; diff --git a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll index 5860151e070c..775dd7b2e3db 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll @@ -10,14 +10,32 @@ 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 Python's `ldap` library. + * + * 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 binding a connection. + * + * See `LDAP2QueryMethods` + */ class LDAP2Bind extends DataFlow::CallCfgNode, LDAPBind::Range { DataFlow::Node queryNode; @@ -46,7 +64,15 @@ private module LDAP { } } + /** + * Provides models for Python's `ldap3` library. + * + * See https://pypi.org/project/ldap3/ + */ private module LDAP3 { + /** + * A class to find `ldap3` methods binding a connection. + */ class LDAP3Bind extends DataFlow::CallCfgNode, LDAPBind::Range { DataFlow::Node queryNode; diff --git a/python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll b/python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll index dcfd49221c52..ef54d1612ac4 100644 --- a/python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll +++ b/python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll @@ -1,9 +1,16 @@ +/** + * Provides a taint-tracking configuration for detecting LDAP improper authentication 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 class to find `LDAPBind` methods using an empty password or set as None. + */ class LDAPImproperAuthSink extends DataFlow::Node { LDAPImproperAuthSink() { exists(LDAPBind ldapBind | @@ -23,6 +30,9 @@ class LDAPImproperAuthSink extends DataFlow::Node { } } +/** + * A taint-tracking configuration for detecting LDAP improper authentications. + */ class LDAPImproperAuthenticationConfig extends TaintTracking::Configuration { LDAPImproperAuthenticationConfig() { this = "LDAPImproperAuthenticationConfig" } From d34d2ed2b10e2580119d28fef0c10f27dfd20063 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 17 Jun 2021 17:42:38 +0200 Subject: [PATCH 16/29] Add .qlref --- .../Security/CWE-287/examples/ImproperLdapAuth.qlref | 1 + 1 file changed, 1 insertion(+) create mode 100644 python/ql/src/experimental/Security/CWE-287/examples/ImproperLdapAuth.qlref diff --git a/python/ql/src/experimental/Security/CWE-287/examples/ImproperLdapAuth.qlref b/python/ql/src/experimental/Security/CWE-287/examples/ImproperLdapAuth.qlref new file mode 100644 index 000000000000..9f5c6e4c43f4 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-287/examples/ImproperLdapAuth.qlref @@ -0,0 +1 @@ +experimental/Security/CWE-287/ImproperLdapAuth.ql From 13cfcec96812e90d1a402d981e6fa27acd28fc9a Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 17 Jun 2021 17:43:34 +0200 Subject: [PATCH 17/29] Change qhelp explanation --- .../experimental/Security/CWE-287/ImproperLdapAuth.qhelp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.qhelp b/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.qhelp index 43f47927fbad..7f4b018bdf2c 100644 --- a/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.qhelp +++ b/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.qhelp @@ -3,8 +3,8 @@ "qhelp.dtd"> -

    If an LDAP query is built using string concatenation or string formatting, and it doesn't carry any kind of authentication, -anonymous binds causes an empty or None-set password to result in a successful authentication.

    +

    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.

    @@ -12,8 +12,7 @@ anonymous binds causes an empty or None-set password to result in a successful a -

    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 while the connection carries no authentication or binds anonymously.

    +

    In the following examples, the code builds a LDAP query whose execution carries no authentication or binds anonymously.

    From 5704ac36dbf81eea33b41f3f2380bfa61149d97b Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 17 Jun 2021 17:44:08 +0200 Subject: [PATCH 18/29] Rework LDAP framework modeling --- .../semmle/python/frameworks/LDAP.qll | 124 +++++++++++------- 1 file changed, 74 insertions(+), 50 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll b/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll index 2843a695b189..5ba727675e86 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll @@ -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::LocalSourceNode 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. * @@ -30,32 +44,45 @@ 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")]] } + } - override DataFlow::Node getQuery() { result = ldapQuery } + /** Gets a reference to a `ldap` bind. */ + private DataFlow::Node ldapBind() { + result = ldapOperation() and + result.(DataFlow::AttrRead).getAttributeName().matches("%bind%") + } + + /** + * A class to find `ldap` methods binding a connection. + * + * See `LDAP2QueryMethods` + */ + private class LDAP2Bind extends DataFlow::CallCfgNode, LDAPBind::Range { + LDAP2Bind() { this.getFunction() = ldapBind() } + + override DataFlow::Node getPassword() { result = this.getArg(1) } } /** @@ -64,9 +91,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) } } @@ -78,8 +103,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) } @@ -92,26 +116,38 @@ 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 = this.getArgByName("password") } } /** @@ -120,14 +156,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) } } @@ -139,12 +168,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) } From 9cbb7e0899ef55a6d8c0f9d1dc3ab7cc42f72e06 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 17 Jun 2021 17:53:58 +0200 Subject: [PATCH 19/29] Change query objective --- .../Security/CWE-287/ImproperLdapAuth.ql | 11 +-- .../experimental/semmle/python/Concepts.qll | 11 +-- .../semmle/python/frameworks/Stdlib.qll | 83 ------------------- .../python/security/LDAPImproperAuth.qll | 41 +++------ 4 files changed, 16 insertions(+), 130 deletions(-) diff --git a/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.ql b/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.ql index f5e92a2ee6d2..27fc46bcdb24 100644 --- a/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.ql +++ b/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.ql @@ -1,7 +1,7 @@ /** * @name Improper LDAP Authentication * @description A user-controlled query carries no authentication - * @kind path-problem + * @kind problem * @problem.severity warning * @id py/improper-ldap-auth * @tags experimental @@ -12,10 +12,7 @@ // Determine precision above import python import experimental.semmle.python.security.LDAPImproperAuth -import DataFlow::PathGraph -from LDAPImproperAuthenticationConfig config, DataFlow::PathNode source, DataFlow::PathNode sink -where config.hasFlowPath(source, sink) -select sink.getNode(), source, sink, - "$@ LDAP query parameter contains $@ and is executed without authentication.", sink.getNode(), - "This", source.getNode(), "a user-provided value" +from LDAPBind ldapBind +where authenticatesImproperly(ldapBind) +select "The following LDAP bind operation is executed without authentication", ldapBind diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll index f98e6961fd47..42af8abd64af 100644 --- a/python/ql/src/experimental/semmle/python/Concepts.qll +++ b/python/ql/src/experimental/semmle/python/Concepts.qll @@ -159,12 +159,7 @@ module LDAPBind { /** * Gets the argument containing the binding expression. */ - abstract DataFlow::Node getPasswordNode(); - - /** - * Gets the argument containing the executed query. - */ - abstract DataFlow::Node getQueryNode(); + abstract DataFlow::Node getPassword(); } } @@ -179,7 +174,5 @@ class LDAPBind extends DataFlow::Node { LDAPBind() { this = range } - DataFlow::Node getPasswordNode() { result = range.getPasswordNode() } - - DataFlow::Node getQueryNode() { result = range.getQueryNode() } + DataFlow::Node getPassword() { result = range.getPassword() } } diff --git a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll index 980b948f7e50..4f3457e0a99b 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Stdlib.qll @@ -98,86 +98,3 @@ private module Re { override DataFlow::Node getRegexNode() { result = regexNode } } } - -/** - * Provides models for Python's ldap-related libraries. - */ -private module LDAP { - /** - * Provides models for Python's `ldap` library. - * - * 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 binding a connection. - * - * See `LDAP2QueryMethods` - */ - class LDAP2Bind extends DataFlow::CallCfgNode, LDAPBind::Range { - DataFlow::Node queryNode; - - LDAP2Bind() { - exists( - DataFlow::AttrRead bindMethod, DataFlow::CallCfgNode searchCall, - DataFlow::AttrRead searchMethod - | - this.getFunction() = bindMethod and - API::moduleImport("ldap").getMember("initialize").getACall() = - bindMethod.getObject().getALocalSource() and - bindMethod.getAttributeName().matches("%bind%") and - searchCall.getFunction() = searchMethod and - bindMethod.getObject().getALocalSource() = searchMethod.getObject().getALocalSource() and - searchMethod.getAttributeName() instanceof LDAP2QueryMethods and - ( - queryNode = searchCall.getArg(2) or - queryNode = searchCall.getArgByName("filterstr") - ) - ) - } - - override DataFlow::Node getPasswordNode() { result = this.getArg(1) } - - override DataFlow::Node getQueryNode() { result = queryNode } - } - } - - /** - * Provides models for Python's `ldap3` library. - * - * See https://pypi.org/project/ldap3/ - */ - private module LDAP3 { - /** - * A class to find `ldap3` methods binding a connection. - */ - class LDAP3Bind extends DataFlow::CallCfgNode, LDAPBind::Range { - DataFlow::Node queryNode; - - LDAP3Bind() { - exists(DataFlow::CallCfgNode searchCall, DataFlow::AttrRead searchMethod | - this = API::moduleImport("ldap3").getMember("Connection").getACall() and - searchMethod.getObject().getALocalSource() = this and - searchCall.getFunction() = searchMethod and - searchMethod.getAttributeName() = "search" and - queryNode = searchCall.getArg(1) - ) - } - - override DataFlow::Node getPasswordNode() { result = this.getArgByName("password") } - - override DataFlow::Node getQueryNode() { result = queryNode } - } - } -} diff --git a/python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll b/python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll index ef54d1612ac4..5d236f0d6a4d 100644 --- a/python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll +++ b/python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll @@ -8,35 +8,14 @@ import semmle.python.dataflow.new.DataFlow import semmle.python.dataflow.new.TaintTracking import semmle.python.dataflow.new.RemoteFlowSources -/** - * A class to find `LDAPBind` methods using an empty password or set as None. - */ -class LDAPImproperAuthSink extends DataFlow::Node { - LDAPImproperAuthSink() { - exists(LDAPBind ldapBind | - ( - ( - DataFlow::localFlow(DataFlow::exprNode(any(None noneName)), ldapBind.getPasswordNode()) or - not exists(ldapBind.getPasswordNode()) - ) - or - exists(StrConst emptyString | - emptyString.getText() = "" and - DataFlow::localFlow(DataFlow::exprNode(emptyString), ldapBind.getPasswordNode()) - ) - ) and - this = ldapBind.getQueryNode() - ) - } -} - -/** - * A taint-tracking configuration for detecting LDAP improper authentications. - */ -class LDAPImproperAuthenticationConfig extends TaintTracking::Configuration { - LDAPImproperAuthenticationConfig() { this = "LDAPImproperAuthenticationConfig" } - - override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } - - override predicate isSink(DataFlow::Node sink) { sink instanceof LDAPImproperAuthSink } +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 1d7ddce8db9ecec5dba4f00ffb0a6f33d223bb47 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 17 Jun 2021 18:10:43 +0200 Subject: [PATCH 20/29] Update .expected --- .../CWE-287/examples/ImproperLdapAuth.qlref | 1 - .../CWE-287/ImproperLdapAuth.expected | 94 ++----------------- 2 files changed, 7 insertions(+), 88 deletions(-) delete mode 100644 python/ql/src/experimental/Security/CWE-287/examples/ImproperLdapAuth.qlref diff --git a/python/ql/src/experimental/Security/CWE-287/examples/ImproperLdapAuth.qlref b/python/ql/src/experimental/Security/CWE-287/examples/ImproperLdapAuth.qlref deleted file mode 100644 index 9f5c6e4c43f4..000000000000 --- a/python/ql/src/experimental/Security/CWE-287/examples/ImproperLdapAuth.qlref +++ /dev/null @@ -1 +0,0 @@ -experimental/Security/CWE-287/ImproperLdapAuth.ql diff --git a/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected b/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected index 1712533de774..d3d868c16eee 100644 --- a/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected +++ b/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected @@ -1,87 +1,7 @@ -edges -| auth_bad_2.py:14:10:14:16 | ControlFlowNode for request | auth_bad_2.py:15:21:15:27 | ControlFlowNode for request | -| auth_bad_2.py:14:10:14:16 | ControlFlowNode for request | auth_bad_2.py:15:21:15:32 | ControlFlowNode for Attribute | -| auth_bad_2.py:15:21:15:27 | ControlFlowNode for request | auth_bad_2.py:15:21:15:32 | ControlFlowNode for Attribute | -| auth_bad_2.py:15:21:15:32 | ControlFlowNode for Attribute | auth_bad_2.py:15:21:15:42 | ControlFlowNode for Subscript | -| auth_bad_2.py:15:21:15:42 | ControlFlowNode for Subscript | auth_bad_2.py:19:61:19:73 | ControlFlowNode for search_filter | -| auth_bad_2.py:29:10:29:16 | ControlFlowNode for request | auth_bad_2.py:30:21:30:27 | ControlFlowNode for request | -| auth_bad_2.py:29:10:29:16 | ControlFlowNode for request | auth_bad_2.py:30:21:30:32 | ControlFlowNode for Attribute | -| auth_bad_2.py:30:21:30:27 | ControlFlowNode for request | auth_bad_2.py:30:21:30:32 | ControlFlowNode for Attribute | -| auth_bad_2.py:30:21:30:32 | ControlFlowNode for Attribute | auth_bad_2.py:30:21:30:42 | ControlFlowNode for Subscript | -| auth_bad_2.py:30:21:30:42 | ControlFlowNode for Subscript | auth_bad_2.py:34:61:34:73 | ControlFlowNode for search_filter | -| auth_bad_2.py:44:10:44:16 | ControlFlowNode for request | auth_bad_2.py:45:21:45:27 | ControlFlowNode for request | -| auth_bad_2.py:44:10:44:16 | ControlFlowNode for request | auth_bad_2.py:45:21:45:32 | ControlFlowNode for Attribute | -| auth_bad_2.py:45:21:45:27 | ControlFlowNode for request | auth_bad_2.py:45:21:45:32 | ControlFlowNode for Attribute | -| auth_bad_2.py:45:21:45:32 | ControlFlowNode for Attribute | auth_bad_2.py:45:21:45:42 | ControlFlowNode for Subscript | -| auth_bad_2.py:45:21:45:42 | ControlFlowNode for Subscript | auth_bad_2.py:49:61:49:73 | ControlFlowNode for search_filter | -| auth_bad_2.py:59:10:59:16 | ControlFlowNode for request | auth_bad_2.py:60:21:60:27 | ControlFlowNode for request | -| auth_bad_2.py:59:10:59:16 | ControlFlowNode for request | auth_bad_2.py:60:21:60:32 | ControlFlowNode for Attribute | -| auth_bad_2.py:60:21:60:27 | ControlFlowNode for request | auth_bad_2.py:60:21:60:32 | ControlFlowNode for Attribute | -| auth_bad_2.py:60:21:60:32 | ControlFlowNode for Attribute | auth_bad_2.py:60:21:60:42 | ControlFlowNode for Subscript | -| auth_bad_2.py:60:21:60:42 | ControlFlowNode for Subscript | auth_bad_2.py:64:61:64:73 | ControlFlowNode for search_filter | -| auth_bad_3.py:14:10:14:16 | ControlFlowNode for request | auth_bad_3.py:15:21:15:27 | ControlFlowNode for request | -| auth_bad_3.py:14:10:14:16 | ControlFlowNode for request | auth_bad_3.py:15:21:15:32 | ControlFlowNode for Attribute | -| auth_bad_3.py:15:21:15:27 | ControlFlowNode for request | auth_bad_3.py:15:21:15:32 | ControlFlowNode for Attribute | -| auth_bad_3.py:15:21:15:32 | ControlFlowNode for Attribute | auth_bad_3.py:15:21:15:42 | ControlFlowNode for Subscript | -| auth_bad_3.py:15:21:15:42 | ControlFlowNode for Subscript | auth_bad_3.py:19:51:19:63 | ControlFlowNode for search_filter | -| auth_bad_3.py:29:10:29:16 | ControlFlowNode for request | auth_bad_3.py:30:21:30:27 | ControlFlowNode for request | -| auth_bad_3.py:29:10:29:16 | ControlFlowNode for request | auth_bad_3.py:30:21:30:32 | ControlFlowNode for Attribute | -| auth_bad_3.py:30:21:30:27 | ControlFlowNode for request | auth_bad_3.py:30:21:30:32 | ControlFlowNode for Attribute | -| auth_bad_3.py:30:21:30:32 | ControlFlowNode for Attribute | auth_bad_3.py:30:21:30:42 | ControlFlowNode for Subscript | -| auth_bad_3.py:30:21:30:42 | ControlFlowNode for Subscript | auth_bad_3.py:34:51:34:63 | ControlFlowNode for search_filter | -| auth_bad_3.py:44:10:44:16 | ControlFlowNode for request | auth_bad_3.py:45:21:45:27 | ControlFlowNode for request | -| auth_bad_3.py:44:10:44:16 | ControlFlowNode for request | auth_bad_3.py:45:21:45:32 | ControlFlowNode for Attribute | -| auth_bad_3.py:45:21:45:27 | ControlFlowNode for request | auth_bad_3.py:45:21:45:32 | ControlFlowNode for Attribute | -| auth_bad_3.py:45:21:45:32 | ControlFlowNode for Attribute | auth_bad_3.py:45:21:45:42 | ControlFlowNode for Subscript | -| auth_bad_3.py:45:21:45:42 | ControlFlowNode for Subscript | auth_bad_3.py:49:51:49:63 | ControlFlowNode for search_filter | -nodes -| auth_bad_2.py:14:10:14:16 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| auth_bad_2.py:15:21:15:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| auth_bad_2.py:15:21:15:32 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| auth_bad_2.py:15:21:15:42 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| auth_bad_2.py:19:61:19:73 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | -| auth_bad_2.py:29:10:29:16 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| auth_bad_2.py:30:21:30:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| auth_bad_2.py:30:21:30:32 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| auth_bad_2.py:30:21:30:42 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| auth_bad_2.py:34:61:34:73 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | -| auth_bad_2.py:44:10:44:16 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| auth_bad_2.py:45:21:45:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| auth_bad_2.py:45:21:45:32 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| auth_bad_2.py:45:21:45:42 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| auth_bad_2.py:49:61:49:73 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | -| auth_bad_2.py:59:10:59:16 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| auth_bad_2.py:60:21:60:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| auth_bad_2.py:60:21:60:32 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| auth_bad_2.py:60:21:60:42 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| auth_bad_2.py:64:61:64:73 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | -| auth_bad_3.py:14:10:14:16 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| auth_bad_3.py:15:21:15:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| auth_bad_3.py:15:21:15:32 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| auth_bad_3.py:15:21:15:42 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| auth_bad_3.py:19:51:19:63 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | -| auth_bad_3.py:29:10:29:16 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| auth_bad_3.py:30:21:30:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| auth_bad_3.py:30:21:30:32 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| auth_bad_3.py:30:21:30:42 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| auth_bad_3.py:34:51:34:63 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | -| auth_bad_3.py:44:10:44:16 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| auth_bad_3.py:45:21:45:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| auth_bad_3.py:45:21:45:32 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| auth_bad_3.py:45:21:45:42 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| auth_bad_3.py:49:51:49:63 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | -#select -| auth_bad_2.py:19:61:19:73 | ControlFlowNode for search_filter | auth_bad_2.py:14:10:14:16 | ControlFlowNode for request | auth_bad_2.py:19:61:19:73 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_2.py:19:61:19:73 | ControlFlowNode for search_filter | This | auth_bad_2.py:14:10:14:16 | ControlFlowNode for request | a user-provided value | -| auth_bad_2.py:19:61:19:73 | ControlFlowNode for search_filter | auth_bad_2.py:15:21:15:27 | ControlFlowNode for request | auth_bad_2.py:19:61:19:73 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_2.py:19:61:19:73 | ControlFlowNode for search_filter | This | auth_bad_2.py:15:21:15:27 | ControlFlowNode for request | a user-provided value | -| auth_bad_2.py:34:61:34:73 | ControlFlowNode for search_filter | auth_bad_2.py:29:10:29:16 | ControlFlowNode for request | auth_bad_2.py:34:61:34:73 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_2.py:34:61:34:73 | ControlFlowNode for search_filter | This | auth_bad_2.py:29:10:29:16 | ControlFlowNode for request | a user-provided value | -| auth_bad_2.py:34:61:34:73 | ControlFlowNode for search_filter | auth_bad_2.py:30:21:30:27 | ControlFlowNode for request | auth_bad_2.py:34:61:34:73 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_2.py:34:61:34:73 | ControlFlowNode for search_filter | This | auth_bad_2.py:30:21:30:27 | ControlFlowNode for request | a user-provided value | -| auth_bad_2.py:49:61:49:73 | ControlFlowNode for search_filter | auth_bad_2.py:44:10:44:16 | ControlFlowNode for request | auth_bad_2.py:49:61:49:73 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_2.py:49:61:49:73 | ControlFlowNode for search_filter | This | auth_bad_2.py:44:10:44:16 | ControlFlowNode for request | a user-provided value | -| auth_bad_2.py:49:61:49:73 | ControlFlowNode for search_filter | auth_bad_2.py:45:21:45:27 | ControlFlowNode for request | auth_bad_2.py:49:61:49:73 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_2.py:49:61:49:73 | ControlFlowNode for search_filter | This | auth_bad_2.py:45:21:45:27 | ControlFlowNode for request | a user-provided value | -| auth_bad_2.py:64:61:64:73 | ControlFlowNode for search_filter | auth_bad_2.py:59:10:59:16 | ControlFlowNode for request | auth_bad_2.py:64:61:64:73 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_2.py:64:61:64:73 | ControlFlowNode for search_filter | This | auth_bad_2.py:59:10:59:16 | ControlFlowNode for request | a user-provided value | -| auth_bad_2.py:64:61:64:73 | ControlFlowNode for search_filter | auth_bad_2.py:60:21:60:27 | ControlFlowNode for request | auth_bad_2.py:64:61:64:73 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_2.py:64:61:64:73 | ControlFlowNode for search_filter | This | auth_bad_2.py:60:21:60:27 | ControlFlowNode for request | a user-provided value | -| auth_bad_3.py:19:51:19:63 | ControlFlowNode for search_filter | auth_bad_3.py:14:10:14:16 | ControlFlowNode for request | auth_bad_3.py:19:51:19:63 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_3.py:19:51:19:63 | ControlFlowNode for search_filter | This | auth_bad_3.py:14:10:14:16 | ControlFlowNode for request | a user-provided value | -| auth_bad_3.py:19:51:19:63 | ControlFlowNode for search_filter | auth_bad_3.py:15:21:15:27 | ControlFlowNode for request | auth_bad_3.py:19:51:19:63 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_3.py:19:51:19:63 | ControlFlowNode for search_filter | This | auth_bad_3.py:15:21:15:27 | ControlFlowNode for request | a user-provided value | -| auth_bad_3.py:34:51:34:63 | ControlFlowNode for search_filter | auth_bad_3.py:29:10:29:16 | ControlFlowNode for request | auth_bad_3.py:34:51:34:63 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_3.py:34:51:34:63 | ControlFlowNode for search_filter | This | auth_bad_3.py:29:10:29:16 | ControlFlowNode for request | a user-provided value | -| auth_bad_3.py:34:51:34:63 | ControlFlowNode for search_filter | auth_bad_3.py:30:21:30:27 | ControlFlowNode for request | auth_bad_3.py:34:51:34:63 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_3.py:34:51:34:63 | ControlFlowNode for search_filter | This | auth_bad_3.py:30:21:30:27 | ControlFlowNode for request | a user-provided value | -| auth_bad_3.py:49:51:49:63 | ControlFlowNode for search_filter | auth_bad_3.py:44:10:44:16 | ControlFlowNode for request | auth_bad_3.py:49:51:49:63 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_3.py:49:51:49:63 | ControlFlowNode for search_filter | This | auth_bad_3.py:44:10:44:16 | ControlFlowNode for request | a user-provided value | -| auth_bad_3.py:49:51:49:63 | ControlFlowNode for search_filter | auth_bad_3.py:45:21:45:27 | ControlFlowNode for request | auth_bad_3.py:49:51:49:63 | ControlFlowNode for search_filter | $@ LDAP query parameter contains $@ and is executed without authentication. | auth_bad_3.py:49:51:49:63 | ControlFlowNode for search_filter | This | auth_bad_3.py:45:21:45:27 | ControlFlowNode for request | a user-provided value | +| The following LDAP bind operation is executed without authentication | auth_bad_2.py:18:5:18: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:48:5:48:43 | ControlFlowNode for Attribute() | +| The following LDAP bind operation is executed without authentication | auth_bad_2.py:63:5:63:39 | ControlFlowNode for Attribute() | +| The following LDAP bind operation is executed without authentication | auth_bad_3.py:18:12:18:57 | ControlFlowNode for Connection() | +| The following LDAP bind operation is executed without authentication | auth_bad_3.py:33:12:33:55 | ControlFlowNode for Connection() | +| The following LDAP bind operation is executed without authentication | auth_bad_3.py:48:12:48:42 | ControlFlowNode for Connection() | From dfe16aae4c3e6748b606bd9acef4f17a24433582 Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Mon, 28 Jun 2021 10:46:13 +0200 Subject: [PATCH 21/29] Python: Handle both positional and keyword args for LDAP bind --- .../semmle/python/frameworks/LDAP.qll | 8 ++++-- .../CWE-287/ImproperLdapAuth.expected | 11 +++++--- .../Security/CWE-287/auth_bad_2.py | 28 +++++++++++++++++++ .../Security/CWE-287/auth_bad_3.py | 16 ++++++++++- 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll b/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll index 5ba727675e86..9531ceeecda2 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll @@ -82,7 +82,9 @@ private module LDAP { private class LDAP2Bind extends DataFlow::CallCfgNode, LDAPBind::Range { LDAP2Bind() { this.getFunction() = ldapBind() } - override DataFlow::Node getPassword() { result = this.getArg(1) } + override DataFlow::Node getPassword() { + result in [this.getArg(1), this.getArgByName("cred")] + } } /** @@ -147,7 +149,9 @@ private module LDAP { class LDAP3Bind extends DataFlow::CallCfgNode, LDAPBind::Range { LDAP3Bind() { this = ldap3Connection().getACall() } - override DataFlow::Node getPassword() { result = this.getArgByName("password") } + override DataFlow::Node getPassword() { + result in [this.getArg(2), this.getArgByName("password")] + } } /** diff --git a/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected b/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected index d3d868c16eee..181e1baadba2 100644 --- a/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected +++ b/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected @@ -1,7 +1,10 @@ | The following LDAP bind operation is executed without authentication | auth_bad_2.py:18:5:18: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:48:5:48:43 | ControlFlowNode for Attribute() | -| The following LDAP bind operation is executed without authentication | auth_bad_2.py:63:5:63:39 | ControlFlowNode for Attribute() | -| The following LDAP bind operation is executed without authentication | auth_bad_3.py:18:12:18:57 | ControlFlowNode for Connection() | -| The following LDAP bind operation is executed without authentication | auth_bad_3.py:33:12:33:55 | ControlFlowNode for Connection() | -| The following LDAP bind operation is executed without authentication | auth_bad_3.py:48:12:48:42 | ControlFlowNode for Connection() | +| The following LDAP bind operation is executed without authentication | auth_bad_2.py:62:5:62:52 | ControlFlowNode for Attribute() | +| The following LDAP bind operation is executed without authentication | auth_bad_2.py:76:5:76:39 | ControlFlowNode for Attribute() | +| The following LDAP bind operation is executed without authentication | auth_bad_2.py:91:5:91:48 | ControlFlowNode for Attribute() | +| The following LDAP bind operation is executed without authentication | auth_bad_3.py:18:12:18: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:47:12:47:55 | ControlFlowNode for Connection() | +| The following LDAP bind operation is executed without authentication | auth_bad_3.py:62:12:62:42 | ControlFlowNode for Connection() | diff --git a/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_2.py b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_2.py index cc64623015e3..3387c95599b9 100644 --- a/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_2.py +++ b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_2.py @@ -48,6 +48,19 @@ def bind_s_example(): ldap_connection.bind_s('cn=root', None) user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) +@app.route("/bind_s_example") +def bind_s_example_kwargs(): + """ + A RemoteFlowSource is used directly as DN and search filter while the bind's password + is set to None + """ + + dn = request.args['dc'] + search_filter = request.args['search'] + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.bind_s(who='cn=root', cred=None) + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) @app.route("/bind_example") def bind_example(): @@ -64,5 +77,20 @@ def bind_example(): user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) +@app.route("/bind_example") +def bind_example(): + """ + A RemoteFlowSource is used directly as DN and search filter while the bind's password + is set to None + """ + + dn = request.args['dc'] + search_filter = request.args['search'] + + ldap_connection = ldap.initialize("ldap://127.0.0.1:1337") + ldap_connection.bind(who='cn=root', cred="") + user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) + + # if __name__ == "__main__": # app.run(debug=True) diff --git a/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_3.py b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_3.py index b69722bf589e..402aff8b7e4c 100644 --- a/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_3.py +++ b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_3.py @@ -15,10 +15,24 @@ def passwordNone(): search_filter = request.args['search'] srv = Server('servername', get_info=ALL) - conn = Connection(srv, user='user_dn', password=None) + conn = Connection(srv, 'user_dn', None) status, result, response, _ = conn.search(dn, search_filter) +@app.route("/passwordNone") +def passwordNoneKwargs(): + """ + A RemoteFlowSource is used directly as DN and search filter while the connection's password + is set to None + """ + + dn = request.args['dc'] + search_filter = 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) + @app.route("/passwordEmpty") def passwordEmpty(): """ From b33f6a315c21b395fbddab38a76d1422330cf8bb Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Mon, 28 Jun 2021 10:51:01 +0200 Subject: [PATCH 22/29] Python: Fix select for py/improper-ldap-auth --- .../Security/CWE-287/ImproperLdapAuth.ql | 2 +- .../CWE-287/ImproperLdapAuth.expected | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.ql b/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.ql index 27fc46bcdb24..560622943993 100644 --- a/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.ql +++ b/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.ql @@ -15,4 +15,4 @@ import experimental.semmle.python.security.LDAPImproperAuth from LDAPBind ldapBind where authenticatesImproperly(ldapBind) -select "The following LDAP bind operation is executed without authentication", ldapBind +select ldapBind, "The following LDAP bind operation is executed without authentication" diff --git a/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected b/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected index 181e1baadba2..7b27b7fcdd5b 100644 --- a/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected +++ b/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected @@ -1,10 +1,10 @@ -| The following LDAP bind operation is executed without authentication | auth_bad_2.py:18:5:18: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:48:5:48:43 | ControlFlowNode for Attribute() | -| The following LDAP bind operation is executed without authentication | auth_bad_2.py:62:5:62:52 | ControlFlowNode for Attribute() | -| The following LDAP bind operation is executed without authentication | auth_bad_2.py:76:5:76:39 | ControlFlowNode for Attribute() | -| The following LDAP bind operation is executed without authentication | auth_bad_2.py:91:5:91:48 | ControlFlowNode for Attribute() | -| The following LDAP bind operation is executed without authentication | auth_bad_3.py:18:12:18: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:47:12:47:55 | ControlFlowNode for Connection() | -| The following LDAP bind operation is executed without authentication | auth_bad_3.py:62:12:62:42 | ControlFlowNode for Connection() | +| auth_bad_2.py:18:5:18: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:48:5:48:43 | ControlFlowNode for Attribute() | The following LDAP bind operation is executed without authentication | +| auth_bad_2.py:62:5:62:52 | ControlFlowNode for Attribute() | The following LDAP bind operation is executed without authentication | +| auth_bad_2.py:76:5:76:39 | ControlFlowNode for Attribute() | The following LDAP bind operation is executed without authentication | +| auth_bad_2.py:91:5:91:48 | ControlFlowNode for Attribute() | The following LDAP bind operation is executed without authentication | +| auth_bad_3.py:18:12:18: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:47:12:47:55 | ControlFlowNode for Connection() | The following LDAP bind operation is executed without authentication | +| auth_bad_3.py:62:12:62:42 | ControlFlowNode for Connection() | The following LDAP bind operation is executed without authentication | From 4a2c99a02194096562892e41355dcd664784998e Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Mon, 28 Jun 2021 10:51:31 +0200 Subject: [PATCH 23/29] Python: Inline `LDAPImproperAuth.qll` Since having it inlined makes the query a bit easier to read. We obviously need to share it if we want to share this predicate, but for now that does not seem to be the case. --- .../Security/CWE-287/ImproperLdapAuth.ql | 15 ++++++++++++- .../python/security/LDAPImproperAuth.qll | 21 ------------------- 2 files changed, 14 insertions(+), 22 deletions(-) delete mode 100644 python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll diff --git a/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.ql b/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.ql index 560622943993..5c27d6417859 100644 --- a/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.ql +++ b/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.ql @@ -11,7 +11,20 @@ // Determine precision above import python -import experimental.semmle.python.security.LDAPImproperAuth +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) diff --git a/python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll b/python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll deleted file mode 100644 index 5d236f0d6a4d..000000000000 --- a/python/ql/src/experimental/semmle/python/security/LDAPImproperAuth.qll +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Provides a taint-tracking configuration for detecting LDAP improper authentication 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 - -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 5477b2e0d51dc80d7e17bc10dc46db03d2fd837a Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Mon, 28 Jun 2021 10:54:00 +0200 Subject: [PATCH 24/29] Python: Minor refactoring cleanup --- python/ql/src/experimental/semmle/python/frameworks/LDAP.qll | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll b/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll index 9531ceeecda2..9d5d093e7a7e 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll @@ -64,7 +64,7 @@ private module LDAP { LDAP2Query() { this.getFunction() = ldapQuery() } override DataFlow::Node getQuery() { - result in [this.getArg(0), [this.getArg(2), this.getArgByName("filterstr")]] + result in [this.getArg(0), this.getArg(2), this.getArgByName("filterstr")] } } From b9422518b354f537a9f3377df51d57936e0a722f Mon Sep 17 00:00:00 2001 From: jorgectf Date: Mon, 28 Jun 2021 14:00:00 +0200 Subject: [PATCH 25/29] Rephrase .qhelp --- .../src/experimental/Security/CWE-287/ImproperLdapAuth.qhelp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.qhelp b/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.qhelp index 7f4b018bdf2c..6f7c2b775701 100644 --- a/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.qhelp +++ b/python/ql/src/experimental/Security/CWE-287/ImproperLdapAuth.qhelp @@ -8,7 +8,7 @@ to result in a successful authentication.

    -

    Use a strong password while establishing a LDAP connection to execute a query a user controls.

    +

    Use a non-empty password while establishing an LDAP connection.

    @@ -17,7 +17,7 @@ to result in a successful authentication.

    -

    In the third and fourth examples, the authentication is established using a password from a secure source such as environment variables.

    +

    In the third and fourth examples, the authentication is established using a password.

    From 1d4d8ab6e0cd53f9785072384891e88de5dbbd70 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Mon, 28 Jun 2021 14:16:52 +0200 Subject: [PATCH 26/29] Fix tests --- .../Security/CWE-287/examples/auth_bad_2.py | 6 ++- .../Security/CWE-287/examples/auth_bad_3.py | 7 +-- .../Security/CWE-287/examples/auth_good_2.py | 9 ++-- .../Security/CWE-287/examples/auth_good_3.py | 10 ++--- .../Security/CWE-287/auth_bad_2.py | 44 +++++++++---------- .../Security/CWE-287/auth_bad_3.py | 30 ++++++------- .../Security/CWE-287/auth_good_2.py | 39 ++++++++-------- .../Security/CWE-287/auth_good_3.py | 12 ++--- 8 files changed, 76 insertions(+), 81 deletions(-) diff --git a/python/ql/src/experimental/Security/CWE-287/examples/auth_bad_2.py b/python/ql/src/experimental/Security/CWE-287/examples/auth_bad_2.py index c893016ee343..550305de5e71 100644 --- a/python/ql/src/experimental/Security/CWE-287/examples/auth_bad_2.py +++ b/python/ql/src/experimental/Security/CWE-287/examples/auth_bad_2.py @@ -1,11 +1,13 @@ from flask import request, Flask import ldap +import ldap.filter +import ldap.dn @app.route("/bind_example") def bind_example(): - dn = request.args['dc'] - search_filter = request.args['search'] + 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', "") diff --git a/python/ql/src/experimental/Security/CWE-287/examples/auth_bad_3.py b/python/ql/src/experimental/Security/CWE-287/examples/auth_bad_3.py index 2cce5b73bd5b..b28aad4155fc 100644 --- a/python/ql/src/experimental/Security/CWE-287/examples/auth_bad_3.py +++ b/python/ql/src/experimental/Security/CWE-287/examples/auth_bad_3.py @@ -1,11 +1,12 @@ 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 = request.args['dc'] - search_filter = request.args['search'] + 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) diff --git a/python/ql/src/experimental/Security/CWE-287/examples/auth_good_2.py b/python/ql/src/experimental/Security/CWE-287/examples/auth_good_2.py index e5fc88f56d1c..c08606321711 100644 --- a/python/ql/src/experimental/Security/CWE-287/examples/auth_good_2.py +++ b/python/ql/src/experimental/Security/CWE-287/examples/auth_good_2.py @@ -1,13 +1,14 @@ from flask import request, Flask import ldap -import os +import ldap.filter +import ldap.dn @app.route("/bind_example") def bind_example(): - dn = request.args['dc'] - search_filter = request.args['search'] + 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', os.environ.get('LDAP_PASSWORD')) + ldap_connection.bind('cn=root', "SecurePa$$!") user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) diff --git a/python/ql/src/experimental/Security/CWE-287/examples/auth_good_3.py b/python/ql/src/experimental/Security/CWE-287/examples/auth_good_3.py index ae554fa9f7d2..0d93943831b1 100644 --- a/python/ql/src/experimental/Security/CWE-287/examples/auth_good_3.py +++ b/python/ql/src/experimental/Security/CWE-287/examples/auth_good_3.py @@ -1,14 +1,14 @@ from ldap3 import Server, Connection, ALL from flask import request, Flask -import os - +from ldap3.utils.dn import escape_rdn +from ldap3.utils.conv import escape_filter_chars @app.route("/passwordFromEnv") def passwordFromEnv(): - dn = request.args['dc'] - search_filter = request.args['search'] + 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=os.environ.get('LDAP_PASSWORD')) + password="SecurePa$$!") status, result, response, _ = conn.search(dn, search_filter) diff --git a/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_2.py b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_2.py index 3387c95599b9..d37cc09cfd06 100644 --- a/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_2.py +++ b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_2.py @@ -1,5 +1,7 @@ from flask import request, Flask import ldap +import ldap.filter +import ldap.dn app = Flask(__name__) @@ -7,12 +9,11 @@ @app.route("/simple_bind_example") def simple_bind_example(): """ - A RemoteFlowSource is used directly as DN and search filter while the bind's password - is not set + The bind's password argument is not set """ - dn = request.args['dc'] - search_filter = request.args['search'] + 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.simple_bind('cn=root') @@ -22,12 +23,11 @@ def simple_bind_example(): @app.route("/simple_bind_s_example") def simple_bind_s_example(): """ - A RemoteFlowSource is used directly as DN and search filter while the bind's password - is not set + The bind's password argument is not set """ - dn = request.args['dc'] - search_filter = request.args['search'] + 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.simple_bind_s('cn=root') @@ -37,12 +37,11 @@ def simple_bind_s_example(): @app.route("/bind_s_example") def bind_s_example(): """ - A RemoteFlowSource is used directly as DN and search filter while the bind's password - is set to None + The bind's password argument is set to None """ - dn = request.args['dc'] - search_filter = request.args['search'] + 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_s('cn=root', None) @@ -51,12 +50,11 @@ def bind_s_example(): @app.route("/bind_s_example") def bind_s_example_kwargs(): """ - A RemoteFlowSource is used directly as DN and search filter while the bind's password - is set to None + The bind's password argument is set to None """ - dn = request.args['dc'] - search_filter = request.args['search'] + 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_s(who='cn=root', cred=None) @@ -65,12 +63,11 @@ def bind_s_example_kwargs(): @app.route("/bind_example") def bind_example(): """ - A RemoteFlowSource is used directly as DN and search filter while the bind's password - is set to None + The bind's password argument is an empty string """ - dn = request.args['dc'] - search_filter = request.args['search'] + 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', "") @@ -80,12 +77,11 @@ def bind_example(): @app.route("/bind_example") def bind_example(): """ - A RemoteFlowSource is used directly as DN and search filter while the bind's password - is set to None + The bind's password argument is an empty string """ - dn = request.args['dc'] - search_filter = request.args['search'] + 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(who='cn=root', cred="") diff --git a/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_3.py b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_3.py index 402aff8b7e4c..2500b4cadb6b 100644 --- a/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_3.py +++ b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_bad_3.py @@ -1,5 +1,7 @@ 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 = Flask(__name__) @@ -7,12 +9,11 @@ @app.route("/passwordNone") def passwordNone(): """ - A RemoteFlowSource is used directly as DN and search filter while the connection's password - is set to None + The bind's password argument is set to None """ - dn = request.args['dc'] - search_filter = request.args['search'] + 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_dn', None) @@ -22,12 +23,11 @@ def passwordNone(): @app.route("/passwordNone") def passwordNoneKwargs(): """ - A RemoteFlowSource is used directly as DN and search filter while the connection's password - is set to None + The bind's password argument is set to None """ - dn = request.args['dc'] - search_filter = request.args['search'] + 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) @@ -36,12 +36,11 @@ def passwordNoneKwargs(): @app.route("/passwordEmpty") def passwordEmpty(): """ - A RemoteFlowSource is used directly as DN and search filter while the connection's password - is empty + The bind's password argument is an empty string """ - dn = request.args['dc'] - search_filter = request.args['search'] + 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="") @@ -51,12 +50,11 @@ def passwordEmpty(): @app.route("/notPassword") def notPassword(): """ - A RemoteFlowSource is used directly as DN and search filter while the connection's password - is not set + The bind's password argument is not set """ - dn = request.args['dc'] - search_filter = request.args['search'] + 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') diff --git a/python/ql/test/experimental/query-tests/Security/CWE-287/auth_good_2.py b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_good_2.py index 925f80a3e9d9..1c959efc613b 100644 --- a/python/ql/test/experimental/query-tests/Security/CWE-287/auth_good_2.py +++ b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_good_2.py @@ -1,6 +1,7 @@ from flask import request, Flask import ldap -import os +import ldap.filter +import ldap.dn app = Flask(__name__) @@ -8,60 +9,56 @@ @app.route("/simple_bind_example") def simple_bind_example(): """ - A RemoteFlowSource is used directly as DN and search filter while the bind's password - is an environment variable + The bind's password argument is a non-empty string """ - dn = request.args['dc'] - search_filter = request.args['search'] + 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.simple_bind('cn=root', os.environ.get('LDAP_PASSWORD')) + ldap_connection.simple_bind('cn=root', "SecurePa$$!") user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) @app.route("/simple_bind_s_example") def simple_bind_s_example(): """ - A RemoteFlowSource is used directly as DN and search filter while the bind's password - is an environment variable + The bind's password argument is a non-empty string """ - dn = request.args['dc'] - search_filter = request.args['search'] + 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.simple_bind_s('cn=root', os.environ.get('LDAP_PASSWORD')) + ldap_connection.simple_bind_s('cn=root', "SecurePa$$!") user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) @app.route("/bind_s_example") def bind_s_example(): """ - A RemoteFlowSource is used directly as DN and search filter while the bind's password - is an environment variable + The bind's password argument is a non-empty string """ - dn = request.args['dc'] - search_filter = request.args['search'] + 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_s('cn=root', os.environ.get('LDAP_PASSWORD')) + ldap_connection.bind_s('cn=root', "SecurePa$$!") user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) @app.route("/bind_example") def bind_example(): """ - A RemoteFlowSource is used directly as DN and search filter while the bind's password - is an environment variable + The bind's password argument is a non-empty string """ - dn = request.args['dc'] - search_filter = request.args['search'] + 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', os.environ.get('LDAP_PASSWORD')) + ldap_connection.bind('cn=root', "SecurePa$$!") user = ldap_connection.search_s(dn, ldap.SCOPE_SUBTREE, search_filter) # if __name__ == "__main__": diff --git a/python/ql/test/experimental/query-tests/Security/CWE-287/auth_good_3.py b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_good_3.py index fd9174c14286..530900877db9 100644 --- a/python/ql/test/experimental/query-tests/Security/CWE-287/auth_good_3.py +++ b/python/ql/test/experimental/query-tests/Security/CWE-287/auth_good_3.py @@ -1,6 +1,7 @@ from ldap3 import Server, Connection, ALL from flask import request, Flask -import os +from ldap3.utils.dn import escape_rdn +from ldap3.utils.conv import escape_filter_chars app = Flask(__name__) @@ -8,16 +9,15 @@ @app.route("/passwordFromEnv") def passwordFromEnv(): """ - A RemoteFlowSource is used directly as DN and search filter while the connection's password - is an environment variable + The bind's password argument is a non-empty string """ - dn = request.args['dc'] - search_filter = request.args['search'] + 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=os.environ.get('LDAP_PASSWORD')) + password="SecurePa$$!") status, result, response, _ = conn.search(dn, search_filter) # if __name__ == "__main__": From 1d432af4985bcc0c2d1afc6d91ea517ff97c4223 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Mon, 28 Jun 2021 14:18:27 +0200 Subject: [PATCH 27/29] Update `.expected` --- .../Security/CWE-287/ImproperLdapAuth.expected | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected b/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected index 7b27b7fcdd5b..766665ee1b6b 100644 --- a/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected +++ b/python/ql/test/experimental/query-tests/Security/CWE-287/ImproperLdapAuth.expected @@ -1,10 +1,10 @@ -| auth_bad_2.py:18:5:18:42 | ControlFlowNode for Attribute() | The following LDAP bind operation is executed without authentication | +| 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:48:5:48:43 | ControlFlowNode for Attribute() | The following LDAP bind operation is executed without authentication | -| auth_bad_2.py:62:5:62:52 | ControlFlowNode for Attribute() | The following LDAP bind operation is executed without authentication | -| auth_bad_2.py:76:5:76:39 | ControlFlowNode for Attribute() | The following LDAP bind operation is executed without authentication | -| auth_bad_2.py:91:5:91:48 | ControlFlowNode for Attribute() | The following LDAP bind operation is executed without authentication | -| auth_bad_3.py:18:12:18:43 | ControlFlowNode for Connection() | 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:47:12:47:55 | ControlFlowNode for Connection() | The following LDAP bind operation is executed without authentication | -| auth_bad_3.py:62:12:62:42 | 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 | From 2f9e6454a59ff853cc8cd99db8a523c1e5d340a4 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Tue, 29 Jun 2021 16:14:55 +0200 Subject: [PATCH 28/29] Hardcode `ldap2` binding functions --- .../semmle/python/frameworks/LDAP.qll | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll b/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll index 9d5d093e7a7e..9d115959f19b 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll @@ -68,16 +68,30 @@ private module LDAP { } } + /** + * 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" + ] + } + } + /** Gets a reference to a `ldap` bind. */ private DataFlow::Node ldapBind() { result = ldapOperation() and - result.(DataFlow::AttrRead).getAttributeName().matches("%bind%") + result.(DataFlow::AttrRead).getAttributeName() instanceof LDAP2BindMethods } /** * A class to find `ldap` methods binding a connection. * - * See `LDAP2QueryMethods` + * See `LDAP2BindMethods` */ private class LDAP2Bind extends DataFlow::CallCfgNode, LDAPBind::Range { LDAP2Bind() { this.getFunction() = ldapBind() } From 42a997cbcbacd7b3169df09de94c18daabee7b3b Mon Sep 17 00:00:00 2001 From: Rasmus Wriedt Larsen Date: Thu, 22 Jul 2021 15:59:13 +0200 Subject: [PATCH 29/29] Python: Fix deprecation warning --- python/ql/src/experimental/semmle/python/frameworks/LDAP.qll | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll b/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll index 9d115959f19b..83b1accafc1c 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/LDAP.qll @@ -26,7 +26,7 @@ private module LDAP { API::Node ldapInitialize() { result = ldap().getMember("initialize") } /** Gets a reference to a `ldap` operation. */ - private DataFlow::LocalSourceNode ldapOperation(DataFlow::TypeTracker t) { + private DataFlow::TypeTrackingNode ldapOperation(DataFlow::TypeTracker t) { t.start() and result.(DataFlow::AttrRead).getObject().getALocalSource() = ldapInitialize().getACall() or