Merge pull request #118 from esben-semmle/js/request-forgery

Approved by asger-semmle
This commit is contained in:
semmle-qlci
2018-09-11 16:28:59 +01:00
committed by GitHub
21 changed files with 628 additions and 62 deletions

View File

@@ -29,3 +29,4 @@
+ semmlecode-javascript-queries/Security/CWE-807/DifferentKindsComparisonBypass.ql: /Security/CWE/CWE-807
+ semmlecode-javascript-queries/Security/CWE-843/TypeConfusionThroughParameterTampering.ql: /Security/CWE/CWE-834
+ semmlecode-javascript-queries/Security/CWE-916/InsufficientPasswordHash.ql: /Security/CWE/CWE-916
+ semmlecode-javascript-queries/Security/CWE-918/RequestForgery.ql: /Security/CWE/CWE-918

View File

@@ -0,0 +1,79 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Directly incorporating user input into an HTTP request
without validating the input can facilitate different kinds of request
forgery attacks, where the attacker essentially controls the request.
If the vulnerable request is in server-side code, then security
mechanisms, such as external firewalls, can be bypassed.
If the vulnerable request is in client-side code, then unsuspecting
users can send malicious requests to other servers, potentially
resulting in a DDOS attack.
</p>
</overview>
<recommendation>
<p>
To guard against request forgery, it is advisable to avoid
putting user input directly into a remote request. If a flexible
remote request mechanism is required, it is recommended to maintain a
list of authorized request targets and choose from that list based on
the user input provided.
</p>
</recommendation>
<example>
<p>
The following example shows an HTTP request parameter
being used directly in a URL request without validating the input,
which facilitates an SSRF attack. The request
<code>http.get(...)</code> is vulnerable since attackers can choose
the value of <code>target</code> to be anything they want. For
instance, the attacker can choose
<code>"internal.example.com/#"</code> as the target, causing the URL
used in the request to be
<code>"https://internal.example.com/#.example.com/data"</code>.
</p>
<p>
A request to <code>https://internal.example.com</code> may
be problematic if that server is not meant to be
directly accessible from the attacker's machine.
</p>
<sample src="examples/RequestForgeryBad.js"/>
<p>
One way to remedy the problem is to use the user input to
select a known fixed string before performing the request:
</p>
<sample src="examples/RequestForgeryGood.js"/>
</example>
<references>
<li>OWASP: <a href="https://www.owasp.org/index.php/Server_Side_Request_Forgery">SSRF</a></li>
</references>
</qhelp>

View File

@@ -0,0 +1,18 @@
/**
* @name Uncontrolled data used in remote request
* @description Sending remote requests with user-controlled data allows for request forgery attacks.
* @kind problem
* @problem.severity error
* @precision medium
* @id js/request-forgery
* @tags security
* external/cwe/cwe-918
*/
import javascript
import semmle.javascript.security.dataflow.RequestForgery::RequestForgery
from Configuration cfg, DataFlow::Node source, Sink sink, DataFlow::Node request
where cfg.hasFlow(source, sink) and
request = sink.getARequest()
select request, "The $@ of this request depends on $@.", sink, sink.getKind(), source, "a user-provided value"

View File

@@ -0,0 +1,12 @@
import http from 'http';
import url from 'url';
var server = http.createServer(function(req, res) {
var target = url.parse(request.url, true).query.target;
// BAD: `target` is controlled by the attacker
http.get('https://' + target + ".example.com/data/", res => {
// process request response ...
});
});

View File

@@ -0,0 +1,19 @@
import http from 'http';
import url from 'url';
var server = http.createServer(function(req, res) {
var target = url.parse(request.url, true).query.target;
var subdomain;
if (target === 'EU') {
subdomain = "europe"
} else {
subdomain = "world"
}
// GOOD: `subdomain` is controlled by the server
http.get('https://' + subdomain + ".example.com/data/", res => {
// process request response ...
});
});

View File

