|
| 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" |
0 commit comments