[Javascript] Add CWE-348 ClientSuppliedIpUsedInSecurityCheck

This commit is contained in:
Gia. Bui Dai
2021-11-15 14:13:06 +07:00
committed by Esben Sparre Andreasen
parent a73cdf3527
commit 4ee36c51eb
8 changed files with 633 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<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>
etc.), which is used to ensure security. Attackers can forge the value of these identifiers to
bypass a blacklist, for example.</p>
</overview>
<recommendation>
<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>
</recommendation>
<example>
<p>The following examples show the bad case and the good case respectively.
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.
The endpoint <code>/good</code> similarly splits the value from <code>X-Forwarded-For</code> header but uses the last, more trustworthy entry.</p>
<sample src="examples/ClientSuppliedIpUsedInSecurityCheck.js" />
</example>
<references>
<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/">
Prevent IP address spoofing with X-Forwarded-For header when using AWS ELB and Clojure Ring</a>
</li>
<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>
</li>
</references>
</qhelp>

View File

@@ -0,0 +1,56 @@
/**
* @name IP address spoofing
* @description A remote endpoint identifier is read from an HTTP header. Attackers can modify the value
* of the identifier to forge the client ip.
* @kind path-problem
* @problem.severity error
* @precision high
* @id js/ip-address-spoofing
* @tags security
* external/cwe/cwe-348
*/
import javascript
import semmle.javascript.dataflow.DataFlow
import semmle.javascript.dataflow.TaintTracking
import ClientSuppliedIpUsedInSecurityCheckLib
/**
* Taint-tracking configuration tracing flow from obtaining a client ip from an HTTP header to a sensitive use.
*/
class ClientSuppliedIpUsedInSecurityCheckConfig extends TaintTracking::Configuration {
ClientSuppliedIpUsedInSecurityCheckConfig() { this = "ClientSuppliedIpUsedInSecurityCheckConfig" }
override predicate isSource(DataFlow::Node source) { source instanceof ClientSuppliedIp }
override predicate isSink(DataFlow::Node sink) { sink instanceof PossibleSecurityCheck }
/**
* Splitting a header value by `,` and taking an entry other than the first is sanitizing, because
* later entries may originate from more-trustworthy intermediate proxies, not the original client.
*/
override predicate isSanitizer(DataFlow::Node node) {
// ip.split(",").pop(); or var temp = ip.split(","); ip = temp.pop();
exists(DataFlow::MethodCallNode split, DataFlow::MethodCallNode pop |
split = node and
split.getMethodName() = "split" and
pop.getMethodName() = "pop" and
split.getACall() = pop
)
or
// ip.split(",")[ip.split(",").length - 1]; or var temp = ip.split(","); ip = temp[temp.length - 1];
exists(DataFlow::MethodCallNode split, DataFlow::PropRead read |
split = node and
split.getMethodName() = "split" and
read = split.getAPropertyRead() and
not read.getPropertyNameExpr().getIntValue() = 0
)
}
}
from
ClientSuppliedIpUsedInSecurityCheckConfig config, DataFlow::PathNode source,
DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "IP address spoofing might include code from $@.",
source.getNode(), "this user input"

View File

