Skip to content

Commit 7de9214

Browse files
committed
Upload LDAP Insecure authentication query and tests
1 parent 6bab41c commit 7de9214

File tree

5 files changed

+574
-0
lines changed

5 files changed

+574
-0
lines changed
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/**
2+
* @name Python Insecure LDAP Authentication
3+
* @description Python LDAP Insecure LDAP Authentication
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @id python/insecure-ldap-auth
7+
* @tags experimental
8+
* security
9+
* external/cwe/cwe-090
10+
*/
11+
12+
import python
13+
import semmle.python.dataflow.new.RemoteFlowSources
14+
import semmle.python.dataflow.new.DataFlow
15+
import semmle.python.dataflow.new.TaintTracking
16+
import semmle.python.dataflow.new.internal.TaintTrackingPublic
17+
import DataFlow::PathGraph
18+
19+
class FalseArg extends ControlFlowNode {
20+
FalseArg() { this.getNode().(Expr).(BooleanLiteral) instanceof False }
21+
}
22+
23+
// From luchua-bc's Insecure LDAP authentication in Java (to reduce false positives)
24+
string getFullHostRegex() { result = "(?i)ldap://[\\[a-zA-Z0-9].*" }
25+
26+
string getSchemaRegex() { result = "(?i)ldap(://)?" }
27+
28+
string getPrivateHostRegex() {
29+
result =
30+
"(?i)localhost(?:[:/?#].*)?|127\\.0\\.0\\.1(?:[:/?#].*)?|10(?:\\.[0-9]+){3}(?:[:/?#].*)?|172\\.16(?:\\.[0-9]+){2}(?:[:/?#].*)?|192.168(?:\\.[0-9]+){2}(?:[:/?#].*)?|\\[?0:0:0:0:0:0:0:1\\]?(?:[:/?#].*)?|\\[?::1\\]?(?:[:/?#].*)?"
31+
}
32+
33+
// "ldap://somethingon.theinternet.com"
34+
class LDAPFullHost extends StrConst {
35+
LDAPFullHost() {
36+
exists(string s |
37+
s = this.getText() and
38+
s.regexpMatch(getFullHostRegex()) and
39+
not s.substring(7, s.length()).regexpMatch(getPrivateHostRegex()) // No need to check for ldaps, would be SSL by default.
40+
)
41+
}
42+
}
43+
44+
class LDAPSchema extends StrConst {
45+
LDAPSchema() { this.getText().regexpMatch(getSchemaRegex()) }
46+
}
47+
48+
class LDAPPrivateHost extends StrConst {
49+
LDAPPrivateHost() { this.getText().regexpMatch(getPrivateHostRegex()) }
50+
}
51+
52+
predicate concatAndCompareAgainstFullHostRegex(Expr schema, Expr host) {
53+
schema instanceof LDAPSchema and
54+
not host instanceof LDAPPrivateHost and
55+
exists(string full_host |
56+
full_host = schema.(StrConst).getText() + host.(StrConst).getText() and
57+
full_host.regexpMatch(getFullHostRegex())
58+
)
59+
}
60+
61+
// "ldap://" + "somethingon.theinternet.com"
62+
class LDAPBothStrings extends BinaryExpr {
63+
LDAPBothStrings() { concatAndCompareAgainstFullHostRegex(this.getLeft(), this.getRight()) }
64+
}
65+
66+
// schema + host
67+
class LDAPBothVar extends BinaryExpr {
68+
LDAPBothVar() {
69+
exists(SsaVariable schemaVar, SsaVariable hostVar |
70+
this.getLeft() = schemaVar.getVariable().getALoad() and // getAUse is incompatible with Expr
71+
this.getRight() = hostVar.getVariable().getALoad() and
72+
concatAndCompareAgainstFullHostRegex(schemaVar
73+
.getDefinition()
74+
.getImmediateDominator()
75+
.getNode(), hostVar.getDefinition().getImmediateDominator().getNode())
76+
)
77+
}
78+
}
79+
80+
// schema + "somethingon.theinternet.com"
81+
class LDAPVarString extends BinaryExpr {
82+
LDAPVarString() {
83+
exists(SsaVariable schemaVar |
84+
this.getLeft() = schemaVar.getVariable().getALoad() and
85+
concatAndCompareAgainstFullHostRegex(schemaVar
86+
.getDefinition()
87+
.getImmediateDominator()
88+
.getNode(), this.getRight())
89+
)
90+
}
91+
}
92+
93+
// "ldap://" + host
94+
class LDAPStringVar extends BinaryExpr {
95+
LDAPStringVar() {
96+
exists(SsaVariable hostVar |
97+
this.getRight() = hostVar.getVariable().getALoad() and
98+
concatAndCompareAgainstFullHostRegex(this.getLeft(),
99+
hostVar.getDefinition().getImmediateDominator().getNode())
100+
)
101+
}
102+
}
103+
104+
class LDAPInsecureAuthSource extends DataFlow::Node {
105+
LDAPInsecureAuthSource() {
106+
this instanceof RemoteFlowSource or
107+
this.asExpr() instanceof LDAPBothStrings or
108+
this.asExpr() instanceof LDAPBothVar or
109+
this.asExpr() instanceof LDAPVarString or
110+
this.asExpr() instanceof LDAPStringVar
111+
}
112+
}
113+
114+
class SafeLDAPOptions extends ControlFlowNode {
115+
SafeLDAPOptions() {
116+
this = Value::named("ldap.OPT_X_TLS_ALLOW").getAReference() or
117+
this = Value::named("ldap.OPT_X_TLS_TRY").getAReference() or
118+
this = Value::named("ldap.OPT_X_TLS_DEMAND").getAReference() or
119+
this = Value::named("ldap.OPT_X_TLS_HARD").getAReference()
120+
}
121+
}
122+
123+
// LDAP3
124+
class LDAPInsecureAuthSink extends DataFlow::Node {
125+
LDAPInsecureAuthSink() {
126+
exists(SsaVariable connVar, CallNode connCall, SsaVariable srvVar, CallNode srvCall |
127+
// set connCall as a Call to ldap3.Connection
128+
connCall = Value::named("ldap3.Connection").getACall() and
129+
// get variable whose definition is a call to ldap3.Connection to correlate ldap3.Server and Connection.start_tls()
130+
connVar.getDefinition().getImmediateDominator() = connCall and
131+
// get connCall's first argument variable definition
132+
srvVar.getAUse() = connCall.getArg(0) and
133+
/*
134+
* // restrict srvVar definition to a ldap3.Server Call
135+
* srvCall = Value::named("ldap3.Server").getACall() and
136+
* srvVar.getDefinition().getImmediateDominator() = srvCall
137+
* // redundant? ldap3.Connection's first argument *must* be ldap3.Server
138+
*/
139+
140+
// set srvCall as srvVar definition's call
141+
srvVar.getDefinition().getImmediateDominator() = srvCall and
142+
// set ldap3.Server's 1st argument as sink
143+
this.asExpr() = srvCall.getArg(0).getNode() and
144+
(
145+
// check ldap3.Server call's 3rd argument (positional) is null and there's no use_ssl
146+
count(srvCall.getAnArg()) < 3 and
147+
count(srvCall.getArgByName("use_ssl")) = 0
148+
or
149+
// check ldap3.Server call's 3rd argument is False
150+
srvCall.getAnArg() instanceof FalseArg
151+
or
152+
// check ldap3.Server argByName "use_ssl" is False
153+
srvCall.getArgByName("use_ssl") instanceof FalseArg
154+
) and
155+
/*
156+
* Avoid flow through any function (Server()) whose variable declaring it (srv) is the first
157+
* argument in any function (Connection()) whose variable declaring it also calls .start_tls
158+
*/
159+
160+
/*
161+
* host = schema + "somethingon.theinternet.com"
162+
* srv = Server(host, port = 1337)
163+
* conn = Connection(srv, "dn", "password")
164+
* conn.start_tls() !
165+
*/
166+
167+
not connVar
168+
.getAUse()
169+
.getImmediateDominator()
170+
.(CallNode)
171+
.getNode()
172+
.getFunc()
173+
.(Attribute)
174+
.getName()
175+
.matches("start_tls")
176+
)
177+
}
178+
}
179+
180+
class LDAPInsecureAuthConfig extends TaintTracking::Configuration {
181+
LDAPInsecureAuthConfig() { this = "LDAPInsecureAuthConfig" }
182+
183+
override predicate isSource(DataFlow::Node source) { source instanceof LDAPInsecureAuthSource }
184+
185+
override predicate isSink(DataFlow::Node sink) { sink instanceof LDAPInsecureAuthSink }
186+
}
187+
188+
from LDAPInsecureAuthConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
189+
where config.hasFlowPath(source, sink)
190+
select sink.getNode(), source, sink, "$@ from $@ is authenticated insecurely.", source.getNode(),
191+
"The host", sink.getNode(), "this LDAP query"
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import ldap
2+
from flask import request, Flask
3+
4+
app = Flask(__name__)
5+
6+
schema = "ldap://"
7+
remote_host = "127.0.0.1"
8+
full_host = "ldap://127.0.0.1"
9+
10+
11+
# GOOD
12+
13+
# SSL through ldap variable option
14+
# host from variable + variable
15+
16+
@app.route("/one")
17+
def one():
18+
ldap_connection_1 = ldap.initialize(schema + remote_host)
19+
ldap_connection_1.set_option(ldap.OPT_X_TLS_DEMAND, True)
20+
ldap_connection_1.simple_bind_s('', '')
21+
user = ldap_connection_1.search_s(
22+
"dn", ldap.SCOPE_SUBTREE, "search_filter")
23+
24+
return user
25+
26+
27+
# SSL through ldap start_tls_s
28+
# hardcoded host
29+
30+
@app.route("/two")
31+
def two():
32+
ldap_connection_2 = ldap.initialize("ldap://127.0.0.1")
33+
ldap_connection_2.start_tls_s()
34+
ldap_connection_2.simple_bind_s('', '')
35+
user = ldap_connection_2.search_s(
36+
"dn", ldap.SCOPE_SUBTREE, "search_filter")
37+
38+
return user
39+
40+
41+
# BAD (not a sink because it's private)
42+
43+
@app.route("/one_bad")
44+
def one_bad():
45+
ldap_connection_3 = ldap.initialize(schema + remote_host)
46+
ldap_connection_3.set_option(ldap.OPT_X_TLS_DEMAND, False)
47+
ldap_connection_3.simple_bind_s('', '')
48+
user = ldap_connection_3.search_s(
49+
"dn", ldap.SCOPE_SUBTREE, "search_filter")
50+
51+
return user
52+
53+
54+
@app.route("/one_bad_2")
55+
def one_bad_2():
56+
ldap_connection_4 = ldap.initialize(schema + remote_host)
57+
ldap_connection_4.set_option(ldap.OPT_X_TLS_NEVER)
58+
ldap_connection_4.simple_bind_s('', '')
59+
user = ldap_connection_4.search_s(
60+
"dn", ldap.SCOPE_SUBTREE, "search_filter")
61+
62+
return user
63+
64+
65+
# if __name__ == "__main__":
66+
# app.run(debug=True)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import ldap
2+
from flask import request, Flask
3+
4+
app = Flask(__name__)
5+
6+
schema = "ldap://"
7+
remote_host = "somethingon.theinternet.com"
8+
full_host = "ldap://somethingon.theinternet.com"
9+
10+
11+
# GOOD
12+
13+
# SSL through ldap variable option
14+
# host from variable + variable
15+
16+
@app.route("/one")
17+
def one():
18+
ldap_connection_5 = ldap.initialize(schema + remote_host)
19+
ldap_connection_5.set_option(ldap.OPT_X_TLS_DEMAND, True)
20+
ldap_connection_5.simple_bind_s('', '')
21+
user = ldap_connection_5.search_s(
22+
"dn", ldap.SCOPE_SUBTREE, "search_filter")
23+
24+
return user
25+
26+
27+
# SSL through ldap start_tls_s
28+
# hardcoded host
29+
30+
@app.route("/two")
31+
def two():
32+
ldap_connection_6 = ldap.initialize("ldap://somethingon.theinternet.com")
33+
ldap_connection_6.start_tls_s()
34+
ldap_connection_6.simple_bind_s('', '')
35+
user = ldap_connection_6.search_s(
36+
"dn", ldap.SCOPE_SUBTREE, "search_filter")
37+
38+
return user
39+
40+
41+
# BAD
42+
43+
@app.route("/one_bad")
44+
def one_bad():
45+
ldap_connection_7 = ldap.initialize(schema + remote_host)
46+
ldap_connection_7.set_option(ldap.OPT_X_TLS_DEMAND, False)
47+
ldap_connection_7.simple_bind_s('', '')
48+
user = ldap_connection_7.search_s(
49+
"dn", ldap.SCOPE_SUBTREE, "search_filter")
50+
51+
return user
52+
53+
54+
@app.route("/one_bad_2")
55+
def one_bad_2():
56+
ldap_connection_8 = ldap.initialize(schema + remote_host)
57+
ldap_connection_8.set_option(ldap.OPT_X_TLS_NEVER)
58+
ldap_connection_8.simple_bind_s('', '')
59+
user = ldap_connection_8.search_s(
60+
"dn", ldap.SCOPE_SUBTREE, "search_filter")
61+
62+
return user
63+
64+
65+
# if __name__ == "__main__":
66+
# app.run(debug=True)

0 commit comments

Comments
 (0)