Files
codeql/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll
2025-03-25 10:45:09 +01:00

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
)
}
}
}