@@ -0,0 +1,78 @@
private import javascript
private import DataFlow::PathGraph
private import semmle.javascript.dataflow.DataFlow
/**
* A data flow source of the client ip obtained according to the remote endpoint identifier specified
* (`X-Forwarded-For`, `X-Real-IP`, `Proxy-Client-IP`, etc.) in the header.
*
* For example: `req.headers["X-Forwarded-For"]`.
*/
abstract class ClientSuppliedIp extends DataFlow::Node { }
private class GenericClientSuppliedIp extends ClientSuppliedIp {
GenericClientSuppliedIp() {
exists(DataFlow::SourceNode source, DataFlow::PropRead read |
this.(RemoteFlowSource).getSourceType() = "Server request header" and source = this
|
read.getPropertyName().toLowerCase() = clientIpParameterName() and
source.flowsTo(read)
)
}
}
private string clientIpParameterName() {
result in [
"x-forwarded-for", "x_forwarded_for", "x-real-ip", "x_real_ip", "proxy-client-ip",
"proxy_client_ip", "wl-proxy-client-ip", "wl_proxy_client_ip", "http_x_forwarded_for",
"http-x-forwarded-for", "http_x_forwarded", "http_x_cluster_client_ip", "http_client_ip",
"http_forwarded_for", "http_forwarded", "http_via", "remote_addr"
]
}
/** A data flow sink for ip address forgery vulnerabilities. */
abstract class PossibleSecurityCheck extends DataFlow::Node { }
/**
* A data flow sink for remote client ip comparison.
*
* For example: `if !ip.startsWith("10.")` determine whether the client ip starts
* with `10.`, and the program can be deceived by forging the ip address.
*/
private class CompareSink extends PossibleSecurityCheck {
CompareSink() {
// ip.startsWith("10.") or ip.includes("10.")
exists(CallExpr call |
call.getAChild() = this.asExpr() and
call.getCalleeName() in ["startsWith", "includes"] and
call.getArgument(0).getStringValue().regexpMatch(getIpAddressRegex()) and
not call.getArgument(0).getStringValue() = "0:0:0:0:0:0:0:1"
)
or
// ip === "127.0.0.1" or ip !== "127.0.0.1" or ip == "127.0.0.1" or or ip != "127.0.0.1"
exists(Comparison compare |
compare instanceof EqualityTest and
(
[compare, compare.getAnOperand()] = this.asExpr() and
compare.getAnOperand().getStringValue() instanceof PrivateHostName and
not compare.getAnOperand().getStringValue() = "0:0:0:0:0:0:0:1"
)
)
}
}
string getIpAddressRegex() {
result =
"^((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])?)$"
}
/**
* A string matching private host names of IPv4 and IPv6, which only matches the host portion therefore checking for port is not necessary.
* 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]
*/
private class PrivateHostName extends string {
bindingset[this]
PrivateHostName() {
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\\]?(?:[:/?#].*)?")
}
}

View File

@@ -0,0 +1,60 @@
const express = require("express");
const app = express();
const port = 3000;
app.get("/bad1", (req, res) => {
var ip = req.headers["x-forwarded-for"];
// Bad: use this value directly
if (ip && ip === "127.0.0.1") {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
res.end("illegal ip");
});
app.get("/bad2", (req, res) => {
var ip = req.headers["x-forwarded-for"];
if (!ip) {
res.writeHead(403);
return res.end("illegal ip");
}
// Bad: the first IP is used
var temp = ip.split(",");
ip = temp[0];
if (ip && ip === "127.0.0.1") {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
res.end("illegal ip");
});
app.get("/good", (req, res) => {
var ip = req.headers["x-forwarded-for"];
if (!ip) {
res.writeHead(403);
return res.end("illegal ip");
}
// 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.
var temp = ip.split(",");
ip = temp[temp.length - 1];
if (ip && ip === "127.0.0.1") {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
res.end("illegal ip");
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});

View File

@@ -0,0 +1,206 @@
const express = require("express");
const app = express();
const port = 3000;
const getClientIPBad = (req) => {
var ip = req.headers["x-forwarded-for"];
return ip;
};
app.get("/bad1", (req, res) => {
var ip = getClientIPBad(req);
if (ip && ip === "127.0.0.1") {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
res.end("illegal ip");
});
app.get("/bad2", (req, res) => {
var ip = req.headers["x-forwarded-for"];
if (ip && ip.startsWith("10.")) {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
res.end("illegal ip");
});
app.get("/bad3", (req, res) => {
var ip = req.headers["x-forwarded-for"];
if (ip && ip.includes("172.16.")) {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
res.end("illegal ip");
});
app.get("/bad4", (req, res) => {
var ip = req.headers["x-forwarded-for"];
if (!ip) {
res.writeHead(403);
return res.end("illegal ip");
}
ip = ip.split(",")[0];
if (ip && ip.includes("172.31.")) {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
res.end("illegal ip");
});
app.get("/bad5", (req, res) => {
var ip = req.headers["x-forwarded-for"];
if (!ip) {
res.writeHead(403);
return res.end("illegal ip");
}
var temp = ip.split(",");
ip = temp[0];
if (ip && ip.includes("192.168.")) {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
res.end("illegal ip");
});
app.get("/false-positive-1", (req, res) => {
var temp = [];
var ip = temp["x-forwarded-for"];
if (ip && ip === "127.0.0.1") {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
res.end("illegal ip");
});
const getClientIPGood = (req) => {
var ip = req.headers["x-forwarded-for"];
if (!ip) {
return "";
}
// 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.
ip = ip.split(",")[ip.split(",").length - 1];
return ip;
};
app.get("/good1", (req, res) => {
var ip = getClientIPGood(req);
if (ip && ip === "127.0.0.1") {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
res.end("illegal ip");
});
app.get("/good2", (req, res) => {
var ip = req.headers["x-forwarded-for"];
if (!ip) {
res.writeHead(403);
return res.end("illegal ip");
}
// 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.
ip = ip.split(",")[ip.split(",").length - 1];
if (ip && ip.startsWith("10.")) {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
res.end("illegal ip");
});
app.get("/good3", (req, res) => {
var ip = req.headers["x-forwarded-for"];
if (!ip) {
res.writeHead(403);
return res.end("illegal ip");
}
// 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.
var temp = ip.split(",");
ip = temp[temp.length - 1];
if (ip && ip.includes("172.16.")) {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
res.end("illegal ip");
});
app.get("/good4", (req, res) => {
var ip = req.headers["x-forwarded-for"];
if (!ip) {
res.writeHead(403);
return res.end("illegal ip");
}
// 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.
ip = ip.split(",").pop();
if (ip && ip.includes("172.31.")) {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
res.end("illegal ip");
});
app.get("/good5", (req, res) => {
var ip = req.headers["x-forwarded-for"];
if (!ip) {
res.writeHead(403);
return res.end("illegal ip");
}
// 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.
var temp = ip.split(",");
ip = temp.pop();
if (ip && ip.includes("192.168.")) {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
res.end("illegal ip");
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});

View File

@@ -0,0 +1,187 @@
const http = require("http");
const getClientIPBad = (req) => {
var ip = req.headers["x-forwarded-for"];
return ip;
};
const getClientIPGood = (req) => {
var ip = req.headers["x-forwarded-for"];
if (!ip) {
return "";
}
// 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.
ip = ip.split(",")[ip.split(",").length - 1];
return ip;
};
const requestListener = function (req, res) {
if (req.url === "/bad1") {
var ip = getClientIPBad(req);
if (ip && ip === "127.0.0.1") {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
return res.end("illegal ip");
} else if (req.url === "/bad2") {
var ip = req.headers["x-forwarded-for"];
if (ip && ip.startsWith("10.")) {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
return res.end("illegal ip");
} else if (req.url === "/bad3") {
var ip = req.headers["x-forwarded-for"];
if (ip && ip.includes("172.16.")) {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
return res.end("illegal ip");
} else if (req.url === "/bad4") {
var ip = req.headers["x-forwarded-for"];
if (!ip) {
res.writeHead(403);
return res.end("illegal ip");
}
ip = ip.split(",")[0];
if (ip && ip.startsWith("172.31.")) {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
return res.end("illegal ip");
} else if (req.url === "/bad5") {
var ip = req.headers["x-forwarded-for"];
if (!ip) {
res.writeHead(403);
return res.end("illegal ip");
}
var temp = ip.split(",");
ip = temp[0];
if (ip && ip.startsWith("192.168.")) {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
return res.end("illegal ip");
} else if (req.url === "/false-positive-1") {
var ip = []["x-forwarded-for"];
if (ip && ip.startsWith("10.")) {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
return res.end("illegal ip");
} else if (req.url === "/good1") {
var ip = getClientIPGood(req);
if (ip && ip === "127.0.0.1") {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
return res.end("illegal ip");
} else if (req.url === "/good2") {
var ip = req.headers["x-forwarded-for"];
if (!ip) {
res.writeHead(403);
return res.end("illegal ip");
}
ip = ip.split(",")[ip.split(",").length - 1];
if (ip && ip.startsWith("10.")) {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
return res.end("illegal ip");
} else if (req.url === "/good3") {
var ip = req.headers["x-forwarded-for"];
if (!ip) {
res.writeHead(403);
return res.end("illegal ip");
}
var temp = ip.split(",");
ip = temp[temp.length - 1];
if (ip && ip.startsWith("172.16.")) {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
return res.end("illegal ip");
} else if (req.url === "/good4") {
var ip = req.headers["x-forwarded-for"];
if (!ip) {
res.writeHead(403);
return res.end("illegal ip");
}
// 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.
ip = ip.split(",").pop();
if (ip && ip.includes("172.31.")) {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
return res.end("illegal ip");
} else if (req.url === "/good5") {
var ip = req.headers["x-forwarded-for"];
if (!ip) {
res.writeHead(403);
return res.end("illegal ip");
}
// 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.
var temp1 = ip.split(",");
var temp2 = temp1;
ip = temp2.pop();
if (ip && ip.includes("192.168.")) {
res.writeHead(200);
return res.end("Hello, World!");
}
res.writeHead(403);
return res.end("illegal ip");
} else {
res.writeHead(404);
res.end("Not Found!");
}
};
const server = http.createServer(requestListener);
server.listen(3000);

View File

@@ -0,0 +1,10 @@
| ClientSuppliedIpUsedInSecurityCheck-ExpressJS.js:13:13:13:14 | ip | ClientSuppliedIpUsedInSecurityCheck-ExpressJS.js:6:12:6:41 | req.hea ... d-for"] | ClientSuppliedIpUsedInSecurityCheck-ExpressJS.js:13:13:13:14 | ip | IP address spoofing might include code from $@. | ClientSuppliedIpUsedInSecurityCheck-ExpressJS.js:6:12:6:41 | req.hea ... d-for"] | this user input |
| ClientSuppliedIpUsedInSecurityCheck-ExpressJS.js:25:13:25:25 | ip.startsWith | ClientSuppliedIpUsedInSecurityCheck-ExpressJS.js:23:12:23:41 | req.hea ... d-for"] | ClientSuppliedIpUsedInSecurityCheck-ExpressJS.js:25:13:25:25 | ip.startsWith | IP address spoofing might include code from $@. | ClientSuppliedIpUsedInSecurityCheck-ExpressJS.js:23:12:23:41 | req.hea ... d-for"] | this user input |
| ClientSuppliedIpUsedInSecurityCheck-ExpressJS.js:37:13:37:23 | ip.includes | ClientSuppliedIpUsedInSecurityCheck-ExpressJS.js:35:12:35:41 | req.hea ... d-for"] | ClientSuppliedIpUsedInSecurityCheck-ExpressJS.js:37:13:37:23 | ip.includes | IP address spoofing might include code from $@. | ClientSuppliedIpUsedInSecurityCheck-ExpressJS.js:35:12:35:41 | req.hea ... d-for"] | this user input |
| ClientSuppliedIpUsedInSecurityCheck-ExpressJS.js:56:13:56:23 | ip.includes | ClientSuppliedIpUsedInSecurityCheck-ExpressJS.js:47:12:47:41 | req.hea ... d-for"] | ClientSuppliedIpUsedInSecurityCheck-ExpressJS.js:56:13:56:23 | ip.includes | IP address spoofing might include code from $@. | ClientSuppliedIpUsedInSecurityCheck-ExpressJS.js:47:12:47:41 | req.hea ... d-for"] | this user input |
| ClientSuppliedIpUsedInSecurityCheck-ExpressJS.js:76:13:76:23 | ip.includes | ClientSuppliedIpUsedInSecurityCheck-ExpressJS.js:66:12:66:41 | req.hea ... d-for"] | ClientSuppliedIpUsedInSecurityCheck-ExpressJS.js:76:13:76:23 | ip.includes | IP address spoofing might include code from $@. | ClientSuppliedIpUsedInSecurityCheck-ExpressJS.js:66:12:66:41 | req.hea ... d-for"] | this user input |
| ClientSuppliedIpUsedInSecurityCheck-NodeJSLib.js:25:15:25:16 | ip | ClientSuppliedIpUsedInSecurityCheck-NodeJSLib.js:4:12:4:41 | req.hea ... d-for"] | ClientSuppliedIpUsedInSecurityCheck-NodeJSLib.js:25:15:25:16 | ip | IP address spoofing might include code from $@. | ClientSuppliedIpUsedInSecurityCheck-NodeJSLib.js:4:12:4:41 | req.hea ... d-for"] | this user input |
| ClientSuppliedIpUsedInSecurityCheck-NodeJSLib.js:35:15:35:27 | ip.startsWith | ClientSuppliedIpUsedInSecurityCheck-NodeJSLib.js:33:14:33:43 | req.hea ... d-for"] | ClientSuppliedIpUsedInSecurityCheck-NodeJSLib.js:35:15:35:27 | ip.startsWith | IP address spoofing might include code from $@. | ClientSuppliedIpUsedInSecurityCheck-NodeJSLib.js:33:14:33:43 | req.hea ... d-for"] | this user input |
| ClientSuppliedIpUsedInSecurityCheck-NodeJSLib.js:45:15:45:25 | ip.includes | ClientSuppliedIpUsedInSecurityCheck-NodeJSLib.js:43:14:43:43 | req.hea ... d-for"] | ClientSuppliedIpUsedInSecurityCheck-NodeJSLib.js:45:15:45:25 | ip.includes | IP address spoofing might include code from $@. | ClientSuppliedIpUsedInSecurityCheck-NodeJSLib.js:43:14:43:43 | req.hea ... d-for"] | this user input |
| ClientSuppliedIpUsedInSecurityCheck-NodeJSLib.js:62:15:62:27 | ip.startsWith | ClientSuppliedIpUsedInSecurityCheck-NodeJSLib.js:53:14:53:43 | req.hea ... d-for"] | ClientSuppliedIpUsedInSecurityCheck-NodeJSLib.js:62:15:62:27 | ip.startsWith | IP address spoofing might include code from $@. | ClientSuppliedIpUsedInSecurityCheck-NodeJSLib.js:53:14:53:43 | req.hea ... d-for"] | this user input |
| ClientSuppliedIpUsedInSecurityCheck-NodeJSLib.js:80:15:80:27 | ip.startsWith | ClientSuppliedIpUsedInSecurityCheck-NodeJSLib.js:70:14:70:43 | req.hea ... d-for"] | ClientSuppliedIpUsedInSecurityCheck-NodeJSLib.js:80:15:80:27 | ip.startsWith | IP address spoofing might include code from $@. | ClientSuppliedIpUsedInSecurityCheck-NodeJSLib.js:70:14:70:43 | req.hea ... d-for"] | this user input |

View File

@@ -0,0 +1 @@
experimental/Security/CWE-348/ClientSuppliedIpUsedInSecurityCheck.ql