Skip to content

Commit 5a90214

Browse files
authored
Merge pull request github#7783 from yoff/python/promote-ldap-injection
Python: promote LDAP injection query
2 parents 8f8621f + 86786d3 commit 5a90214

File tree

23 files changed

+441
-67
lines changed

23 files changed

+441
-67
lines changed

docs/codeql/support/reusables/frameworks.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ Python built-in support
171171
Twisted, Web framework
172172
Flask-Admin, Web framework
173173
starlette, Asynchronous Server Gateway Interface (ASGI)
174+
python-ldap, Lightweight Directory Access Protocol (LDAP)
175+
ldap3, Lightweight Directory Access Protocol (LDAP)
174176
requests, HTTP client
175177
dill, Serialization
176178
PyYAML, Serialization

python/ql/lib/semmle/python/Concepts.qll

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,44 @@ module RegexExecution {
449449
}
450450
}
451451

452+
/** Provides classes for modeling LDAP-related APIs. */
453+
module LDAP {
454+
/**
455+
* A data-flow node that executes an LDAP query.
456+
*
457+
* Extend this class to refine existing API models. If you want to model new APIs,
458+
* extend `LDAPQuery::Range` instead.
459+
*/
460+
class LdapExecution extends DataFlow::Node {
461+
LdapExecution::Range range;
462+
463+
LdapExecution() { this = range }
464+
465+
/** Gets the argument containing the filter string. */
466+
DataFlow::Node getFilter() { result = range.getFilter() }
467+
468+
/** Gets the argument containing the base DN. */
469+
DataFlow::Node getBaseDn() { result = range.getBaseDn() }
470+
}
471+
472+
/** Provides classes for modeling new LDAP query execution-related APIs. */
473+
module LdapExecution {
474+
/**
475+
* A data-flow node that executes an LDAP query.
476+
*
477+
* Extend this class to model new APIs. If you want to refine existing API models,
478+
* extend `LDAPQuery` instead.
479+
*/
480+
abstract class Range extends DataFlow::Node {
481+
/** Gets the argument containing the filter string. */
482+
abstract DataFlow::Node getFilter();
483+
484+
/** Gets the argument containing the base DN. */
485+
abstract DataFlow::Node getBaseDn();
486+
}
487+
}
488+
}
489+
452490
/**
453491
* A data-flow node that escapes meta-characters, which could be used to prevent
454492
* injection attacks.
@@ -506,8 +544,20 @@ module Escaping {
506544
/** Gets the escape-kind for escaping a string so it can safely be included in HTML. */
507545
string getHtmlKind() { result = "html" }
508546

509-
/** Gets the escape-kind for escaping a string so it can safely be included in HTML. */
547+
/** Gets the escape-kind for escaping a string so it can safely be included in a regular expression. */
510548
string getRegexKind() { result = "regex" }
549+
550+
/**
551+
* Gets the escape-kind for escaping a string so it can safely be used as a
552+
* distinguished name (DN) in an LDAP search.
553+
*/
554+
string getLdapDnKind() { result = "ldap_dn" }
555+
556+
/**
557+
* Gets the escape-kind for escaping a string so it can safely be used as a
558+
* filter in an LDAP search.
559+
*/
560+
string getLdapFilterKind() { result = "ldap_filter" }
511561
// TODO: If adding an XML kind, update the modeling of the `MarkupSafe` PyPI package.
512562
//
513563
// Technically it claims to escape for both HTML and XML, but for now we don't have
@@ -532,6 +582,21 @@ class RegexEscaping extends Escaping {
532582
RegexEscaping() { range.getKind() = Escaping::getRegexKind() }
533583
}
534584

