Skip to content

Commit ed54fdc

Browse files
authored
Merge pull request #4118 from dellalibera/js/ldap
[javascript] CodeQL to detect LDAP Injection
2 parents d56ea22 + 116e7d0 commit ed54fdc

File tree

9 files changed

+362
-0
lines changed

9 files changed

+362
-0
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>If an LDAP query is built using string concatenation or string formatting, and the
7+
components of the concatenation include user input without any proper sanitization, a user
8+
is likely to be able to run malicious LDAP queries.</p>
9+
</overview>
10+
11+
<recommendation>
12+
<p>If user input must be included in an LDAP query, it should be escaped to
13+
avoid a malicious user providing special characters that change the meaning
14+
of the query. In NodeJS, it is possible to build the LDAP query using frameworks like <code>ldapjs</code>.
15+
The library provides a <code>Filter API</code>, however it's still possibile to pass a string version of an LDAP filter.
16+
A good practice is to escape filter characters that could change the meaning of the query (https://tools.ietf.org/search/rfc4515#section-3).</p>
17+
</recommendation>
18+
19+
<example>
20+
<p>In the following examples, the code accepts a <code>username</code> from the user, which it uses in a LDAP query.</p>
21+
22+
<p>The first and the second example uses the unsanitized user input directly
23+
in the search filter for the LDAP query.
24+
A malicious user could provide special characters to change the meaning of these
25+
queries, and search for a completely different set of values.
26+
</p>
27+
<sample src="examples/example_bad1.js" />
28+
<sample src="examples/example_bad2.js" />
29+
30+
31+
<p>In the third example the <code>username</code> is sanitized before it is included in the search filters.
32+
This ensures the meaning of the query cannot be changed by a malicious user.</p>
33+
34+
<sample src="examples/example_good1.js" />
35+
36+
<p>In the fourth example the <code>username</code> is passed to an <code>OrFilter</code> filter before it is included in the search filters.
37+
This ensures the meaning of the query cannot be changed by a malicious user.</p>
38+
39+
<sample src="examples/example_good2.js" />
40+
</example>
41+
42+
<references>
43+
<li>OWASP: <a href="https://cheatsheetseries.owasp.org/cheatsheets/LDAP_Injection_Prevention_Cheat_Sheet.html">LDAP Injection Prevention Cheat Sheet</a>.</li>
44+
<li>LDAPjs: <a href="http://ldapjs.org/index.html">Documentation for LDAPjs</a>.</li>
45+
<li>Github: <a href="https://github.com/ldapjs/node-ldapjs">ldapjs</a>.</li>
46+
<li>Wikipedia: <a href="https://en.wikipedia.org/wiki/LDAP_injection">LDAP injection</a>.</li>
47+
<li>BlackHat: <a href="https://www.blackhat.com/presentations/bh-europe-08/Alonso-Parada/Whitepaper/bh-eu-08-alonso-parada-WP.pdf">LDAP Injection and Blind LDAP Injection</a>.</li>
48+
<li>LDAP: <a href="https://ldap.com/2018/05/04/understanding-and-defending-against-ldap-injection-attacks/">Understanding and Defending Against LDAP Injection Attacks</a>.</li>
49+
</references>
50+
</qhelp>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @name LDAP query built from user-controlled sources
3+
* @description Building an LDAP query from user-controlled sources is vulnerable to insertion of
4+
* malicious LDAP code by the user.
5+
* @kind path-problem
6+
* @problem.severity error
7+
* @precision high
8+
* @id javascript/ldap-injection
9+
* @tags security
10+
* external/cwe/cwe-090
11+
*/
12+
13+
import javascript
14+
import DataFlow::PathGraph
15+
import LdapInjection::LdapInjection
16+
17+
from LdapInjectionConfiguration config, DataFlow::PathNode source, DataFlow::PathNode sink
18+
where config.hasFlowPath(source, sink)
19+
select sink.getNode(), source, sink, "$@ might include code from $@.",
20+
sink.getNode().(Sink).getQueryCall(), "LDAP query call", source.getNode(), "user-provided value"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import javascript
2+
3+
module LdapInjection {
4+
import LdapInjectionCustomizations::LdapInjection
5+
6+
/**
7+
* A taint-tracking configuration for reasoning about LDAP injection vulnerabilities.
8+
*/
9+
class LdapInjectionConfiguration extends TaintTracking::Configuration {
10+
LdapInjectionConfiguration() { this = "LdapInjection" }
11+
12+
override predicate isSource(DataFlow::Node source) { source instanceof Source }
13+
14+
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
15+
16+
override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer }
17+
}
18+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Provides default sources, sinks and sanitizers for reasoning about
3+
* LDAP injection vulnerabilities, as well as extension points for
4+
* adding your own.
5+
*/
6+
7+
import javascript
8+
import Ldapjs::Ldapjs
9+
10+
module LdapInjection {
11+
/**
12+
* A data flow source for LDAP injection vulnerabilities.
13+
*/
14+
abstract class Source extends DataFlow::Node { }
15+
16+
/**
17+
* A data flow sink for LDAP injection vulnerabilities.
18+
*/
19+
abstract class Sink extends DataFlow::Node {
20+
/**
21+
* Gets the LDAP query call that the sink flows into.
22+
*/
23+
abstract DataFlow::Node getQueryCall();
24+
}
25+
26+
/**
27+
* A sanitizer for LDAP injection vulnerabilities.
28+
*/
29+
abstract class Sanitizer extends DataFlow::Node { }
30+
31+
/**
32+
* A source of remote user input, considered as a flow source for LDAP injection.
33+
*/
34+
class RemoteSource extends Source {
35+
RemoteSource() { this instanceof RemoteFlowSource }
36+
}
37+
38+
/**
39+
* An LDAP filter for an API call that executes an operation against the LDAP server.
40+
*/
41+
class LdapjsSearchFilterAsSink extends Sink {
42+
LdapjsSearchFilterAsSink() { this instanceof LdapjsSearchFilter }
43+
44+
override DataFlow::InvokeNode getQueryCall() {
45+
result = this.(LdapjsSearchFilter).getQueryCall()
46+
}
47+
}
48+
49+
/**
50+
* An LDAP DN argument for an API call that executes an operation against the LDAP server.
51+
*/
52+
class LdapjsDNArgumentAsSink extends Sink {
53+
LdapjsDNArgumentAsSink() { this instanceof LdapjsDNArgument }
54+
55+
override DataFlow::InvokeNode getQueryCall() { result = this.(LdapjsDNArgument).getQueryCall() }
56+
}
57+
58+
/**
59+
* A call to a function whose name suggests that it escapes LDAP search query parameter.
60+
*/
61+
class FilterOrDNSanitizationCall extends Sanitizer, DataFlow::CallNode {
62+
FilterOrDNSanitizationCall() {
63+
exists(string sanitize, string input |
64+
sanitize = "(?:escape|saniti[sz]e|validate|filter)" and
65+
input = "[Ii]nput?"
66+
|
67+
this
68+
.getCalleeName()
69+
.regexpMatch("(?i)(" + sanitize + input + ")" + "|(" + input + sanitize + ")")
70+
)
71+
}
72+
}
73+
74+
/**
75+
* A step through the parseFilter API (https://github.com/ldapjs/node-ldapjs/issues/181).
76+
*/
77+
class StepThroughParseFilter extends TaintTracking::AdditionalTaintStep, DataFlow::CallNode {
78+
StepThroughParseFilter() { this instanceof LdapjsParseFilter }
79+
80+
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
81+
pred = this.getArgument(0) and
82+
succ = this
83+
}
84+
}
85+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* Provides classes for working with [ldapjs](https://github.com/ldapjs/node-ldapjs) (Client only)
3+
*/
4+
5+
import javascript
6+
7+
module Ldapjs {
8+
/**
9+
* Gets a method name on an LDAPjs client that accepts a DN as the first argument.
10+
*/
11+
private string getLdapjsClientDNMethodName() {
12+
result = ["add", "bind", "compare", "del", "modify", "modifyDN", "search"]
13+
}
14+
15+
/**
16+
* Gets a data flow source node for an LDAP client.
17+
*/
18+
abstract class LdapClient extends DataFlow::SourceNode { }
19+
20+
/**
21+
* Gets a data flow source node for the ldapjs library.
22+
*/
23+
private DataFlow::SourceNode ldapjs() { result = DataFlow::moduleImport("ldapjs") }
24+
25+
/**
26+
* Gets a data flow source node for the ldapjs client.
27+
*/
28+
class LdapjsClient extends LdapClient {
29+
LdapjsClient() { this = ldapjs().getAMemberCall("createClient") }
30+
}
31+
32+
/**
33+
* Gets a data flow node for the client `search` options.
34+
*/
35+
class LdapjsSearchOptions extends DataFlow::SourceNode {
36+
DataFlow::CallNode queryCall;
37+
38+
LdapjsSearchOptions() {
39+
queryCall = any(LdapjsClient client).getAMemberCall("search") and
40+
this = queryCall.getArgument(1).getALocalSource()
41+
}
42+
43+
/**
44+
* Gets the LDAP query call that these options are used in.
45+
*/
46+
DataFlow::InvokeNode getQueryCall() { result = queryCall }
47+
}
48+
49+
/**
50+
* A filter used in a `search` operation against the LDAP server.
51+
*/
52+
class LdapjsSearchFilter extends DataFlow::Node {
53+
LdapjsSearchOptions options;
54+
55+
LdapjsSearchFilter() { this = options.getAPropertyWrite("filter").getRhs() }
56+
57+
/**
58+
* Gets the LDAP query call that this filter is used in.
59+
*/
60+
DataFlow::InvokeNode getQueryCall() { result = options.getQueryCall() }
61+
}
62+
63+
/**
64+
* A call to the ldapjs Client API methods.
65+
*/
66+
class LdapjsClientAPICall extends DataFlow::CallNode {
67+
LdapjsClientAPICall() {
68+
this = any(LdapjsClient client).getAMemberCall(getLdapjsClientDNMethodName())
69+
}
70+
}
71+
72+
/**
73+
* A distinguished name (DN) used in a Client API call against the LDAP server.
74+
*/
75+
class LdapjsDNArgument extends DataFlow::Node {
76+
LdapjsClientAPICall queryCall;
77+
78+
LdapjsDNArgument() { this = queryCall.getArgument(0) }
79+
80+
/**
81+
* Gets the LDAP query call that this DN is used in.
82+
*/
83+
DataFlow::InvokeNode getQueryCall() { result = queryCall }
84+
}
85+
86+
/**
87+
* Ldapjs parseFilter method call.
88+
*/
89+
class LdapjsParseFilter extends DataFlow::CallNode {
90+
LdapjsParseFilter() { this = ldapjs().getAMemberCall("parseFilter") }
91+
}
92+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const http = require('http');
2+
const url = require('url');
3+
const ldap = require('ldapjs');
4+
const client = ldap.createClient({
5+
url: 'ldap://127.0.0.1:1389'
6+
});
7+
8+
const server = http.createServer((req, res) => {
9+
let q = url.parse(req.url, true);
10+
11+
let username = q.query.username;
12+
13+
var opts = {
14+
// BAD
15+
filter: `(|(name=${username})(username=${username}))`
16+
};
17+
18+
client.search('o=example', opts, function (err, res) {
19+
20+
});
21+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const http = require('http');
2+
const url = require('url');
3+
const ldap = require('ldapjs');
4+
const client = ldap.createClient({
5+
url: 'ldap://127.0.0.1:1389'
6+
});
7+
8+
const server = http.createServer((req, res) => {
9+
let q = url.parse(req.url, true);
10+
11+
let username = q.query.username;
12+
13+
// BAD
14+
client.search('o=example', { filter: `(|(name=${username})(username=${username}))` }, function (err, res) {
15+
});
16+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const http = require('http');
2+
const url = require('url');
3+
const ldap = require('ldapjs');
4+
const client = ldap.createClient({
5+
url: 'ldap://127.0.0.1:1389'
6+
});
7+
8+
9+
// https://github.com/vesse/node-ldapauth-fork/commit/3feea43e243698bcaeffa904a7324f4d96df60e4
10+
const sanitizeInput = function (input) {
11+
return input
12+
.replace(/\*/g, '\\2a')
13+
.replace(/\(/g, '\\28')
14+
.replace(/\)/g, '\\29')
15+
.replace(/\\/g, '\\5c')
16+
.replace(/\0/g, '\\00')
17+
.replace(/\//g, '\\2f');
18+
};
19+
20+
const server = http.createServer((req, res) => {
21+
let q = url.parse(req.url, true);
22+
23+
let username = q.query.username;
24+
25+
// GOOD
26+
username = sanitizeInput(username);
27+
28+
client.search('o=example', { filter: `(|(name=${username})(username=${username}))` }, function (err, res) {
29+
});
30+
31+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const http = require('http');
2+
const url = require('url');
3+
const ldap = require('ldapjs');
4+
const client = ldap.createClient({
5+
url: 'ldap://127.0.0.1:1389'
6+
});
7+
8+
const server = http.createServer((req, res) => {
9+
let q = url.parse(req.url, true);
10+
11+
let username = q.query.username;
12+
13+
// GOOD (https://github.com/ldapjs/node-ldapjs/issues/181)
14+
let f = new OrFilter({
15+
filters: [
16+
new EqualityFilter({
17+
attribute: 'name',
18+
value: username
19+
}),
20+
new EqualityFilter({
21+
attribute: 'username',
22+
value: username
23+
})
24+
]
25+
});
26+
27+
client.search('o=example', { filter: f }, function (err, res) {
28+
});
29+
});

0 commit comments

Comments
 (0)