@@ -54,6 +54,7 @@ import semmle.javascript.frameworks.AWS
import semmle.javascript.frameworks.Azure
import semmle.javascript.frameworks.Babel
import semmle.javascript.frameworks.ComposedFunctions
import semmle.javascript.frameworks.ClientRequests
import semmle.javascript.frameworks.Credentials
import semmle.javascript.frameworks.CryptoLibraries
import semmle.javascript.frameworks.DigitalOcean

View File

@@ -0,0 +1,194 @@
/**
* Provides classes for modelling the client-side of a URL request.
*
* Subclass `ClientRequest` to refine the behavior of the analysis on existing client requests.
* Subclass `CustomClientRequest` to introduce new kinds of client requests.
*/
import javascript
/**
* A call that performs a request to a URL.
*/
abstract class CustomClientRequest extends DataFlow::InvokeNode {
/**
* Gets the URL of the request.
*/
abstract DataFlow::Node getUrl();
}
/**
* A call that performs a request to a URL.
*/
class ClientRequest extends DataFlow::InvokeNode {
CustomClientRequest custom;
ClientRequest() {
this = custom
}
/**
* Gets the URL of the request.
*/
DataFlow::Node getUrl() {
result = custom.getUrl()
}
}
/**
* Gets name of an HTTP request method, in all-lowercase.
*/
private string httpMethodName() {
result = any(HTTP::RequestMethodName m).toLowerCase()
}
/**
* Gets the name of a property that likely contains a URL value.
*/
private string urlPropertyName() {
result = "uri" or
result = "url"
}
/**
* A model of a URL request made using the `request` library.
*/
private class RequestUrlRequest extends CustomClientRequest {
DataFlow::Node url;
RequestUrlRequest() {
exists (string moduleName, DataFlow::SourceNode callee |
this = callee.getACall() |
(
moduleName = "request" or
moduleName = "request-promise" or
moduleName = "request-promise-any" or
moduleName = "request-promise-native"
) and
(
callee = DataFlow::moduleImport(moduleName) or
callee = DataFlow::moduleMember(moduleName, httpMethodName())
) and
(
url = getArgument(0) or
url = getOptionArgument(0, urlPropertyName())
)
)
}
override DataFlow::Node getUrl() {
result = url
}
}
/**
* A model of a URL request made using the `axios` library.
*/
private class AxiosUrlRequest extends CustomClientRequest {
DataFlow::Node url;
AxiosUrlRequest() {
exists (string moduleName, DataFlow::SourceNode callee |
this = callee.getACall() |
moduleName = "axios" and
(
callee = DataFlow::moduleImport(moduleName) or
callee = DataFlow::moduleMember(moduleName, httpMethodName()) or
callee = DataFlow::moduleMember(moduleName, "request")
) and
(
url = getArgument(0) or
// depends on the method name and the call arity, over-approximating slightly in the name of simplicity
url = getOptionArgument([0..2], urlPropertyName())
)
)
}
override DataFlow::Node getUrl() {
result = url
}
}
/**
* A model of a URL request made using an implementation of the `fetch` API.
*/
private class FetchUrlRequest extends CustomClientRequest {
DataFlow::Node url;
FetchUrlRequest() {
exists (string moduleName, DataFlow::SourceNode callee |
this = callee.getACall() |
(
moduleName = "node-fetch" or
moduleName = "cross-fetch" or
moduleName = "isomorphic-fetch"
) and
callee = DataFlow::moduleImport(moduleName) and
url = getArgument(0)
)
or
(
this = DataFlow::globalVarRef("fetch").getACall() and
url = getArgument(0)
)
}
override DataFlow::Node getUrl() {
result = url
}
}
/**
* A model of a URL request made using the `got` library.
*/
private class GotUrlRequest extends CustomClientRequest {
DataFlow::Node url;
GotUrlRequest() {
exists (string moduleName, DataFlow::SourceNode callee |
this = callee.getACall() |
moduleName = "got" and
(
callee = DataFlow::moduleImport(moduleName) or
callee = DataFlow::moduleMember(moduleName, "stream")
) and
url = getArgument(0) and not exists (getOptionArgument(1, "baseUrl"))
)
}
override DataFlow::Node getUrl() {
result = url
}
}
/**
* A model of a URL request made using the `superagent` library.
*/
private class SuperAgentUrlRequest extends CustomClientRequest {
DataFlow::Node url;
SuperAgentUrlRequest() {
exists (string moduleName, DataFlow::SourceNode callee |
this = callee.getACall() |
moduleName = "superagent" and
callee = DataFlow::moduleMember(moduleName, httpMethodName()) and
url = getArgument(0)
)
}
override DataFlow::Node getUrl() {
result = url
}
}