585+
/**
586+
* An escape of a string so it can be safely used as a distinguished name (DN)
587+
* in an LDAP search.
588+
*/
589+
class LdapDnEscaping extends Escaping {
590+
LdapDnEscaping() { range.getKind() = Escaping::getLdapDnKind() }
591+
}
592+
593+
/**
594+
* An escape of a string so it can be safely used as a filter in an LDAP search.
595+
*/
596+
class LdapFilterEscaping extends Escaping {
597+
LdapFilterEscaping() { range.getKind() = Escaping::getLdapFilterKind() }
598+
}
599+
535600
/** Provides classes for modeling HTTP-related APIs. */
536601
module HTTP {
537602
/** Gets an HTTP verb, in upper case */

python/ql/lib/semmle/python/Frameworks.qll

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ private import semmle.python.frameworks.FlaskSqlAlchemy
2222
private import semmle.python.frameworks.Idna
2323
private import semmle.python.frameworks.Invoke
2424
private import semmle.python.frameworks.Jmespath
25+
private import semmle.python.frameworks.Ldap
26+
private import semmle.python.frameworks.Ldap3
2527
private import semmle.python.frameworks.MarkupSafe
2628
private import semmle.python.frameworks.Multidict
2729
private import semmle.python.frameworks.Mysql
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `python-ldap` PyPI package (imported as `ldap`).
3+
* See https://www.python-ldap.org/en/python-ldap-3.3.0/index.html
4+
*/
5+
6+
private import python
7+
private import semmle.python.dataflow.new.DataFlow
8+
private import semmle.python.Concepts
9+
private import semmle.python.ApiGraphs
10+
11+
/**
12+
* Provides models for the `python-ldap` PyPI package (imported as `ldap`).
13+
*
14+
* See https://www.python-ldap.org/en/python-ldap-3.3.0/index.html
15+
*/
16+
private module Ldap {
17+
/**
18+
* The execution of an `ldap` query.
19+
*
20+
* See https://www.python-ldap.org/en/python-ldap-3.3.0/reference/ldap.html#functions
21+
*/
22+
private class LdapQueryExecution extends DataFlow::CallCfgNode, LDAP::LdapExecution::Range {
23+
LdapQueryExecution() {
24+
this =
25+
API::moduleImport("ldap")
26+
.getMember("initialize")
27+
.getReturn()
28+
.getMember(["search", "search_s", "search_st", "search_ext", "search_ext_s"])
29+
.getACall()
30+
}
31+
32+
override DataFlow::Node getFilter() {
33+
result in [this.getArg(2), this.getArgByName("filterstr")]
34+
}
35+
36+
override DataFlow::Node getBaseDn() { result in [this.getArg(0), this.getArgByName("base")] }
37+
}
38+
39+
/**
40+
* A call to `ldap.dn.escape_dn_chars`.
41+
*
42+
* See https://github.com/python-ldap/python-ldap/blob/7ce471e238cdd9a4dd8d17baccd1c9e05e6f894a/Lib/ldap/dn.py#L17
43+
*/
44+
private class LdapEscapeDnCall extends DataFlow::CallCfgNode, Escaping::Range {
45+
LdapEscapeDnCall() {
46+
this = API::moduleImport("ldap").getMember("dn").getMember("escape_dn_chars").getACall()
47+
}
48+
49+
override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("s")] }
50+
51+
override DataFlow::Node getOutput() { result = this }
52+
53+
override string getKind() { result = Escaping::getLdapDnKind() }
54+
}
55+
56+
/**
57+
* A call to `ldap.filter.escape_filter_chars`.
58+
*
59+
* See https://www.python-ldap.org/en/python-ldap-3.3.0/reference/ldap-filter.html#ldap.filter.escape_filter_chars
60+
*/
61+
private class LdapEscapeFilterCall extends DataFlow::CallCfgNode, Escaping::Range {
62+
LdapEscapeFilterCall() {
63+
this =
64+
API::moduleImport("ldap").getMember("filter").getMember("escape_filter_chars").getACall()
65+
}
66+
67+
override DataFlow::Node getAnInput() {
68+
result in [this.getArg(0), this.getArgByName("assertion_value")]
69+
}
70+
71+
override DataFlow::Node getOutput() { result = this }
72+
73+
override string getKind() { result = Escaping::getLdapFilterKind() }
74+
}
75+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `ldap3` PyPI package
3+
* See https://pypi.org/project/ldap3/
4+
*/
5+
6+
private import python
7+
private import semmle.python.dataflow.new.DataFlow
8+
private import semmle.python.Concepts
9+
private import semmle.python.ApiGraphs
10+
11+
/**
12+
* Provides models for the `ldap3` PyPI package
13+
*
14+
* See https://pypi.org/project/ldap3/
15+
*/
16+
private module Ldap3 {
17+
/** The execution of an `ldap` query. */
18+
private class LdapQueryExecution extends DataFlow::CallCfgNode, LDAP::LdapExecution::Range {
19+
LdapQueryExecution() {
20+
this =
21+
API::moduleImport("ldap3")
22+
.getMember("Connection")
23+
.getReturn()
24+
.getMember("search")
25+
.getACall()
26+
}
27+
28+
override DataFlow::Node getFilter() {
29+
result in [this.getArg(1), this.getArgByName("search_filter")]
30+
}
31+
32+
override DataFlow::Node getBaseDn() {
33+
result in [this.getArg(0), this.getArgByName("search_base")]
34+
}
35+
}
36+
37+
/**
38+
* A call to `ldap3.utils.dn.escape_rdn`.
39+
*
40+
* See https://github.com/cannatag/ldap3/blob/4d33166f0869b929f59c6e6825a1b9505eb99967/ldap3/utils/dn.py#L390
41+
*/
42+
private class LdapEscapeDnCall extends DataFlow::CallCfgNode, Escaping::Range {
43+
LdapEscapeDnCall() {
44+
this =
45+
API::moduleImport("ldap3")
46+
.getMember("utils")
47+
.getMember("dn")
48+
.getMember("escape_rdn")
49+
.getACall()
50+
}
51+
52+
override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("rdn")] }
53+
54+
override DataFlow::Node getOutput() { result = this }
55+
56+
override string getKind() { result = Escaping::getLdapDnKind() }
57+
}
58+
59+
/**
60+
* A call to `ldap3.utils.conv.escape_filter_chars`.
61+
*
62+
* See https://github.com/cannatag/ldap3/blob/4d33166f0869b929f59c6e6825a1b9505eb99967/ldap3/utils/conv.py#L91
63+
*/
64+
private class LdapEscapeFilterCall extends DataFlow::CallCfgNode, Escaping::Range {
65+
LdapEscapeFilterCall() {
66+
this =
67+
API::moduleImport("ldap3")
68+
.getMember("utils")
69+
.getMember("conv")
70+
.getMember("escape_filter_chars")
71+
.getACall()
72+
}
73+
74+
override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("text")] }
75+
76+
override DataFlow::Node getOutput() { result = this }
77+
78+
override string getKind() { result = Escaping::getLdapFilterKind() }
79+
}
80+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* Provides taint-tracking configurations for detecting LDAP injection vulnerabilities
3+
*
4+
* Note, for performance reasons: only import this file if
5+
* `LdapInjection::Configuration` is needed, otherwise
6+
* `LdapInjectionCustomizations` should be imported instead.
7+
*/
8+
9+
import python
10+
import semmle.python.Concepts
11+
import semmle.python.dataflow.new.DataFlow
12+
import semmle.python.dataflow.new.TaintTracking
13+
import semmle.python.dataflow.new.RemoteFlowSources
14+
15+
/**
16+
* Provides aint-tracking configurations for detecting LDAP injection vulnerabilities.class
17+
*
18+
* Two configurations are provided. One is for detecting LDAP injection
19+
* via the distinguished name (DN). The other is for detecting LDAP injection
20+
* via the filter. These require different escapings.
21+
*/
22+
module LdapInjection {
23+
import LdapInjectionCustomizations::LdapInjection
24+
25+
/**
26+
* A taint-tracking configuration for detecting LDAP injection vulnerabilities
27+
* via the distinguished name (DN) parameter of an LDAP search.
28+
*/
29+
class DnConfiguration extends TaintTracking::Configuration {
30+
DnConfiguration() { this = "LdapDnInjection" }
31+
32+
override predicate isSource(DataFlow::Node source) { source instanceof Source }
33+
34+
override predicate isSink(DataFlow::Node sink) { sink instanceof DnSink }
35+
36+
override predicate isSanitizer(DataFlow::Node node) { node instanceof DnSanitizer }
37+
38+
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
39+
guard instanceof DnSanitizerGuard
40+
}
41+
}
42+
43+
/**
44+
* A taint-tracking configuration for detecting LDAP injection vulnerabilities
45+
* via the filter parameter of an LDAP search.
46+
*/
47+
class FilterConfiguration extends TaintTracking::Configuration {
48+
FilterConfiguration() { this = "LdapFilterInjection" }
49+
50+
override predicate isSource(DataFlow::Node source) { source instanceof Source }
51+
52+
override predicate isSink(DataFlow::Node sink) { sink instanceof FilterSink }
53+
54+
override predicate isSanitizer(DataFlow::Node node) { node instanceof FilterSanitizer }
55+
56+
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
57+
guard instanceof FilterSanitizerGuard
58+
}
59+
}
60+
}

0 commit comments

Comments
 (0)