Skip to content

Commit a818580

Browse files
author
Gia. Bui Dai
committed
[Javascript] Add CWE-348 ClientSuppliedIpUsedInSecurityCheck
1 parent d4fd878 commit a818580

8 files changed

+633
-0
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>An original client IP address is retrieved from an http header (<code>X-Forwarded-For</code> or <code>X-Real-IP</code> or <code>Proxy-Client-IP</code>
7+
etc.), which is used to ensure security. Attackers can forge the value of these identifiers to
8+
bypass a blacklist, for example.</p>
9+
10+
</overview>
11+
<recommendation>
12+
13+
<p>Do not trust the values of HTTP headers allegedly identifying the originating IP. If you are aware your application will run behind some reverse proxies then the last entry of a <code>X-Forwarded-For</code> header value may be more trustworthy than the rest of it because some reverse proxies append the IP address they observed to the end of any remote-supplied header.</p>
14+
15+
</recommendation>
16+
<example>
17+
18+
<p>The following examples show the bad case and the good case respectively.
19+
In <code>/bad1</code> endpoint, the client IP from the <code>X-Forwarded-For</code> header is used directly to check against a whitelist. In <code>/bad2</code> endpoint, this value is split into multiple IPs, separated by a comma, but the less-trustworthy first one is used. Both of these examples could be deceived by providing a forged HTTP header.
20+
The endpoint <code>/good</code> similarly splits the value from <code>X-Forwarded-For</code> header but uses the last, more trustworthy entry.</p>
21+
22+
<sample src="examples/ClientSuppliedIpUsedInSecurityCheck.js" />
23+
24+
</example>
25+
<references>
26+
27+
<li>Dennis Schneider: <a href="https://www.dennis-schneider.com/blog/prevent-ip-address-spoofing-with-x-forwarded-for-header-and-aws-elb-in-clojure-ring/">
28+
Prevent IP address spoofing with X-Forwarded-For header when using AWS ELB and Clojure Ring</a>
29+
</li>
30+
31+
<li>Security Rule Zero: <a href="https://www.f5.com/company/blog/security-rule-zero-a-warning-about-x-forwarded-for">A Warning about X-Forwarded-For</a>
32+
</li>
33+
34+
</references>
35+
</qhelp>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* @name IP address spoofing
3+
* @description A remote endpoint identifier is read from an HTTP header. Attackers can modify the value
4+
* of the identifier to forge the client ip.
5+
* @kind path-problem
6+
* @problem.severity error
7+
* @precision high
8+
* @id js/ip-address-spoofing
9+
* @tags security
10+
* external/cwe/cwe-348
11+
*/
12+
13+
import javascript
14+
import semmle.javascript.dataflow.DataFlow
15+
import semmle.javascript.dataflow.TaintTracking
16+
import ClientSuppliedIpUsedInSecurityCheckLib
17+
18+
/**
19+
* Taint-tracking configuration tracing flow from obtaining a client ip from an HTTP header to a sensitive use.
20+
*/
21+
class ClientSuppliedIpUsedInSecurityCheckConfig extends TaintTracking::Configuration {
22+
ClientSuppliedIpUsedInSecurityCheckConfig() { this = "ClientSuppliedIpUsedInSecurityCheckConfig" }
23+
24+
override predicate isSource(DataFlow::Node source) { source instanceof ClientSuppliedIp }
25+
26+
override predicate isSink(DataFlow::Node sink) { sink instanceof PossibleSecurityCheck }
27+
28+
/**
29+
* Splitting a header value by `,` and taking an entry other than the first is sanitizing, because
30+
* later entries may originate from more-trustworthy intermediate proxies, not the original client.
31+
*/
32+
override predicate isSanitizer(DataFlow::Node node) {
33+
// ip.split(",").pop(); or var temp = ip.split(","); ip = temp.pop();
34+
exists(DataFlow::MethodCallNode split, DataFlow::MethodCallNode pop |
35+
split = node and
36+
split.getMethodName() = "split" and
37+
pop.getMethodName() = "pop" and
38+
split.getACall() = pop
39+
)
40+
or
41+
// ip.split(",")[ip.split(",").length - 1]; or var temp = ip.split(","); ip = temp[temp.length - 1];
42+
exists(DataFlow::MethodCallNode split, DataFlow::PropRead read |
43+
split = node and
44+
split.getMethodName() = "split" and
45+
read = split.getAPropertyRead() and
46+
not read.getPropertyNameExpr().getIntValue() = 0
47+
)
48+
}
49+
}
50+
51+
from
52+
ClientSuppliedIpUsedInSecurityCheckConfig config, DataFlow::PathNode source,
53+
DataFlow::PathNode sink
54+
where config.hasFlowPath(source, sink)
55+
select sink.getNode(), source, sink, "IP address spoofing might include code from $@.",
56+
source.getNode(), "this user input"
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
private import javascript
2+
private import DataFlow::PathGraph
3+
private import semmle.javascript.dataflow.DataFlow
4+
5+
/**
6+
* A data flow source of the client ip obtained according to the remote endpoint identifier specified
7+
* (`X-Forwarded-For`, `X-Real-IP`, `Proxy-Client-IP`, etc.) in the header.
8+
*
9+
* For example: `req.headers["X-Forwarded-For"]`.
10+
*/
11+
abstract class ClientSuppliedIp extends DataFlow::Node { }
12+
13+
private class GenericClientSuppliedIp extends ClientSuppliedIp {
14+
GenericClientSuppliedIp() {
15+
exists(DataFlow::SourceNode source, DataFlow::PropRead read |
16+
this.(RemoteFlowSource).getSourceType() = "Server request header" and source = this
17+
|
18+
read.getPropertyName().toLowerCase() = clientIpParameterName() and
19+
source.flowsTo(read)
20+
)
21+
}
22+
}
23+
24+
private string clientIpParameterName() {
25+
result in [
26+
"x-forwarded-for", "x_forwarded_for", "x-real-ip", "x_real_ip", "proxy-client-ip",
27+
"proxy_client_ip", "wl-proxy-client-ip", "wl_proxy_client_ip", "http_x_forwarded_for",
28+
"http-x-forwarded-for", "http_x_forwarded", "http_x_cluster_client_ip", "http_client_ip",
29+
"http_forwarded_for", "http_forwarded", "http_via", "remote_addr"
30+
]
31+
}
32+
33+
/** A data flow sink for ip address forgery vulnerabilities. */
34+
abstract class PossibleSecurityCheck extends DataFlow::Node { }
35+
36+
/**
37+
* A data flow sink for remote client ip comparison.
38+
*
39+
* For example: `if !ip.startsWith("10.")` determine whether the client ip starts
40+
* with `10.`, and the program can be deceived by forging the ip address.
41+
*/
42+
private class CompareSink extends PossibleSecurityCheck {
43+
CompareSink() {
44+
// ip.startsWith("10.") or ip.includes("10.")
45+
exists(CallExpr call |
46+
call.getAChild() = this.asExpr() and
47+
call.getCalleeName() in ["startsWith", "includes"] and
48+
call.getArgument(0).getStringValue().regexpMatch(getIpAddressRegex()) and
49+
not call.getArgument(0).getStringValue() = "0:0:0:0:0:0:0:1"
50+
)
51+
or
52+
// ip === "127.0.0.1" or ip !== "127.0.0.1" or ip == "127.0.0.1" or or ip != "127.0.0.1"
53+
exists(Comparison compare |
54+
compare instanceof EqualityTest and
55+
(
56+
[compare, compare.getAnOperand()] = this.asExpr() and
57+
compare.getAnOperand().getStringValue() instanceof PrivateHostName and
58+
not compare.getAnOperand().getStringValue() = "0:0:0:0:0:0:0:1"
59+
)
60+
)
61+
}
62+
}
63+
64+
string getIpAddressRegex() {
65+
result =
66+
"^((10\\.((1\\d{2})?|(2[0-4]\\d)?|(25[0-5])?|([1-9]\\d|[0-9])?)(\\.)?)|(192\\.168\\.)|172\\.(1[6789]|2[0-9]|3[01])\\.)((1\\d{2})?|(2[0-4]\\d)?|(25[0-5])?|([1-9]\\d|[0-9])?)(\\.)?((1\\d{2})?|(2[0-4]\\d)?|(25[0-5])?|([1-9]\\d|[0-9])?)$"
67+
}
68+
69+
/**
70+
* A string matching private host names of IPv4 and IPv6, which only matches the host portion therefore checking for port is not necessary.
71+
* Several examples are localhost, reserved IPv4 IP addresses including 127.0.0.1, 10.x.x.x, 172.16.x,x, 192.168.x,x, and reserved IPv6 addresses including [0:0:0:0:0:0:0:1] and [::1]
72+
*/
73+
private class PrivateHostName extends string {
74+
bindingset[this]
75+
PrivateHostName() {
76+
this.regexpMatch("(?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\\]?(?:[:/?#].*)?")
77+
}
78+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
const express = require("express");
2+
const app = express();
3+
const port = 3000;
4+
5+
app.get("/bad1", (req, res) => {
6+
var ip = req.headers["x-forwarded-for"];
7+
8+
// Bad: use this value directly
9+
if (ip && ip === "127.0.0.1") {
10+
res.writeHead(200);
11+
return res.end("Hello, World!");
12+
}
13+
14+
res.writeHead(403);
15+
res.end("illegal ip");
16+
});
17+
18+
app.get("/bad2", (req, res) => {
19+
var ip = req.headers["x-forwarded-for"];
20+
if (!ip) {
21+
res.writeHead(403);
22+
return res.end("illegal ip");
23+
}
24+
25+
// Bad: the first IP is used
26+
var temp = ip.split(",");
27+
ip = temp[0];
28+
29+
if (ip && ip === "127.0.0.1") {
30+
res.writeHead(200);
31+
return res.end("Hello, World!");
32+
}
33+
34+
res.writeHead(403);
35+
res.end("illegal ip");
36+
});
37+
38+
app.get("/good", (req, res) => {
39+
var ip = req.headers["x-forwarded-for"];
40+
if (!ip) {
41+
res.writeHead(403);
42+
return res.end("illegal ip");
43+
}
44+
45+
// Good: if this application runs behind a reverse proxy it may append the real remote IP to the end of any client-supplied X-Forwarded-For header.
46+
var temp = ip.split(",");
47+
ip = temp[temp.length - 1];
48+
49+
if (ip && ip === "127.0.0.1") {
50+
res.writeHead(200);
51+
return res.end("Hello, World!");
52+
}
53+
54+
res.writeHead(403);
55+
res.end("illegal ip");
56+
});
57+
58+
app.listen(port, () => {
59+
console.log(`Example app listening at http://localhost:${port}`);
60+
});

0 commit comments

Comments
 (0)