View File

@@ -33,37 +33,51 @@ module Electron {
this = DataFlow::moduleMember("electron", "BrowserView").getAnInstantiation()
}
}
/**
* A Node.js-style HTTP or HTTPS request made using an Electron module.
*/
abstract class ClientRequest extends NodeJSLib::ClientRequest {}
abstract class CustomElectronClientRequest extends NodeJSLib::CustomNodeJSClientRequest {}
/**
* A Node.js-style HTTP or HTTPS request made using an Electron module.
*/
class ElectronClientRequest extends NodeJSLib::NodeJSClientRequest {
ElectronClientRequest() {
this instanceof CustomElectronClientRequest
}
}
/**
* A Node.js-style HTTP or HTTPS request made using `electron.net`, for example `net.request(url)`.
*/
private class NetRequest extends ClientRequest {
private class NetRequest extends CustomElectronClientRequest {
NetRequest() {
this = DataFlow::moduleMember("electron", "net").getAMemberCall("request")
}
override DataFlow::Node getOptions() {
result = this.(DataFlow::MethodCallNode).getArgument(0)
override DataFlow::Node getUrl() {
result = getArgument(0) or
result = getOptionArgument(0, "url")
}
}
/**
* A Node.js-style HTTP or HTTPS request made using `electron.client`, for example `new client(url)`.
*/
private class NewClientRequest extends ClientRequest {
private class NewClientRequest extends CustomElectronClientRequest {
NewClientRequest() {
this = DataFlow::moduleMember("electron", "ClientRequest").getAnInstantiation()
}
override DataFlow::Node getOptions() {
result = this.(DataFlow::NewNode).getArgument(0)
override DataFlow::Node getUrl() {
result = getArgument(0) or
result = getOptionArgument(0, "url")
}
}
@@ -75,12 +89,12 @@ module Electron {
exists(NodeJSLib::ClientRequestHandler handler |
this = handler.getParameter(0) and
handler.getAHandledEvent() = "redirect" and
handler.getClientRequest() instanceof ClientRequest
handler.getClientRequest() instanceof ElectronClientRequest
)
}
override string getSourceType() {
result = "Electron ClientRequest redirect event"
result = "ElectronClientRequest redirect event"
}
}
}

View File

@@ -502,53 +502,47 @@ module NodeJSLib {
}
/**
* A data flow node that is an HTTP or HTTPS client request made by a Node.js server, for example `http.request(url)`.
* A data flow node that is an HTTP or HTTPS client request made by a Node.js application, for example `http.request(url)`.
*/
abstract class ClientRequest extends DataFlow::DefaultSourceNode {
/**
* Gets the options object or string URL used to make the request.
*/
abstract DataFlow::Node getOptions();
abstract class CustomNodeJSClientRequest extends CustomClientRequest {
}
/**
* A data flow node that is an HTTP or HTTPS client request made by a Node.js application, for example `http.request(url)`.
*/
class NodeJSClientRequest extends ClientRequest {
NodeJSClientRequest() {
this instanceof CustomNodeJSClientRequest
}
}
/**
* A data flow node that is an HTTP or HTTPS client request made by a Node.js server, for example `http.request(url)`.
* A model of a URL request in the Node.js `http` library.
*/
private class HttpRequest extends ClientRequest {
HttpRequest() {
exists(string protocol |
private class NodeHttpUrlRequest extends CustomNodeJSClientRequest {
DataFlow::Node url;
NodeHttpUrlRequest() {
exists (string moduleName, DataFlow::SourceNode callee |
this = callee.getACall() |
(moduleName = "http" or moduleName = "https") and
(
protocol = "http" or
protocol = "https"
)
and
this = DataFlow::moduleImport(protocol).getAMemberCall("request")
callee = DataFlow::moduleMember(moduleName, any(HTTP::RequestMethodName m).toLowerCase())
or
callee = DataFlow::moduleMember(moduleName, "request")
) and
url = getArgument(0)
)
}
override DataFlow::Node getOptions() {
result = this.(DataFlow::MethodCallNode).getArgument(0)
}
}
/**
* A data flow node that is an HTTP or HTTPS client request made by a Node.js process, for example `https.get(url)`.
*/
private class HttpGet extends ClientRequest {
HttpGet() {
exists(string protocol |
(
protocol = "http" or
protocol = "https"
)
and
this = DataFlow::moduleImport(protocol).getAMemberCall("get")
)
}
override DataFlow::Node getOptions() {
result = this.(DataFlow::MethodCallNode).getArgument(0)
override DataFlow::Node getUrl() {
result = url
}
}
/**
@@ -556,13 +550,13 @@ module NodeJSLib {
*/
private class ClientRequestCallbackParam extends DataFlow::ParameterNode, RemoteFlowSource {
ClientRequestCallbackParam() {
exists(ClientRequest req |
exists(NodeJSClientRequest req |
this = req.(DataFlow::MethodCallNode).getCallback(1).getParameter(0)
)
}
override string getSourceType() {
result = "ClientRequest callback parameter"
result = "NodeJSClientRequest callback parameter"
}
}
@@ -589,7 +583,7 @@ module NodeJSLib {
*/
class ClientRequestHandler extends DataFlow::FunctionNode {
string handledEvent;
ClientRequest clientRequest;
NodeJSClientRequest clientRequest;
ClientRequestHandler() {
exists(DataFlow::MethodCallNode mcn |
@@ -609,7 +603,7 @@ module NodeJSLib {
/**
* Gets a request this callback is registered for.
*/
ClientRequest getClientRequest() {
NodeJSClientRequest getClientRequest() {
result = clientRequest
}
}
@@ -626,7 +620,7 @@ module NodeJSLib {
}
override string getSourceType() {
result = "ClientRequest response event"
result = "NodeJSClientRequest response event"
}
}
@@ -643,7 +637,7 @@ module NodeJSLib {
}
override string getSourceType() {
result = "ClientRequest data event"
result = "NodeJSClientRequest data event"
}
}
@@ -667,7 +661,7 @@ module NodeJSLib {
}
override string getSourceType() {
result = "ClientRequest login event"
result = "NodeJSClientRequest login event"
}
}
@@ -725,7 +719,7 @@ module NodeJSLib {
}
override string getSourceType() {
result = "ClientRequest error event"
result = "NodeJSClientRequest error event"
}
}
}

View File

@@ -0,0 +1,87 @@
/**
* Provides a taint-tracking configuration for reasoning about request forgery.
*/
import semmle.javascript.security.dataflow.RemoteFlowSources
import UrlConcatenation
module RequestForgery {
/**
* A data flow source for request forgery.
*/
abstract class Source extends DataFlow::Node { }
/**
* A data flow sink for request forgery.
*/
abstract class Sink extends DataFlow::Node {
/**
* Gets a request that uses this sink.
*/
abstract DataFlow::Node getARequest();
/**
* Gets the kind of this sink.
*/
abstract string getKind();
}
/**
* A sanitizer for request forgery.
*/
abstract class Sanitizer extends DataFlow::Node { }
/**
* A taint tracking configuration for request forgery.
*/
class Configuration extends TaintTracking::Configuration {
Configuration() {
this = "RequestForgery"
}
override predicate isSource(DataFlow::Node source) {
source instanceof Source
}
override predicate isSink(DataFlow::Node sink) {
sink instanceof Sink
}
override predicate isSanitizer(DataFlow::Node node) {
super.isSanitizer(node) or
node instanceof Sanitizer
}
override predicate isSanitizer(DataFlow::Node source, DataFlow::Node sink) {
sanitizingPrefixEdge(source, sink)
}
}
/** A source of remote user input, considered as a flow source for request forgery. */
private class RemoteFlowSourceAsSource extends Source {
RemoteFlowSourceAsSource() { this instanceof RemoteFlowSource }
}
/**
* The URL of a URL request, viewed as a sink for request forgery.
*/
private class ClientRequestUrlAsSink extends Sink {
ClientRequest request;
ClientRequestUrlAsSink() {
this = request.getUrl()
}
override DataFlow::Node getARequest() {
result = request
}
override string getKind() {
result = "URL"
}
}
}

View File

@@ -0,0 +1,18 @@
| tst.js:11:5:11:16 | request(url) |
| tst.js:13:5:13:20 | request.get(url) |
| tst.js:15:5:15:23 | request.delete(url) |
| tst.js:17:5:17:25 | request ... url }) |
| tst.js:19:5:19:23 | requestPromise(url) |
| tst.js:21:5:21:23 | superagent.get(url) |
| tst.js:23:5:23:17 | http.get(url) |
| tst.js:25:5:25:14 | axios(url) |
| tst.js:27:5:27:18 | axios.get(url) |
| tst.js:29:5:29:23 | axios({ url: url }) |
| tst.js:31:5:31:12 | got(url) |
| tst.js:33:5:33:19 | got.stream(url) |
| tst.js:35:5:35:21 | window.fetch(url) |
| tst.js:37:5:37:18 | nodeFetch(url) |
| tst.js:39:5:39:20 | net.request(url) |
| tst.js:41:5:41:29 | net.req ... url }) |
| tst.js:43:5:43:26 | new Cli ... st(url) |
| tst.js:45:5:45:35 | new Cli ... url }) |

View File

@@ -0,0 +1,4 @@
import javascript
from ClientRequest r
select r

View File

@@ -0,0 +1,22 @@
| tst.js:11:5:11:16 | request(url) | tst.js:11:13:11:15 | url |
| tst.js:13:5:13:20 | request.get(url) | tst.js:13:17:13:19 | url |
| tst.js:15:5:15:23 | request.delete(url) | tst.js:15:20:15:22 | url |
| tst.js:17:5:17:25 | request ... url }) | tst.js:17:13:17:24 | { url: url } |
| tst.js:17:5:17:25 | request ... url }) | tst.js:17:20:17:22 | url |
| tst.js:19:5:19:23 | requestPromise(url) | tst.js:19:20:19:22 | url |
| tst.js:21:5:21:23 | superagent.get(url) | tst.js:21:20:21:22 | url |
| tst.js:23:5:23:17 | http.get(url) | tst.js:23:14:23:16 | url |
| tst.js:25:5:25:14 | axios(url) | tst.js:25:11:25:13 | url |
| tst.js:27:5:27:18 | axios.get(url) | tst.js:27:15:27:17 | url |
| tst.js:29:5:29:23 | axios({ url: url }) | tst.js:29:11:29:22 | { url: url } |
| tst.js:29:5:29:23 | axios({ url: url }) | tst.js:29:18:29:20 | url |
| tst.js:31:5:31:12 | got(url) | tst.js:31:9:31:11 | url |
| tst.js:33:5:33:19 | got.stream(url) | tst.js:33:16:33:18 | url |
| tst.js:35:5:35:21 | window.fetch(url) | tst.js:35:18:35:20 | url |
| tst.js:37:5:37:18 | nodeFetch(url) | tst.js:37:15:37:17 | url |
| tst.js:39:5:39:20 | net.request(url) | tst.js:39:17:39:19 | url |
| tst.js:41:5:41:29 | net.req ... url }) | tst.js:41:17:41:28 | { url: url } |
| tst.js:41:5:41:29 | net.req ... url }) | tst.js:41:24:41:26 | url |
| tst.js:43:5:43:26 | new Cli ... st(url) | tst.js:43:23:43:25 | url |
| tst.js:45:5:45:35 | new Cli ... url }) | tst.js:45:23:45:34 | { url: url } |
| tst.js:45:5:45:35 | new Cli ... url }) | tst.js:45:30:45:32 | url |

