From 019cb2cca4e5d72cd80315eb31b1f544b903b9ca Mon Sep 17 00:00:00 2001 From: Keshav Malik Date: Sun, 9 Aug 2026 19:32:09 +0530 Subject: [PATCH] Python: Model ldap3 paged searches --- .../2026-08-07-ldap3-paged-search.md | 4 + .../ql/lib/semmle/python/frameworks/Ldap3.qll | 9 + .../LdapInjection.expected | 174 +++++++++++++----- .../CWE-090-LdapInjection/ldap3_bad.py | 58 ++++++ .../CWE-090-LdapInjection/ldap3_good.py | 62 +++++++ 5 files changed, 258 insertions(+), 49 deletions(-) create mode 100644 python/ql/lib/change-notes/2026-08-07-ldap3-paged-search.md diff --git a/python/ql/lib/change-notes/2026-08-07-ldap3-paged-search.md b/python/ql/lib/change-notes/2026-08-07-ldap3-paged-search.md new file mode 100644 index 000000000000..99ec8ff35d0b --- /dev/null +++ b/python/ql/lib/change-notes/2026-08-07-ldap3-paged-search.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* The `py/ldap-injection` query now detects user-controlled search bases and filters passed to `ldap3.Connection.extend.standard.paged_search`. diff --git a/python/ql/lib/semmle/python/frameworks/Ldap3.qll b/python/ql/lib/semmle/python/frameworks/Ldap3.qll index 860df4a3b84a..3fb5c898c9b5 100644 --- a/python/ql/lib/semmle/python/frameworks/Ldap3.qll +++ b/python/ql/lib/semmle/python/frameworks/Ldap3.qll @@ -23,6 +23,15 @@ private module Ldap3 { .getReturn() .getMember("search") .getACall() + or + this = + API::moduleImport("ldap3") + .getMember("Connection") + .getReturn() + .getMember("extend") + .getMember("standard") + .getMember("paged_search") + .getACall() } override DataFlow::Node getFilter() { diff --git a/python/ql/test/query-tests/Security/CWE-090-LdapInjection/LdapInjection.expected b/python/ql/test/query-tests/Security/CWE-090-LdapInjection/LdapInjection.expected index 20170029899e..61c5be4172df 100644 --- a/python/ql/test/query-tests/Security/CWE-090-LdapInjection/LdapInjection.expected +++ b/python/ql/test/query-tests/Security/CWE-090-LdapInjection/LdapInjection.expected @@ -1,26 +1,81 @@ +#select +| ldap3_bad.py:25:17:25:18 | ControlFlowNode for dn | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap3_bad.py:25:17:25:18 | ControlFlowNode for dn | LDAP query parameter (DN) depends on a $@. | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | +| ldap3_bad.py:25:21:25:33 | ControlFlowNode for search_filter | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap3_bad.py:25:21:25:33 | ControlFlowNode for search_filter | LDAP query parameter (filter) depends on a $@. | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | +| ldap3_bad.py:42:9:42:10 | ControlFlowNode for dn | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap3_bad.py:42:9:42:10 | ControlFlowNode for dn | LDAP query parameter (DN) depends on a $@. | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | +| ldap3_bad.py:42:13:42:25 | ControlFlowNode for search_filter | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap3_bad.py:42:13:42:25 | ControlFlowNode for search_filter | LDAP query parameter (filter) depends on a $@. | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | +| ldap3_bad.py:56:21:56:22 | ControlFlowNode for dn | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap3_bad.py:56:21:56:22 | ControlFlowNode for dn | LDAP query parameter (DN) depends on a $@. | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | +| ldap3_bad.py:56:39:56:51 | ControlFlowNode for search_filter | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap3_bad.py:56:39:56:51 | ControlFlowNode for search_filter | LDAP query parameter (filter) depends on a $@. | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | +| ldap3_bad.py:69:9:69:10 | ControlFlowNode for dn | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap3_bad.py:69:9:69:10 | ControlFlowNode for dn | LDAP query parameter (DN) depends on a $@. | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | +| ldap3_bad.py:69:13:69:25 | ControlFlowNode for search_filter | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap3_bad.py:69:13:69:25 | ControlFlowNode for search_filter | LDAP query parameter (filter) depends on a $@. | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | +| ldap3_bad.py:82:48:82:72 | ControlFlowNode for Attribute() | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap3_bad.py:82:48:82:72 | ControlFlowNode for Attribute() | LDAP query parameter (DN) depends on a $@. | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | +| ldap3_bad.py:85:32:85:64 | ControlFlowNode for Attribute() | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap3_bad.py:85:32:85:64 | ControlFlowNode for Attribute() | LDAP query parameter (filter) depends on a $@. | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | +| ldap3_bad.py:96:9:96:33 | ControlFlowNode for Attribute() | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap3_bad.py:96:9:96:33 | ControlFlowNode for Attribute() | LDAP query parameter (DN) depends on a $@. | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | +| ldap3_bad.py:96:36:96:68 | ControlFlowNode for Attribute() | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap3_bad.py:96:36:96:68 | ControlFlowNode for Attribute() | LDAP query parameter (filter) depends on a $@. | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | +| ldap_bad.py:21:9:21:10 | ControlFlowNode for dn | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap_bad.py:21:9:21:10 | ControlFlowNode for dn | LDAP query parameter (DN) depends on a $@. | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | +| ldap_bad.py:21:33:21:45 | ControlFlowNode for search_filter | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap_bad.py:21:33:21:45 | ControlFlowNode for search_filter | LDAP query parameter (filter) depends on a $@. | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | +| ldap_bad.py:37:9:37:10 | ControlFlowNode for dn | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap_bad.py:37:9:37:10 | ControlFlowNode for dn | LDAP query parameter (DN) depends on a $@. | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | +| ldap_bad.py:37:33:37:45 | ControlFlowNode for search_filter | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap_bad.py:37:33:37:45 | ControlFlowNode for search_filter | LDAP query parameter (filter) depends on a $@. | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | +| ldap_bad.py:55:9:55:10 | ControlFlowNode for dn | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap_bad.py:55:9:55:10 | ControlFlowNode for dn | LDAP query parameter (DN) depends on a $@. | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | +| ldap_bad.py:55:43:55:55 | ControlFlowNode for search_filter | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap_bad.py:55:43:55:55 | ControlFlowNode for search_filter | LDAP query parameter (filter) depends on a $@. | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | edges | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | provenance | | | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | provenance | | -| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:13:17:13:23 | ControlFlowNode for request | provenance | | -| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:13:17:13:23 | ControlFlowNode for request | provenance | | -| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:14:21:14:27 | ControlFlowNode for request | provenance | | -| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:30:17:30:23 | ControlFlowNode for request | provenance | | -| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:30:17:30:23 | ControlFlowNode for request | provenance | | -| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:31:21:31:27 | ControlFlowNode for request | provenance | | -| ldap3_bad.py:13:5:13:13 | ControlFlowNode for unsafe_dc | ldap3_bad.py:16:5:16:6 | ControlFlowNode for dn | provenance | | -| ldap3_bad.py:13:17:13:23 | ControlFlowNode for request | ldap3_bad.py:13:5:13:13 | ControlFlowNode for unsafe_dc | provenance | AdditionalTaintStep | -| ldap3_bad.py:13:17:13:23 | ControlFlowNode for request | ldap3_bad.py:14:5:14:17 | ControlFlowNode for unsafe_filter | provenance | AdditionalTaintStep | -| ldap3_bad.py:14:5:14:17 | ControlFlowNode for unsafe_filter | ldap3_bad.py:17:5:17:17 | ControlFlowNode for search_filter | provenance | | -| ldap3_bad.py:14:21:14:27 | ControlFlowNode for request | ldap3_bad.py:14:5:14:17 | ControlFlowNode for unsafe_filter | provenance | AdditionalTaintStep | -| ldap3_bad.py:16:5:16:6 | ControlFlowNode for dn | ldap3_bad.py:21:17:21:18 | ControlFlowNode for dn | provenance | | -| ldap3_bad.py:17:5:17:17 | ControlFlowNode for search_filter | ldap3_bad.py:21:21:21:33 | ControlFlowNode for search_filter | provenance | | -| ldap3_bad.py:30:5:30:13 | ControlFlowNode for unsafe_dc | ldap3_bad.py:33:5:33:6 | ControlFlowNode for dn | provenance | | -| ldap3_bad.py:30:17:30:23 | ControlFlowNode for request | ldap3_bad.py:30:5:30:13 | ControlFlowNode for unsafe_dc | provenance | AdditionalTaintStep | -| ldap3_bad.py:30:17:30:23 | ControlFlowNode for request | ldap3_bad.py:31:5:31:17 | ControlFlowNode for unsafe_filter | provenance | AdditionalTaintStep | -| ldap3_bad.py:31:5:31:17 | ControlFlowNode for unsafe_filter | ldap3_bad.py:34:5:34:17 | ControlFlowNode for search_filter | provenance | | -| ldap3_bad.py:31:21:31:27 | ControlFlowNode for request | ldap3_bad.py:31:5:31:17 | ControlFlowNode for unsafe_filter | provenance | AdditionalTaintStep | -| ldap3_bad.py:33:5:33:6 | ControlFlowNode for dn | ldap3_bad.py:38:9:38:10 | ControlFlowNode for dn | provenance | | -| ldap3_bad.py:34:5:34:17 | ControlFlowNode for search_filter | ldap3_bad.py:38:13:38:25 | ControlFlowNode for search_filter | provenance | | +| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:17:17:17:23 | ControlFlowNode for request | provenance | | +| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:17:17:17:23 | ControlFlowNode for request | provenance | | +| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:18:21:18:27 | ControlFlowNode for request | provenance | | +| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:34:17:34:23 | ControlFlowNode for request | provenance | | +| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:34:17:34:23 | ControlFlowNode for request | provenance | | +| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:35:21:35:27 | ControlFlowNode for request | provenance | | +| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:47:17:47:23 | ControlFlowNode for request | provenance | | +| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:47:17:47:23 | ControlFlowNode for request | provenance | | +| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:48:21:48:27 | ControlFlowNode for request | provenance | | +| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:61:17:61:23 | ControlFlowNode for request | provenance | | +| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:61:17:61:23 | ControlFlowNode for request | provenance | | +| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:62:21:62:27 | ControlFlowNode for request | provenance | | +| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:74:17:74:23 | ControlFlowNode for request | provenance | | +| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:74:17:74:23 | ControlFlowNode for request | provenance | | +| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:75:21:75:27 | ControlFlowNode for request | provenance | | +| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:90:17:90:23 | ControlFlowNode for request | provenance | | +| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:90:17:90:23 | ControlFlowNode for request | provenance | | +| ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | ldap3_bad.py:91:21:91:27 | ControlFlowNode for request | provenance | | +| ldap3_bad.py:17:5:17:13 | ControlFlowNode for unsafe_dc | ldap3_bad.py:20:5:20:6 | ControlFlowNode for dn | provenance | | +| ldap3_bad.py:17:17:17:23 | ControlFlowNode for request | ldap3_bad.py:17:5:17:13 | ControlFlowNode for unsafe_dc | provenance | AdditionalTaintStep | +| ldap3_bad.py:17:17:17:23 | ControlFlowNode for request | ldap3_bad.py:18:5:18:17 | ControlFlowNode for unsafe_filter | provenance | AdditionalTaintStep | +| ldap3_bad.py:18:5:18:17 | ControlFlowNode for unsafe_filter | ldap3_bad.py:21:5:21:17 | ControlFlowNode for search_filter | provenance | | +| ldap3_bad.py:18:21:18:27 | ControlFlowNode for request | ldap3_bad.py:18:5:18:17 | ControlFlowNode for unsafe_filter | provenance | AdditionalTaintStep | +| ldap3_bad.py:20:5:20:6 | ControlFlowNode for dn | ldap3_bad.py:25:17:25:18 | ControlFlowNode for dn | provenance | | +| ldap3_bad.py:21:5:21:17 | ControlFlowNode for search_filter | ldap3_bad.py:25:21:25:33 | ControlFlowNode for search_filter | provenance | | +| ldap3_bad.py:34:5:34:13 | ControlFlowNode for unsafe_dc | ldap3_bad.py:37:5:37:6 | ControlFlowNode for dn | provenance | | +| ldap3_bad.py:34:17:34:23 | ControlFlowNode for request | ldap3_bad.py:34:5:34:13 | ControlFlowNode for unsafe_dc | provenance | AdditionalTaintStep | +| ldap3_bad.py:34:17:34:23 | ControlFlowNode for request | ldap3_bad.py:35:5:35:17 | ControlFlowNode for unsafe_filter | provenance | AdditionalTaintStep | +| ldap3_bad.py:35:5:35:17 | ControlFlowNode for unsafe_filter | ldap3_bad.py:38:5:38:17 | ControlFlowNode for search_filter | provenance | | +| ldap3_bad.py:35:21:35:27 | ControlFlowNode for request | ldap3_bad.py:35:5:35:17 | ControlFlowNode for unsafe_filter | provenance | AdditionalTaintStep | +| ldap3_bad.py:37:5:37:6 | ControlFlowNode for dn | ldap3_bad.py:42:9:42:10 | ControlFlowNode for dn | provenance | | +| ldap3_bad.py:38:5:38:17 | ControlFlowNode for search_filter | ldap3_bad.py:42:13:42:25 | ControlFlowNode for search_filter | provenance | | +| ldap3_bad.py:47:5:47:13 | ControlFlowNode for unsafe_dc | ldap3_bad.py:50:5:50:6 | ControlFlowNode for dn | provenance | | +| ldap3_bad.py:47:17:47:23 | ControlFlowNode for request | ldap3_bad.py:47:5:47:13 | ControlFlowNode for unsafe_dc | provenance | AdditionalTaintStep | +| ldap3_bad.py:47:17:47:23 | ControlFlowNode for request | ldap3_bad.py:48:5:48:17 | ControlFlowNode for unsafe_filter | provenance | AdditionalTaintStep | +| ldap3_bad.py:48:5:48:17 | ControlFlowNode for unsafe_filter | ldap3_bad.py:51:5:51:17 | ControlFlowNode for search_filter | provenance | | +| ldap3_bad.py:48:21:48:27 | ControlFlowNode for request | ldap3_bad.py:48:5:48:17 | ControlFlowNode for unsafe_filter | provenance | AdditionalTaintStep | +| ldap3_bad.py:50:5:50:6 | ControlFlowNode for dn | ldap3_bad.py:56:21:56:22 | ControlFlowNode for dn | provenance | | +| ldap3_bad.py:51:5:51:17 | ControlFlowNode for search_filter | ldap3_bad.py:56:39:56:51 | ControlFlowNode for search_filter | provenance | | +| ldap3_bad.py:61:5:61:13 | ControlFlowNode for unsafe_dc | ldap3_bad.py:64:5:64:6 | ControlFlowNode for dn | provenance | | +| ldap3_bad.py:61:17:61:23 | ControlFlowNode for request | ldap3_bad.py:61:5:61:13 | ControlFlowNode for unsafe_dc | provenance | AdditionalTaintStep | +| ldap3_bad.py:61:17:61:23 | ControlFlowNode for request | ldap3_bad.py:62:5:62:17 | ControlFlowNode for unsafe_filter | provenance | AdditionalTaintStep | +| ldap3_bad.py:62:5:62:17 | ControlFlowNode for unsafe_filter | ldap3_bad.py:65:5:65:17 | ControlFlowNode for search_filter | provenance | | +| ldap3_bad.py:62:21:62:27 | ControlFlowNode for request | ldap3_bad.py:62:5:62:17 | ControlFlowNode for unsafe_filter | provenance | AdditionalTaintStep | +| ldap3_bad.py:64:5:64:6 | ControlFlowNode for dn | ldap3_bad.py:69:9:69:10 | ControlFlowNode for dn | provenance | | +| ldap3_bad.py:65:5:65:17 | ControlFlowNode for search_filter | ldap3_bad.py:69:13:69:25 | ControlFlowNode for search_filter | provenance | | +| ldap3_bad.py:74:5:74:13 | ControlFlowNode for unsafe_dc | ldap3_bad.py:82:48:82:72 | ControlFlowNode for Attribute() | provenance | | +| ldap3_bad.py:74:17:74:23 | ControlFlowNode for request | ldap3_bad.py:74:5:74:13 | ControlFlowNode for unsafe_dc | provenance | AdditionalTaintStep | +| ldap3_bad.py:74:17:74:23 | ControlFlowNode for request | ldap3_bad.py:75:5:75:17 | ControlFlowNode for unsafe_filter | provenance | AdditionalTaintStep | +| ldap3_bad.py:75:5:75:17 | ControlFlowNode for unsafe_filter | ldap3_bad.py:85:32:85:64 | ControlFlowNode for Attribute() | provenance | | +| ldap3_bad.py:75:21:75:27 | ControlFlowNode for request | ldap3_bad.py:75:5:75:17 | ControlFlowNode for unsafe_filter | provenance | AdditionalTaintStep | +| ldap3_bad.py:90:5:90:13 | ControlFlowNode for unsafe_dc | ldap3_bad.py:96:9:96:33 | ControlFlowNode for Attribute() | provenance | | +| ldap3_bad.py:90:17:90:23 | ControlFlowNode for request | ldap3_bad.py:90:5:90:13 | ControlFlowNode for unsafe_dc | provenance | AdditionalTaintStep | +| ldap3_bad.py:90:17:90:23 | ControlFlowNode for request | ldap3_bad.py:91:5:91:17 | ControlFlowNode for unsafe_filter | provenance | AdditionalTaintStep | +| ldap3_bad.py:91:5:91:17 | ControlFlowNode for unsafe_filter | ldap3_bad.py:96:36:96:68 | ControlFlowNode for Attribute() | provenance | | +| ldap3_bad.py:91:21:91:27 | ControlFlowNode for request | ldap3_bad.py:91:5:91:17 | ControlFlowNode for unsafe_filter | provenance | AdditionalTaintStep | | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap_bad.py:1:19:1:25 | ControlFlowNode for request | provenance | | | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap_bad.py:1:19:1:25 | ControlFlowNode for request | provenance | | | ldap_bad.py:1:19:1:25 | ControlFlowNode for request | ldap_bad.py:13:17:13:23 | ControlFlowNode for request | provenance | | @@ -58,24 +113,56 @@ nodes | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember | | ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | | ldap3_bad.py:1:19:1:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| ldap3_bad.py:13:5:13:13 | ControlFlowNode for unsafe_dc | semmle.label | ControlFlowNode for unsafe_dc | -| ldap3_bad.py:13:17:13:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| ldap3_bad.py:13:17:13:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| ldap3_bad.py:14:5:14:17 | ControlFlowNode for unsafe_filter | semmle.label | ControlFlowNode for unsafe_filter | -| ldap3_bad.py:14:21:14:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| ldap3_bad.py:16:5:16:6 | ControlFlowNode for dn | semmle.label | ControlFlowNode for dn | -| ldap3_bad.py:17:5:17:17 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | -| ldap3_bad.py:21:17:21:18 | ControlFlowNode for dn | semmle.label | ControlFlowNode for dn | -| ldap3_bad.py:21:21:21:33 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | -| ldap3_bad.py:30:5:30:13 | ControlFlowNode for unsafe_dc | semmle.label | ControlFlowNode for unsafe_dc | -| ldap3_bad.py:30:17:30:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| ldap3_bad.py:30:17:30:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| ldap3_bad.py:31:5:31:17 | ControlFlowNode for unsafe_filter | semmle.label | ControlFlowNode for unsafe_filter | -| ldap3_bad.py:31:21:31:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| ldap3_bad.py:33:5:33:6 | ControlFlowNode for dn | semmle.label | ControlFlowNode for dn | -| ldap3_bad.py:34:5:34:17 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | -| ldap3_bad.py:38:9:38:10 | ControlFlowNode for dn | semmle.label | ControlFlowNode for dn | -| ldap3_bad.py:38:13:38:25 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | +| ldap3_bad.py:17:5:17:13 | ControlFlowNode for unsafe_dc | semmle.label | ControlFlowNode for unsafe_dc | +| ldap3_bad.py:17:17:17:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ldap3_bad.py:17:17:17:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ldap3_bad.py:18:5:18:17 | ControlFlowNode for unsafe_filter | semmle.label | ControlFlowNode for unsafe_filter | +| ldap3_bad.py:18:21:18:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ldap3_bad.py:20:5:20:6 | ControlFlowNode for dn | semmle.label | ControlFlowNode for dn | +| ldap3_bad.py:21:5:21:17 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | +| ldap3_bad.py:25:17:25:18 | ControlFlowNode for dn | semmle.label | ControlFlowNode for dn | +| ldap3_bad.py:25:21:25:33 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | +| ldap3_bad.py:34:5:34:13 | ControlFlowNode for unsafe_dc | semmle.label | ControlFlowNode for unsafe_dc | +| ldap3_bad.py:34:17:34:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ldap3_bad.py:34:17:34:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ldap3_bad.py:35:5:35:17 | ControlFlowNode for unsafe_filter | semmle.label | ControlFlowNode for unsafe_filter | +| ldap3_bad.py:35:21:35:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ldap3_bad.py:37:5:37:6 | ControlFlowNode for dn | semmle.label | ControlFlowNode for dn | +| ldap3_bad.py:38:5:38:17 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | +| ldap3_bad.py:42:9:42:10 | ControlFlowNode for dn | semmle.label | ControlFlowNode for dn | +| ldap3_bad.py:42:13:42:25 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | +| ldap3_bad.py:47:5:47:13 | ControlFlowNode for unsafe_dc | semmle.label | ControlFlowNode for unsafe_dc | +| ldap3_bad.py:47:17:47:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ldap3_bad.py:47:17:47:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ldap3_bad.py:48:5:48:17 | ControlFlowNode for unsafe_filter | semmle.label | ControlFlowNode for unsafe_filter | +| ldap3_bad.py:48:21:48:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ldap3_bad.py:50:5:50:6 | ControlFlowNode for dn | semmle.label | ControlFlowNode for dn | +| ldap3_bad.py:51:5:51:17 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | +| ldap3_bad.py:56:21:56:22 | ControlFlowNode for dn | semmle.label | ControlFlowNode for dn | +| ldap3_bad.py:56:39:56:51 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | +| ldap3_bad.py:61:5:61:13 | ControlFlowNode for unsafe_dc | semmle.label | ControlFlowNode for unsafe_dc | +| ldap3_bad.py:61:17:61:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ldap3_bad.py:61:17:61:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ldap3_bad.py:62:5:62:17 | ControlFlowNode for unsafe_filter | semmle.label | ControlFlowNode for unsafe_filter | +| ldap3_bad.py:62:21:62:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ldap3_bad.py:64:5:64:6 | ControlFlowNode for dn | semmle.label | ControlFlowNode for dn | +| ldap3_bad.py:65:5:65:17 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | +| ldap3_bad.py:69:9:69:10 | ControlFlowNode for dn | semmle.label | ControlFlowNode for dn | +| ldap3_bad.py:69:13:69:25 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | +| ldap3_bad.py:74:5:74:13 | ControlFlowNode for unsafe_dc | semmle.label | ControlFlowNode for unsafe_dc | +| ldap3_bad.py:74:17:74:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ldap3_bad.py:74:17:74:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ldap3_bad.py:75:5:75:17 | ControlFlowNode for unsafe_filter | semmle.label | ControlFlowNode for unsafe_filter | +| ldap3_bad.py:75:21:75:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ldap3_bad.py:82:48:82:72 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| ldap3_bad.py:85:32:85:64 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| ldap3_bad.py:90:5:90:13 | ControlFlowNode for unsafe_dc | semmle.label | ControlFlowNode for unsafe_dc | +| ldap3_bad.py:90:17:90:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ldap3_bad.py:90:17:90:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ldap3_bad.py:91:5:91:17 | ControlFlowNode for unsafe_filter | semmle.label | ControlFlowNode for unsafe_filter | +| ldap3_bad.py:91:21:91:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| ldap3_bad.py:96:9:96:33 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| ldap3_bad.py:96:36:96:68 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember | | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember | | ldap_bad.py:1:19:1:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | @@ -108,14 +195,3 @@ nodes | ldap_bad.py:55:9:55:10 | ControlFlowNode for dn | semmle.label | ControlFlowNode for dn | | ldap_bad.py:55:43:55:55 | ControlFlowNode for search_filter | semmle.label | ControlFlowNode for search_filter | subpaths -#select -| ldap3_bad.py:21:17:21:18 | ControlFlowNode for dn | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap3_bad.py:21:17:21:18 | ControlFlowNode for dn | LDAP query parameter (DN) depends on a $@. | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| ldap3_bad.py:21:21:21:33 | ControlFlowNode for search_filter | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap3_bad.py:21:21:21:33 | ControlFlowNode for search_filter | LDAP query parameter (filter) depends on a $@. | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| ldap3_bad.py:38:9:38:10 | ControlFlowNode for dn | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap3_bad.py:38:9:38:10 | ControlFlowNode for dn | LDAP query parameter (DN) depends on a $@. | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| ldap3_bad.py:38:13:38:25 | ControlFlowNode for search_filter | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap3_bad.py:38:13:38:25 | ControlFlowNode for search_filter | LDAP query parameter (filter) depends on a $@. | ldap3_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| ldap_bad.py:21:9:21:10 | ControlFlowNode for dn | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap_bad.py:21:9:21:10 | ControlFlowNode for dn | LDAP query parameter (DN) depends on a $@. | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| ldap_bad.py:21:33:21:45 | ControlFlowNode for search_filter | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap_bad.py:21:33:21:45 | ControlFlowNode for search_filter | LDAP query parameter (filter) depends on a $@. | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| ldap_bad.py:37:9:37:10 | ControlFlowNode for dn | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap_bad.py:37:9:37:10 | ControlFlowNode for dn | LDAP query parameter (DN) depends on a $@. | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| ldap_bad.py:37:33:37:45 | ControlFlowNode for search_filter | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap_bad.py:37:33:37:45 | ControlFlowNode for search_filter | LDAP query parameter (filter) depends on a $@. | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| ldap_bad.py:55:9:55:10 | ControlFlowNode for dn | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap_bad.py:55:9:55:10 | ControlFlowNode for dn | LDAP query parameter (DN) depends on a $@. | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | -| ldap_bad.py:55:43:55:55 | ControlFlowNode for search_filter | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | ldap_bad.py:55:43:55:55 | ControlFlowNode for search_filter | LDAP query parameter (filter) depends on a $@. | ldap_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | diff --git a/python/ql/test/query-tests/Security/CWE-090-LdapInjection/ldap3_bad.py b/python/ql/test/query-tests/Security/CWE-090-LdapInjection/ldap3_bad.py index a69aef9ad3d8..6a936ddc10bf 100644 --- a/python/ql/test/query-tests/Security/CWE-090-LdapInjection/ldap3_bad.py +++ b/python/ql/test/query-tests/Security/CWE-090-LdapInjection/ldap3_bad.py @@ -1,5 +1,9 @@ from flask import request, Flask # $ Source import ldap3 +import ldap3 as l3 +from ldap3 import Connection as LdapConnection, Server as LdapServer +from ldap3.utils.dn import escape_rdn +from ldap3.utils.conv import escape_filter_chars app = Flask(__name__) @@ -37,5 +41,59 @@ def direct(): conn = ldap3.Connection(srv, user=dn, auto_bind=True).search( dn, search_filter) # $ Alert + +@app.route("/paged-keyword") +def paged_keyword(): + unsafe_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + dn = "dc={}".format(unsafe_dc) + search_filter = "(user={})".format(unsafe_filter) + + srv = ldap3.Server('ldap://127.0.0.1') + conn = ldap3.Connection(srv, user=dn, auto_bind=True) + list(conn.extend.standard.paged_search( + search_base=dn, search_filter=search_filter)) # $ Alert + + +@app.route("/paged-positional") +def paged_positional(): + unsafe_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + dn = "dc={}".format(unsafe_dc) + search_filter = "(user={})".format(unsafe_filter) + + srv = ldap3.Server('ldap://127.0.0.1') + ldap3.Connection(srv, user=dn, auto_bind=True).extend.standard.paged_search( + dn, search_filter, generator=False) # $ Alert + + +@app.route("/paged-partially-sanitized") +def paged_partially_sanitized(): + unsafe_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + safe_dn = "dc={}".format(escape_rdn(unsafe_dc)) + safe_filter = "(user={})".format(escape_filter_chars(unsafe_filter)) + + conn = LdapConnection(LdapServer('ldap://127.0.0.1')) + conn.extend.standard.paged_search( + search_filter=safe_filter, search_base="dc={}".format(unsafe_dc), # $ Alert + generator=False) + list(conn.extend.standard.paged_search( + safe_dn, search_filter="(user={})".format(unsafe_filter))) # $ Alert + + +@app.route("/paged-bound-method") +def paged_bound_method(): + unsafe_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + conn = l3.Connection(l3.Server('ldap://127.0.0.1')) + paged_search = conn.extend.standard.paged_search + list(paged_search( + "dc={}".format(unsafe_dc), "(user={})".format(unsafe_filter))) # $ Alert + # if __name__ == "__main__": # app.run(debug=True) diff --git a/python/ql/test/query-tests/Security/CWE-090-LdapInjection/ldap3_good.py b/python/ql/test/query-tests/Security/CWE-090-LdapInjection/ldap3_good.py index bb2e6d7af83e..d2315275f606 100644 --- a/python/ql/test/query-tests/Security/CWE-090-LdapInjection/ldap3_good.py +++ b/python/ql/test/query-tests/Security/CWE-090-LdapInjection/ldap3_good.py @@ -45,5 +45,67 @@ def direct(): conn = ldap3.Connection(srv, user=dn, auto_bind=True).search( dn, search_filter) + +@app.route("/paged-keyword") +def paged_keyword(): + unsafe_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + dn = "dc={}".format(escape_rdn(unsafe_dc)) + search_filter = "(user={})".format(escape_filter_chars(unsafe_filter)) + + srv = ldap3.Server('ldap://127.0.0.1') + conn = ldap3.Connection(srv, user=dn, auto_bind=True) + conn.extend.standard.paged_search( + search_base=dn, search_filter=search_filter) + + +@app.route("/paged-positional") +def paged_positional(): + unsafe_dc = request.args['dc'] + unsafe_filter = request.args['username'] + + dn = "dc={}".format(escape_rdn(unsafe_dc)) + search_filter = "(user={})".format(escape_filter_chars(unsafe_filter)) + + srv = ldap3.Server('ldap://127.0.0.1') + ldap3.Connection(srv, user=dn, auto_bind=True).extend.standard.paged_search( + dn, search_filter) + + +@app.route("/paged-non-sink-arguments") +def paged_non_sink_arguments(): + unsafe = request.args['value'] + + srv = ldap3.Server('ldap://127.0.0.1') + conn = ldap3.Connection(srv) + conn.extend.standard.paged_search( + search_base="dc=example,dc=com", + search_filter="(objectClass=person)", + attributes=[unsafe], + controls=unsafe, + paged_size=unsafe, + generator=False) + + +class OtherStandardOperations: + def paged_search(self, search_base, search_filter): + pass + + +class OtherExtensions: + standard = OtherStandardOperations() + + +class OtherConnection: + extend = OtherExtensions() + + +@app.route("/paged-unrelated") +def paged_unrelated(): + unsafe_dc = request.args['dc'] + unsafe_filter = request.args['username'] + OtherConnection().extend.standard.paged_search(unsafe_dc, unsafe_filter) + # if __name__ == "__main__": # app.run(debug=True)