mirror of
https://github.com/github/codeql.git
synced 2025-12-20 02:44:30 +01:00
970 lines
32 KiB
Plaintext
970 lines
32 KiB
Plaintext
/**
|
|
* Provides classes for modeling the client-side of a URL request.
|
|
*
|
|
* Subclass `ClientRequest` to refine the behavior of the analysis on existing client requests.
|
|
* Subclass `ClientRequest::Range` to introduce new kinds of client requests.
|
|
*/
|
|
|
|
import javascript
|
|
|
|
/**
|
|
* A call that performs a request to a URL.
|
|
*
|
|
* Example: An HTTP POST request is client request that sends some
|
|
* `data` to a `url`, where both the headers and the body of the request
|
|
* contribute to the `data`.
|
|
*
|
|
* Extend this class to work with client request APIs for which there is already a model.
|
|
* To model additional APIs, extend `ClientRequest::Range` and implement its abstract member
|
|
* predicates.
|
|
*/
|
|
class ClientRequest extends DataFlow::InvokeNode instanceof ClientRequest::Range {
|
|
/**
|
|
* Gets the URL of the request.
|
|
*/
|
|
DataFlow::Node getUrl() { result = super.getUrl() }
|
|
|
|
/**
|
|
* Gets the host of the request.
|
|
*/
|
|
DataFlow::Node getHost() { result = super.getHost() }
|
|
|
|
/**
|
|
* Gets a node that contributes to the data-part this request.
|
|
*/
|
|
DataFlow::Node getADataNode() { result = super.getADataNode() }
|
|
|
|
/**
|
|
* Gets a data flow node that refers to some representation of the response, possibly
|
|
* wrapped in a promise object.
|
|
*
|
|
* The `responseType` describes how the response is represented as a JavaScript value
|
|
* (after resolving promises), and may assume the following values:
|
|
* - Any response type defined by [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType):
|
|
* - `text`: The result is a string
|
|
* - `json`: The result is a deserialized JSON object
|
|
* - `arraybuffer`: The result is an `ArrayBuffer` object
|
|
* - `blob`: The result is a `Blob` object
|
|
* - `document`: The result is a deserialized HTML or XML document
|
|
* - Any of the following additional response types defined by this library:
|
|
* - `fetch.response`: The result is a `Response` object from [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Response).
|
|
* - `stream`: The result is a Node.js stream and `http.IncomingMessage` object
|
|
* - `header`: The result is the value of a header, as a string
|
|
* - `headers`: The result is a mapping from header names to their values.
|
|
* - `error`: The result is an error in an unspecified format, possibly containing information from the response
|
|
* - An empty string, indicating an unknown response type.
|
|
* - Any value provided by custom implementations of `ClientRequest::Range`.
|
|
*/
|
|
DataFlow::Node getAResponseDataNode(string responseType, boolean promise) {
|
|
result = super.getAResponseDataNode(responseType, promise)
|
|
}
|
|
|
|
/**
|
|
* Gets a node that refers to data from the response, possibly
|
|
* wrapped in a promise object.
|
|
*/
|
|
DataFlow::Node getAResponseDataNode() { result = this.getAResponseDataNode(_, _) }
|
|
|
|
/**
|
|
* Gets a data-flow node that determines where in the file-system the result of the request should be saved.
|
|
*/
|
|
DataFlow::Node getASavePath() { result = super.getASavePath() }
|
|
}
|
|
|
|
module ClientRequest {
|
|
/**
|
|
* A call that performs a request to a URL.
|
|
*
|
|
* Extend this class and implement its abstract member predicates to model additional
|
|
* client request APIs. To work with APIs for which there is already a model, extend
|
|
* `ClientRequest` instead.
|
|
*/
|
|
abstract class Range extends DataFlow::InvokeNode {
|
|
/**
|
|
* Gets the URL of the request.
|
|
*/
|
|
abstract DataFlow::Node getUrl();
|
|
|
|
/**
|
|
* Gets the host of the request.
|
|
*/
|
|
abstract DataFlow::Node getHost();
|
|
|
|
/**
|
|
* Gets a node that contributes to the data-part this request.
|
|
*/
|
|
abstract DataFlow::Node getADataNode();
|
|
|
|
/**
|
|
* Gets a data flow node that refers to some representation of the response, possibly
|
|
* wrapped in a promise object.
|
|
*
|
|
* See the decription of `responseType` in `ClientRequest::getAResponseDataNode`.
|
|
*/
|
|
DataFlow::Node getAResponseDataNode(string responseType, boolean promise) { none() }
|
|
|
|
/**
|
|
* Gets a data-flow node that determines where in the file-system the result of the request should be saved.
|
|
*/
|
|
DataFlow::Node getASavePath() { none() }
|
|
}
|
|
|
|
/**
|
|
* Gets the name of an HTTP request method, in all-lowercase.
|
|
*/
|
|
private string httpMethodName() { result = any(Http::RequestMethodName m).toLowerCase() }
|
|
|
|
/**
|
|
* Gets a model of an instance of the `request` library, or one of
|
|
* its wrappers, `promise` is true if the instance uses promises
|
|
* rather than callbacks.
|
|
*/
|
|
private DataFlow::SourceNode getRequestLibrary(boolean promise) {
|
|
exists(string moduleName | result = DataFlow::moduleImport(moduleName) |
|
|
promise = false and
|
|
moduleName = "request"
|
|
or
|
|
promise = true and
|
|
(
|
|
moduleName = "request-promise" or
|
|
moduleName = "request-promise-any" or
|
|
moduleName = "request-promise-native"
|
|
)
|
|
)
|
|
or
|
|
result = getRequestLibrary(promise).getAMethodCall("defaults")
|
|
}
|
|
|
|
/**
|
|
* A model of a URL request made using the `request` library.
|
|
*/
|
|
class RequestUrlRequest extends ClientRequest::Range, DataFlow::CallNode {
|
|
boolean promise;
|
|
|
|
RequestUrlRequest() {
|
|
exists(DataFlow::SourceNode callee | this = callee.getACall() |
|
|
callee = getRequestLibrary(promise) or
|
|
callee = getRequestLibrary(promise).getAPropertyRead(httpMethodName())
|
|
)
|
|
}
|
|
|
|
override DataFlow::Node getUrl() {
|
|
result = this.getArgument(0) or
|
|
result = this.getOptionArgument(0, urlPropertyName())
|
|
}
|
|
|
|
override DataFlow::Node getHost() { none() }
|
|
|
|
/** Gets the response type from the options passed in. */
|
|
string getResponseType() {
|
|
if this.getOptionArgument(1, "json").mayHaveBooleanValue(true)
|
|
then result = "json"
|
|
else result = "text"
|
|
}
|
|
|
|
override DataFlow::Node getAResponseDataNode(string responseType, boolean pr) {
|
|
responseType = this.getResponseType() and
|
|
promise = true and
|
|
pr = true and
|
|
result = this
|
|
or
|
|
responseType = this.getResponseType() and
|
|
promise = false and
|
|
pr = false and
|
|
(
|
|
result = this.getCallback([1 .. 2]).getParameter(2)
|
|
or
|
|
result = this.getCallback([1 .. 2]).getParameter(1).getAPropertyRead("body")
|
|
)
|
|
or
|
|
responseType = "error" and
|
|
promise = false and
|
|
pr = false and
|
|
result = this.getCallback([1 .. 2]).getParameter(0)
|
|
}
|
|
|
|
override DataFlow::Node getADataNode() { result = this.getArgument(1) }
|
|
|
|
override DataFlow::Node getASavePath() {
|
|
exists(DataFlow::CallNode write |
|
|
write = DataFlow::moduleMember("fs", "createWriteStream").getACall() and
|
|
write = this.getAMemberCall("pipe").getArgument(0).getALocalSource() and
|
|
result = write.getArgument(0)
|
|
)
|
|
}
|
|
}
|
|
|
|
/** Gets the string `url` or `uri`. */
|
|
private string urlPropertyName() { result = "url" or result = "uri" }
|
|
|
|
/** An API entry-point for the global variable `axios`. */
|
|
private class AxiosGlobalEntryPoint extends API::EntryPoint {
|
|
AxiosGlobalEntryPoint() { this = "axiosGlobal" }
|
|
|
|
override DataFlow::SourceNode getASource() { result = DataFlow::globalVarRef("axios") }
|
|
}
|
|
|
|
/** Gets a reference to the `axios` library. */
|
|
private API::Node axios() {
|
|
result = API::moduleImport("axios")
|
|
or
|
|
result = API::root().getASuccessor(API::Label::entryPoint(any(AxiosGlobalEntryPoint entry)))
|
|
}
|
|
|
|
/**
|
|
* A model of a URL request made using the `axios` library.
|
|
*/
|
|
class AxiosUrlRequest extends ClientRequest::Range, API::CallNode {
|
|
string method;
|
|
|
|
AxiosUrlRequest() {
|
|
this = axios().getACall() and
|
|
method = "request"
|
|
or
|
|
this = axios().getMember(method).getACall() and
|
|
method = [httpMethodName(), "request", "postForm", "putForm", "patchForm", "getUri"]
|
|
or
|
|
this = axios().getMember("create").getReturn().getACall() and
|
|
method = "request"
|
|
}
|
|
|
|
private int getOptionsArgIndex() {
|
|
method = "request" and
|
|
result = 0
|
|
or
|
|
(method = "get" or method = "delete" or method = "head") and
|
|
result = 1
|
|
or
|
|
(method = "post" or method = "put" or method = "patch") and
|
|
result = 2
|
|
}
|
|
|
|
private DataFlow::Node getOptionArgument(string name) {
|
|
result = this.getOptionArgument(this.getOptionsArgIndex(), name)
|
|
}
|
|
|
|
override DataFlow::Node getUrl() {
|
|
result = this.getArgument(0) or
|
|
result = this.getOptionArgument(urlPropertyName())
|
|
}
|
|
|
|
override DataFlow::Node getHost() { result = this.getOptionArgument("host") }
|
|
|
|
override DataFlow::Node getADataNode() {
|
|
method = "request" and
|
|
result = this.getOptionArgument(0, "data")
|
|
or
|
|
method = ["post", "put"] and
|
|
result = [this.getArgument(1), this.getOptionArgument(2, "data")]
|
|
or
|
|
method = ["postForm", "putForm", "patchForm"] and result = this.getArgument(1)
|
|
or
|
|
result = this.getOptionArgument([0 .. 2], ["headers", "params"])
|
|
}
|
|
|
|
/** Gets the response type from the options passed in. */
|
|
string getResponseType() {
|
|
exists(DataFlow::Node option | option = this.getOptionArgument("responseType") |
|
|
option.mayHaveStringValue(result)
|
|
or
|
|
option.analyze().getAValue().isIndefinite(_) and
|
|
result = ""
|
|
)
|
|
or
|
|
not exists(this.getOptionArgument("responseType")) and
|
|
result = "json"
|
|
or
|
|
this.getArgument(this.getOptionsArgIndex()).analyze().getAValue().isIndefinite(_) and
|
|
result = ""
|
|
}
|
|
|
|
override DataFlow::Node getAResponseDataNode(string responseType, boolean promise) {
|
|
responseType = this.getResponseType() and
|
|
promise = true and
|
|
result = this
|
|
or
|
|
responseType = this.getResponseType() and
|
|
promise = false and
|
|
result = this.getReturn().getPromisedError().getMember("response").asSource()
|
|
}
|
|
}
|
|
|
|
/** An expression that is used as a credential in a request. */
|
|
private class AuthorizationHeader extends CredentialsNode {
|
|
AuthorizationHeader() {
|
|
exists(DataFlow::PropWrite write | write.getPropertyName().regexpMatch("(?i)authorization") |
|
|
this = write.getRhs()
|
|
)
|
|
or
|
|
exists(DataFlow::MethodCallNode call | call.getMethodName() = ["append", "set"] |
|
|
call.getNumArgument() = 2 and
|
|
call.getArgument(0).getStringValue().regexpMatch("(?i)authorization") and
|
|
this = call.getArgument(1)
|
|
)
|
|
}
|
|
|
|
override string getCredentialsKind() { result = "authorization header" }
|
|
}
|
|
|
|
/**
|
|
* A model of a URL request made using an implementation of the `fetch` API.
|
|
*/
|
|
class FetchUrlRequest extends ClientRequest::Range {
|
|
DataFlow::Node url;
|
|
|
|
FetchUrlRequest() {
|
|
exists(DataFlow::SourceNode fetch |
|
|
fetch = DataFlow::moduleImport(["node-fetch", "cross-fetch", "isomorphic-fetch"])
|
|
or
|
|
fetch = DataFlow::moduleMember("whatwg-fetch", "fetch")
|
|
or
|
|
fetch = DataFlow::globalVarRef("fetch") // https://fetch.spec.whatwg.org/#fetch-api
|
|
|
|
|
this = fetch.getACall() and
|
|
url = this.getArgument(0)
|
|
)
|
|
}
|
|
|
|
override DataFlow::Node getUrl() { result = url }
|
|
|
|
override DataFlow::Node getHost() { none() }
|
|
|
|
override DataFlow::Node getADataNode() {
|
|
exists(string name | name = "headers" or name = "body" |
|
|
result = this.getOptionArgument(1, name)
|
|
)
|
|
}
|
|
|
|
override DataFlow::Node getAResponseDataNode(string responseType, boolean promise) {
|
|
responseType = "fetch.response" and
|
|
promise = true and
|
|
result = this
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Classes for modeling the url request library `needle`.
|
|
*/
|
|
private module Needle {
|
|
/**
|
|
* A model of a URL request made using `require("needle")(...)`.
|
|
*/
|
|
class PromisedNeedleRequest extends ClientRequest::Range {
|
|
PromisedNeedleRequest() { this = DataFlow::moduleImport("needle").getACall() }
|
|
|
|
override DataFlow::Node getUrl() { result = this.getArgument(1) }
|
|
|
|
override DataFlow::Node getHost() { none() }
|
|
|
|
override DataFlow::Node getADataNode() {
|
|
result = this.getOptionArgument([2, 3], "headers")
|
|
or
|
|
result = this.getArgument(2)
|
|
}
|
|
|
|
override DataFlow::Node getAResponseDataNode(string responseType, boolean promise) {
|
|
responseType = "fetch.response" and
|
|
promise = true and
|
|
result = this
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A model of a URL request made using `require("needle")[method](...)`.
|
|
* E.g. `needle.get("http://example.org", (err, resp, body) => {})`.
|
|
*
|
|
* As opposed to the calls modeled in `PromisedNeedleRequest` these calls do not return promises.
|
|
* Instead they take an optional callback as their last argument.
|
|
*/
|
|
class NeedleMethodRequest extends ClientRequest::Range {
|
|
boolean hasData;
|
|
|
|
NeedleMethodRequest() {
|
|
exists(string method |
|
|
method = ["get", "head"] and hasData = false
|
|
or
|
|
method = ["post", "put", "patch", "delete"] and hasData = true
|
|
or
|
|
method = "request" and hasData = [true, false]
|
|
|
|
|
this = DataFlow::moduleMember("needle", method).getACall()
|
|
)
|
|
}
|
|
|
|
override DataFlow::Node getUrl() { result = this.getArgument(0) }
|
|
|
|
override DataFlow::Node getHost() { none() }
|
|
|
|
override DataFlow::Node getADataNode() {
|
|
hasData = true and
|
|
(
|
|
result = this.getArgument(1)
|
|
or
|
|
result = this.getOptionArgument(2, "headers")
|
|
)
|
|
or
|
|
hasData = false and
|
|
result = this.getOptionArgument(1, "headers")
|
|
}
|
|
|
|
override DataFlow::Node getAResponseDataNode(string responseType, boolean promise) {
|
|
promise = false and
|
|
result = this.getABoundCallbackParameter(this.getNumArgument() - 1, 1) and
|
|
responseType = "fetch.response"
|
|
or
|
|
promise = false and
|
|
result = this.getABoundCallbackParameter(this.getNumArgument() - 1, 2) and
|
|
responseType = "json"
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents an instance of the `got` HTTP client library.
|
|
*/
|
|
abstract private class GotInstance extends API::Node {
|
|
/**
|
|
* Gets the options object associated with this instance of `got`.
|
|
*/
|
|
API::Node getOptions() { none() }
|
|
}
|
|
|
|
/**
|
|
* Represents the root `got` module import.
|
|
* For example: `const got = require('got')`.
|
|
*/
|
|
private class RootGotInstance extends GotInstance {
|
|
RootGotInstance() { this = API::moduleImport("got") }
|
|
}
|
|
|
|
/**
|
|
* Represents an instance of `got` created by calling the `extend()` method.
|
|
* It may also be chained with multiple calls to `extend()`.
|
|
*
|
|
* For example: `const client = got.extend({ prefixUrl: 'https://example.com' })`.
|
|
*/
|
|
private class ExtendGotInstance extends GotInstance {
|
|
private GotInstance base;
|
|
private API::CallNode extendCall;
|
|
|
|
ExtendGotInstance() {
|
|
extendCall = base.getMember("extend").getACall() and
|
|
this = extendCall.getReturn()
|
|
}
|
|
|
|
override API::Node getOptions() {
|
|
result = extendCall.getParameter(0) or result = base.getOptions()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A model of a URL request made using the `got` library.
|
|
*/
|
|
class GotUrlRequest extends ClientRequest::Range {
|
|
GotInstance got;
|
|
|
|
GotUrlRequest() {
|
|
exists(API::Node callee | this = callee.getACall() |
|
|
callee =
|
|
[
|
|
got,
|
|
got.getMember(["stream", "get", "post", "put", "patch", "head", "delete", "paginate"])
|
|
]
|
|
)
|
|
}
|
|
|
|
override DataFlow::Node getUrl() {
|
|
result = this.getArgument(0) and
|
|
not exists(this.getOptionArgument(1, "baseUrl"))
|
|
or
|
|
// Handle URL from options passed to extend()
|
|
result = got.getOptions().getMember("url").asSink() and
|
|
not exists(this.getArgument(0))
|
|
or
|
|
// Handle URL from options passed as third argument when first arg is undefined/missing
|
|
exists(API::InvokeNode optionsCall |
|
|
optionsCall = API::moduleImport("got").getMember("Options").getAnInvocation() and
|
|
optionsCall.getReturn().getAValueReachableFromSource() = this.getAnArgument() and
|
|
result = optionsCall.getParameter(0).getMember("url").asSink()
|
|
)
|
|
}
|
|
|
|
override DataFlow::Node getHost() {
|
|
exists(string name |
|
|
name = "host" or
|
|
name = "hostname"
|
|
|
|
|
result = this.getOptionArgument(1, name)
|
|
)
|
|
}
|
|
|
|
override DataFlow::Node getADataNode() {
|
|
exists(string name | name = "headers" or name = "body" or name = "query" |
|
|
result = this.getOptionArgument(1, name)
|
|
)
|
|
}
|
|
|
|
/** Holds if the result is a stream. */
|
|
predicate isStream() {
|
|
this.getOptionArgument(1, "stream").mayHaveBooleanValue(true)
|
|
or
|
|
this = DataFlow::moduleMember("got", "stream").getACall()
|
|
}
|
|
|
|
/** Holds if the result is a JSON object. */
|
|
predicate isJson() { this.getOptionArgument(1, "json").mayHaveBooleanValue(true) }
|
|
|
|
override DataFlow::Node getAResponseDataNode(string responseType, boolean promise) {
|
|
result = this and
|
|
if this.isStream()
|
|
then
|
|
responseType = "stream" and
|
|
promise = false
|
|
else
|
|
if this.isJson()
|
|
then (
|
|
responseType = "json" and
|
|
promise = true
|
|
) else (
|
|
responseType = "text" and
|
|
promise = true
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets an instantiation `socket` of `require("net").Socket`.
|
|
*/
|
|
private API::Node netSocketInstantiation(DataFlow::NewNode socket) {
|
|
result = API::moduleImport("net").getMember("Socket").getInstance() and
|
|
socket = result.asSource()
|
|
}
|
|
|
|
/**
|
|
* A model of a request made using `(new require("net").Socket()).connect(args);`.
|
|
*/
|
|
class NetSocketRequest extends ClientRequest::Range {
|
|
DataFlow::NewNode socket;
|
|
|
|
NetSocketRequest() { this = netSocketInstantiation(socket).getMember("connect").getACall() }
|
|
|
|
override DataFlow::Node getUrl() {
|
|
result = this.getArgument([0, 1]) // there are multiple overrides of `connect`, and the URL can be in the first or second argument.
|
|
}
|
|
|
|
override DataFlow::Node getHost() { result = this.getOptionArgument(0, "host") }
|
|
|
|
override DataFlow::Node getAResponseDataNode(string responseType, boolean promise) {
|
|
responseType = "text" and
|
|
promise = false and
|
|
exists(DataFlow::CallNode call |
|
|
call = netSocketInstantiation(socket).getMember("on").getACall() and
|
|
call.getArgument(0).mayHaveStringValue("data") and
|
|
result = call.getABoundCallbackParameter(1, 0)
|
|
)
|
|
}
|
|
|
|
override DataFlow::Node getADataNode() {
|
|
exists(DataFlow::CallNode call |
|
|
call = netSocketInstantiation(socket).getMember("write").getACall() and
|
|
result = call.getArgument(0)
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the name of a superagent request method.
|
|
*/
|
|
private string getSuperagentRequestMethodName() {
|
|
result = [httpMethodName(), any(Http::RequestMethodName m), "del", "DEL"]
|
|
}
|
|
|
|
/**
|
|
* A model of a URL request made using the `superagent` library.
|
|
*/
|
|
class SuperAgentUrlRequest extends ClientRequest::Range {
|
|
DataFlow::Node url;
|
|
|
|
SuperAgentUrlRequest() {
|
|
exists(string moduleName | moduleName = "superagent" |
|
|
// Handle method calls like superagent.get(url)
|
|
this = API::moduleImport(moduleName).getMember(getSuperagentRequestMethodName()).getACall() and
|
|
url = this.getArgument(0)
|
|
or
|
|
// Handle direct calls like superagent('GET', url)
|
|
this = API::moduleImport(moduleName).getACall() and
|
|
this.getArgument(0).mayHaveStringValue(getSuperagentRequestMethodName()) and
|
|
url = this.getArgument(1)
|
|
or
|
|
// Handle agent calls like superagent.agent().get(url)
|
|
exists(DataFlow::SourceNode agent |
|
|
agent = API::moduleImport(moduleName).getMember("agent").getACall() and
|
|
this = agent.getAMethodCall(httpMethodName()) and
|
|
url = this.getArgument(0)
|
|
)
|
|
)
|
|
}
|
|
|
|
override DataFlow::Node getUrl() { result = url }
|
|
|
|
override DataFlow::Node getHost() { none() }
|
|
|
|
override DataFlow::Node getADataNode() {
|
|
exists(string name | name = "set" or name = "send" or name = "query" |
|
|
result = this.getAChainedMethodCall(name).getAnArgument()
|
|
)
|
|
}
|
|
|
|
override DataFlow::Node getAResponseDataNode(string responseType, boolean promise) {
|
|
responseType = "stream" and
|
|
promise = true and
|
|
result = this
|
|
or
|
|
exists(DataFlow::FunctionNode callback |
|
|
callback = this.getAChainedMethodCall("end").getCallback(0) and
|
|
promise = false and
|
|
(
|
|
responseType = "error" and result = callback.getParameter(0)
|
|
or
|
|
responseType = "stream" and result = callback.getParameter(1)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A model of a URL request made using the `XMLHttpRequest` browser class.
|
|
*
|
|
* Note: Prefer to use the `ClientRequest` class as it is more general.
|
|
*/
|
|
class XmlHttpRequest extends ClientRequest::Range {
|
|
XmlHttpRequest() {
|
|
this = DataFlow::globalVarRef("XMLHttpRequest").getAnInstantiation()
|
|
or
|
|
// closure shim for XMLHttpRequest
|
|
this = Closure::moduleImport("goog.net.XmlHttp").getAnInvocation()
|
|
}
|
|
|
|
override DataFlow::Node getUrl() { result = this.getAMethodCall("open").getArgument(1) }
|
|
|
|
override DataFlow::Node getHost() { none() }
|
|
|
|
override DataFlow::Node getADataNode() { result = this.getAMethodCall("send").getArgument(0) }
|
|
|
|
private string getExplicitResponseType() {
|
|
this.getAPropertyWrite("responseType").getRhs().mayHaveStringValue(result)
|
|
}
|
|
|
|
/**
|
|
* Gets the response type corresponding to the `response` property,
|
|
* but not the explicitly typed properties, `reponseText` and `responseXML`.
|
|
*/
|
|
string getAssignedResponseType() {
|
|
result = this.getExplicitResponseType()
|
|
or
|
|
not exists(this.getExplicitResponseType()) and
|
|
result = "text"
|
|
}
|
|
|
|
/** Gets an event listener registered on this XHR object. */
|
|
DataFlow::FunctionNode getAnEventListener() {
|
|
result = this.getAPropertyWrite("on" + any(string s)).getRhs().getAFunctionValue()
|
|
or
|
|
result = this.getAMethodCall("addEventListener").getArgument(1).getAFunctionValue()
|
|
}
|
|
|
|
/**
|
|
* Gets a node that refers to this XHR object.
|
|
*
|
|
* In particular, this can be the receiver of an event handler.
|
|
*/
|
|
DataFlow::SourceNode getAnAlias() {
|
|
result = this
|
|
or
|
|
// The value of `this` in an event listener refers to the XHR object
|
|
result = this.getAnEventListener().getReceiver()
|
|
}
|
|
|
|
override DataFlow::Node getAResponseDataNode(string responseType, boolean promise) {
|
|
promise = false and
|
|
(
|
|
exists(string prop | result = this.getAnAlias().getAPropertyRead(prop) |
|
|
prop = "response" and responseType = this.getAssignedResponseType()
|
|
or
|
|
prop = "responseText" and responseType = "text"
|
|
or
|
|
prop = "responseUrl" and responseType = "text"
|
|
or
|
|
prop = "statusText" and responseType = "text"
|
|
or
|
|
prop = "responseXML" and responseType = "document"
|
|
)
|
|
or
|
|
exists(string method | result = this.getAnAlias().getAMethodCall(method) |
|
|
method = "getAllResponseHeaders" and responseType = "headers"
|
|
or
|
|
method = "getResponseHeader" and responseType = "header"
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A model of a URL request made using the `XhrIo` class from the closure library.
|
|
*/
|
|
class ClosureXhrIoRequest extends ClientRequest::Range {
|
|
DataFlow::SourceNode base;
|
|
boolean static;
|
|
|
|
ClosureXhrIoRequest() {
|
|
exists(DataFlow::SourceNode xhrIo | xhrIo = Closure::moduleImport("goog.net.XhrIo") |
|
|
static = true and
|
|
base = xhrIo and
|
|
this = xhrIo.getAMethodCall("send")
|
|
or
|
|
static = false and
|
|
base = xhrIo.getAnInstantiation() and
|
|
this = base.getAMethodCall("send")
|
|
)
|
|
}
|
|
|
|
override DataFlow::Node getUrl() { result = this.getArgument(0) }
|
|
|
|
override DataFlow::Node getHost() { none() }
|
|
|
|
override DataFlow::Node getADataNode() { result = this.getArgument([2 .. 3]) }
|
|
|
|
/** Gets an event listener with `this` bound to this object. */
|
|
DataFlow::FunctionNode getAnEventListener() {
|
|
static = true and
|
|
result = this.getAnArgument().getAFunctionValue()
|
|
or
|
|
static = false and
|
|
exists(DataFlow::MethodCallNode listen, string name |
|
|
listen = base.getAMethodCall(name) and
|
|
(name = "listen" or name = "listenOnce") and
|
|
base.flowsTo(listen.getArgument(3)) and
|
|
result = listen
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Gets a node that refers to this XHR object.
|
|
*
|
|
* In particular, this can be the receiver of an event handler.
|
|
*/
|
|
DataFlow::SourceNode getAnAlias() {
|
|
static = false and
|
|
result = base
|
|
or
|
|
result = this.getAnEventListener().getReceiver()
|
|
}
|
|
|
|
/**
|
|
* Gets the response type corresponding to `getResponse()` but not
|
|
* for explicitly typed calls like `getResponseJson()`.
|
|
*/
|
|
string getAssignedResponseType() {
|
|
this.getAMethodCall("setResponseType").getArgument(0).mayHaveStringValue(result)
|
|
or
|
|
not exists(this.getAMethodCall("setResponseType")) and
|
|
result = "text"
|
|
}
|
|
|
|
override DataFlow::Node getAResponseDataNode(string responseType, boolean promise) {
|
|
promise = false and
|
|
exists(string method | result = this.getAnAlias().getAMethodCall(method) |
|
|
method = "getResponse" and responseType = this.getAssignedResponseType()
|
|
or
|
|
method = "getResponseHeader" and responseType = "header"
|
|
or
|
|
method = "getResponseHeaders" and responseType = "headers"
|
|
or
|
|
method = "getResponseJson" and responseType = "json"
|
|
or
|
|
method = "getResponseText" and responseType = "text"
|
|
or
|
|
method = "getResponseXml" and responseType = "document"
|
|
or
|
|
method = "getStatusText" and responseType = "text"
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets a reference to an instance of `chrome-remote-interface`.
|
|
*
|
|
* An instantiation of `chrome-remote-interface` either accepts a callback or returns a promise.
|
|
*/
|
|
private API::Node chromeRemoteInterface() {
|
|
exists(API::CallNode call | call = API::moduleImport("chrome-remote-interface").getACall() |
|
|
// the client is inside in a promise.
|
|
result = call.getReturn().getPromised()
|
|
or
|
|
// the client is accessed directly using a callback.
|
|
result = call.getParameter([0 .. 1]).getParameter(0)
|
|
)
|
|
}
|
|
|
|
/**
|
|
* A call to navigate a browser controlled by `chrome-remote-interface` to a specific URL.
|
|
*/
|
|
class ChromeRemoteInterfaceRequest extends ClientRequest::Range, DataFlow::CallNode {
|
|
int optionsArg;
|
|
|
|
ChromeRemoteInterfaceRequest() {
|
|
exists(API::Node instance | instance = chromeRemoteInterface() |
|
|
optionsArg = 0 and
|
|
this = instance.getMember("Page").getMember("navigate").getACall()
|
|
or
|
|
optionsArg = 1 and
|
|
this = instance.getMember("send").getACall() and
|
|
this.getArgument(0).mayHaveStringValue("Page.navigate")
|
|
)
|
|
}
|
|
|
|
override DataFlow::Node getUrl() {
|
|
result = this.getArgument(optionsArg).getALocalSource().getAPropertyWrite("url").getRhs()
|
|
}
|
|
|
|
override DataFlow::Node getHost() { none() }
|
|
|
|
override DataFlow::Node getADataNode() { none() }
|
|
}
|
|
|
|
/**
|
|
* A call to `nugget` that downloads one of more files to a destination determined by an options object given as the second argument.
|
|
*/
|
|
class Nugget extends ClientRequest::Range, DataFlow::CallNode {
|
|
Nugget() { this = DataFlow::moduleImport("nugget").getACall() }
|
|
|
|
override DataFlow::Node getUrl() { result = this.getArgument(0) }
|
|
|
|
override DataFlow::Node getHost() { none() }
|
|
|
|
override DataFlow::Node getADataNode() { none() }
|
|
|
|
override DataFlow::Node getASavePath() {
|
|
result = this.getArgument(1).getALocalSource().getAPropertyWrite("target").getRhs()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A shell execution of `curl` that downloads some file.
|
|
*/
|
|
class CurlDownload extends ClientRequest::Range instanceof SystemCommandExecution {
|
|
CurlDownload() {
|
|
(
|
|
super.getACommandArgument().getStringValue() = "curl" or
|
|
super
|
|
.getACommandArgument()
|
|
.(StringOps::ConcatenationRoot)
|
|
.getConstantStringParts()
|
|
.matches("curl %")
|
|
)
|
|
}
|
|
|
|
override DataFlow::Node getUrl() {
|
|
result = super.getArgumentList().getALocalSource().getAPropertyWrite().getRhs() or
|
|
result = super.getACommandArgument().(StringOps::ConcatenationRoot).getALeaf()
|
|
}
|
|
|
|
override DataFlow::Node getHost() { none() }
|
|
|
|
override DataFlow::Node getADataNode() { none() }
|
|
}
|
|
|
|
/**
|
|
* A model of a URL request made using `jsdom.fromUrl()`.
|
|
*/
|
|
class JSDomFromUrl extends ClientRequest::Range {
|
|
JSDomFromUrl() {
|
|
this = API::moduleImport("jsdom").getMember("JSDOM").getMember("fromURL").getACall()
|
|
}
|
|
|
|
override DataFlow::Node getUrl() { result = this.getArgument(0) }
|
|
|
|
override DataFlow::Node getHost() { none() }
|
|
|
|
override DataFlow::Node getADataNode() { none() }
|
|
}
|
|
|
|
/**
|
|
* Classes and predicates modeling the `apollo-client` library.
|
|
*/
|
|
private module ApolloClient {
|
|
/**
|
|
* Gets a function from `apollo-client` that accepts an options object that may contain a `uri` property.
|
|
*/
|
|
API::Node apolloUriCallee() {
|
|
result = API::moduleImport("apollo-link-http").getMember(["HttpLink", "createHttpLink"])
|
|
or
|
|
result =
|
|
API::moduleImport(["apollo-boost", "apollo-client", "apollo-client-preset"])
|
|
.getMember(["ApolloClient", "HttpLink", "createNetworkInterface"])
|
|
or
|
|
result = API::moduleImport("apollo-link-ws").getMember("WebSocketLink")
|
|
}
|
|
|
|
/**
|
|
* A model of a URL request made using apollo-client.
|
|
*/
|
|
class ApolloClientRequest extends ClientRequest::Range, API::InvokeNode {
|
|
ApolloClientRequest() { this = apolloUriCallee().getAnInvocation() }
|
|
|
|
override DataFlow::Node getUrl() { result = this.getParameter(0).getMember("uri").asSink() }
|
|
|
|
override DataFlow::Node getHost() { none() }
|
|
|
|
override DataFlow::Node getADataNode() { none() }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A model of a URL request made using [form-data](https://www.npmjs.com/package/form-data).
|
|
*/
|
|
class FormDataRequest extends ClientRequest::Range, API::InvokeNode {
|
|
API::Node form;
|
|
|
|
FormDataRequest() {
|
|
form = API::moduleImport("form-data").getInstance() and
|
|
this = form.getMember("submit").getACall()
|
|
}
|
|
|
|
override DataFlow::Node getUrl() { result = this.getArgument(0) }
|
|
|
|
override DataFlow::Node getHost() { result = this.getParameter(0).getMember("host").asSink() }
|
|
|
|
override DataFlow::Node getADataNode() {
|
|
result = form.getMember("append").getACall().getParameter(1).asSink()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Threat model source representing HTTP response data.
|
|
* Marks nodes originating from a client request's response data as tainted.
|
|
*/
|
|
private class ClientRequestThreatModel extends ThreatModelSource::Range {
|
|
ClientRequestThreatModel() { this = any(ClientRequest r).getAResponseDataNode() }
|
|
|
|
override string getThreatModel() { result = "response" }
|
|
|
|
override string getSourceType() { result = "HTTP response data" }
|
|
}
|
|
|
|
/**
|
|
* An additional taint step that captures taint propagation from the receiver of fetch response methods
|
|
* (such as "json", "text", "blob", and "arrayBuffer") to the call result.
|
|
*/
|
|
private class FetchResponseStep extends TaintTracking::AdditionalTaintStep {
|
|
override predicate step(DataFlow::Node node1, DataFlow::Node node2) {
|
|
exists(DataFlow::MethodCallNode call |
|
|
call.getMethodName() in ["json", "text", "blob", "arrayBuffer"] and
|
|
node1 = call.getReceiver() and
|
|
node2 = call and
|
|
call.getNumArgument() = 0
|
|
)
|
|
}
|
|
}
|
|
}
|