View File

@@ -0,0 +1,4 @@
import javascript
from ClientRequest r
select r, r.getUrl()

View File

@@ -0,0 +1,50 @@
import request from 'request';
import requestPromise from 'request-promise';
import superagent from 'superagent';
import http from 'http';
import express from 'express';
import axios from 'axios';
import got from 'got';
import nodeFetch from 'node-fetch';
import {ClientRequest, net} from 'electron';
(function() {
request(url);
request.get(url);
request.delete(url);
request({ url: url });
requestPromise(url);
superagent.get(url);
http.get(url);
axios(url);
axios.get(url);
axios({ url: url });
got(url);
got.stream(url);
window.fetch(url);
nodeFetch(url);
net.request(url);
net.request({ url: url });
new ClientRequest(url);
new ClientRequest({ url: url });
unknown(url);
unknown({ url:url });
});

View File

@@ -1,4 +1,4 @@
import javascript
from NodeJSLib::ClientRequest cr
from Electron::ElectronClientRequest cr
select cr

View File

@@ -1,4 +1,4 @@
import javascript
from NodeJSLib::ClientRequest cr
from NodeJSLib::NodeJSClientRequest cr
select cr

View File

@@ -0,0 +1,6 @@
| tst.js:16:5:16:20 | request(tainted) | The $@ of this request depends on $@. | tst.js:16:13:16:19 | tainted | URL | tst.js:12:29:12:35 | req.url | a user-provided value |
| tst.js:18:5:18:24 | request.get(tainted) | The $@ of this request depends on $@. | tst.js:18:17:18:23 | tainted | URL | tst.js:12:29:12:35 | req.url | a user-provided value |
| tst.js:22:5:22:20 | request(options) | The $@ of this request depends on $@. | tst.js:21:19:21:25 | tainted | URL | tst.js:12:29:12:35 | req.url | a user-provided value |
| tst.js:24:5:24:32 | request ... ainted) | The $@ of this request depends on $@. | tst.js:24:13:24:31 | "http://" + tainted | URL | tst.js:12:29:12:35 | req.url | a user-provided value |
| tst.js:26:5:26:43 | request ... ainted) | The $@ of this request depends on $@. | tst.js:26:13:26:42 | "http:/ ... tainted | URL | tst.js:12:29:12:35 | req.url | a user-provided value |
| tst.js:28:5:28:44 | request ... ainted) | The $@ of this request depends on $@. | tst.js:28:13:28:43 | "http:/ ... tainted | URL | tst.js:12:29:12:35 | req.url | a user-provided value |

View File

@@ -0,0 +1 @@
Security/CWE-918/RequestForgery.ql

View File

@@ -0,0 +1,31 @@
import request from 'request';
import requestPromise from 'request-promise';
import superagent from 'superagent';
import http from 'http';
import express from 'express';
import axios from 'axios';
import got from 'got';
import nodeFetch from 'node-fetch';
import url from 'url';
var server = http.createServer(function(req, res) {
var tainted = url.parse(req.url, true).query.url;
request("example.com"); // OK
request(tainted); // NOT OK
request.get(tainted); // NOT OK
var options = {};
options.url = tainted;
request(options); // NOT OK
request("http://" + tainted); // NOT OK
request("http://example.com" + tainted); // NOT OK
request("http://example.com/" + tainted); // NOT OK
request("http://example.com/?" + tainted); // OK
})