mirror of
https://github.com/github/codeql.git
synced 2026-06-20 12:21:13 +02:00
Previously it was not possible to associate a ResponseSendArgument with its header definitions if they did not have the same route handler.
But for calls like `new Response(body, { headers })` the headers are fairly obvious whereas the route handler is unnecessarily hard to find. So we use the direct and obvious association between 'body' and 'headers' in the call.
616 lines
20 KiB
Plaintext
616 lines
20 KiB
Plaintext
/**
|
|
* Provides classes for modeling common HTTP concepts.
|
|
*/
|
|
|
|
import javascript
|
|
private import semmle.javascript.DynamicPropertyAccess
|
|
private import semmle.javascript.dataflow.internal.StepSummary
|
|
private import semmle.javascript.dataflow.internal.CallGraphs
|
|
private import DataFlow::PseudoProperties as PseudoProperties
|
|
|
|
module Http {
|
|
/**
|
|
* A function invocation that causes a redirect response to be sent.
|
|
*/
|
|
abstract class RedirectInvocation extends DataFlow::CallNode {
|
|
/** Gets the argument specifying the URL to redirect to. */
|
|
abstract DataFlow::Node getUrlArgument();
|
|
|
|
/** Gets the route handler this redirect occurs in. */
|
|
abstract RouteHandler getRouteHandler();
|
|
}
|
|
|
|
/**
|
|
* An expression that sets HTTP response headers.
|
|
*
|
|
* Note that header names are case-insensitive, so this class
|
|
* always normalizes them to lower case: arguments representing
|
|
* header names are expected to be lower case, and similarly
|
|
* results representing header names are always lower case.
|
|
*/
|
|
abstract class HeaderDefinition extends DataFlow::Node {
|
|
/**
|
|
* Gets the (lower-case) name of a header set by this definition.
|
|
*/
|
|
abstract string getAHeaderName();
|
|
|
|
/**
|
|
* Holds if the header with (lower-case) name `headerName` is set to `headerValue`.
|
|
*/
|
|
abstract predicate defines(string headerName, string headerValue);
|
|
|
|
/**
|
|
* Gets the handler this definition occurs in.
|
|
*/
|
|
abstract RouteHandler getRouteHandler();
|
|
}
|
|
|
|
/**
|
|
* An expression that sets HTTP response headers implicitly.
|
|
*/
|
|
abstract class ImplicitHeaderDefinition extends HeaderDefinition {
|
|
override string getAHeaderName() { this.defines(result, _) }
|
|
}
|
|
|
|
/**
|
|
* An expression that sets HTTP response headers explicitly.
|
|
*/
|
|
abstract class ExplicitHeaderDefinition extends HeaderDefinition {
|
|
override string getAHeaderName() { this.definesHeaderValue(result, _) }
|
|
|
|
override predicate defines(string headerName, string headerValue) {
|
|
exists(DataFlow::Node e |
|
|
this.definesHeaderValue(headerName, e) and
|
|
headerValue = e.getStringValue()
|
|
)
|
|
}
|
|
|
|
/** Holds if the header with (lower-case) name `headerName` is set to the value of `headerValue`. */
|
|
abstract predicate definesHeaderValue(string headerName, DataFlow::Node headerValue);
|
|
|
|
/** Returns the expression used to compute the header name. */
|
|
abstract DataFlow::Node getNameNode();
|
|
}
|
|
|
|
/**
|
|
* The name of an HTTP request method, in all-uppercase.
|
|
*/
|
|
class RequestMethodName extends string {
|
|
RequestMethodName() {
|
|
this =
|
|
[
|
|
"CHECKOUT", "COPY", "DELETE", "GET", "HEAD", "LOCK", "MERGE", "MKACTIVITY", "MKCOL",
|
|
"MOVE", "M-SEARCH", "NOTIFY", "OPTIONS", "PATCH", "POST", "PURGE", "PUT", "REPORT",
|
|
"SEARCH", "SUBSCRIBE", "TRACE", "UNLOCK", "UNSUBSCRIBE"
|
|
]
|
|
}
|
|
|
|
/**
|
|
* Holds if this kind of HTTP request should be considered free of side effects,
|
|
* such as for `GET` and `HEAD` requests.
|
|
*/
|
|
predicate isSafe() {
|
|
this = ["GET", "HEAD", "OPTIONS", "PRI", "PROPFIND", "REPORT", "SEARCH", "TRACE"]
|
|
}
|
|
|
|
/**
|
|
* Holds if this kind of HTTP request should not generally be considered free of side effects,
|
|
* such as for `POST` or `PUT` requests.
|
|
*/
|
|
predicate isUnsafe() { not this.isSafe() }
|
|
}
|
|
|
|
/**
|
|
* An expression whose value is sent as (part of) the body of an HTTP response.
|
|
*/
|
|
abstract class ResponseBody extends DataFlow::Node {
|
|
/**
|
|
* Gets the route handler that sends this expression.
|
|
*/
|
|
abstract RouteHandler getRouteHandler();
|
|
|
|
/**
|
|
* Gets a header definition associated with this response body, if it they are provided
|
|
* by the same call.
|
|
*/
|
|
HeaderDefinition getAnAssociatedHeaderDefinition() { none() }
|
|
}
|
|
|
|
/**
|
|
* An expression whose value is included directly (and not, say, via a template)
|
|
* in the body of an HTTP response.
|
|
*/
|
|
abstract class ResponseSendArgument extends ResponseBody { }
|
|
|
|
/**
|
|
* An expression that sets a cookie in an HTTP response.
|
|
*/
|
|
abstract class CookieDefinition extends DataFlow::Node {
|
|
/**
|
|
* Gets the argument, if any, specifying the raw cookie header.
|
|
*/
|
|
DataFlow::Node getHeaderArgument() { none() }
|
|
|
|
/**
|
|
* Gets the argument, if any, specifying the cookie name.
|
|
*/
|
|
DataFlow::Node getNameArgument() { none() }
|
|
|
|
/**
|
|
* Gets the argument, if any, specifying the cookie value.
|
|
*/
|
|
DataFlow::Node getValueArgument() { none() }
|
|
|
|
/** Gets the route handler that sets this cookie. */
|
|
abstract RouteHandler getRouteHandler();
|
|
}
|
|
|
|
/**
|
|
* An expression that sets the `Set-Cookie` header of an HTTP response.
|
|
*/
|
|
class SetCookieHeader extends CookieDefinition instanceof HeaderDefinition {
|
|
SetCookieHeader() { super.getAHeaderName() = "set-cookie" }
|
|
|
|
override DataFlow::Node getHeaderArgument() {
|
|
this.(ExplicitHeaderDefinition).definesHeaderValue("set-cookie", result)
|
|
}
|
|
|
|
override RouteHandler getRouteHandler() { result = HeaderDefinition.super.getRouteHandler() }
|
|
}
|
|
|
|
/**
|
|
* An expression that creates a new server.
|
|
*/
|
|
abstract class ServerDefinition extends DataFlow::Node {
|
|
/**
|
|
* Gets a route handler of the server.
|
|
*/
|
|
abstract RouteHandler getARouteHandler();
|
|
}
|
|
|
|
/**
|
|
* A callback for handling a request on some route on a server.
|
|
*/
|
|
abstract class RouteHandler extends DataFlow::Node {
|
|
/**
|
|
* Gets a header this handler sets.
|
|
*
|
|
* Note that header names are case-insensitive; this predicate always converts
|
|
* header names to lower case.
|
|
*/
|
|
abstract HeaderDefinition getAResponseHeader(string name);
|
|
|
|
/**
|
|
* Gets a request object originating from this route handler.
|
|
*
|
|
* Use `RequestSource.ref()` to get reference to this request object.
|
|
*/
|
|
final Servers::RequestSource getARequestSource() { result.getRouteHandler() = this }
|
|
|
|
/**
|
|
* Gets a response object originating from this route handler.
|
|
*
|
|
* Use `ResponseSource.ref()` to get reference to this response object.
|
|
*/
|
|
final Servers::ResponseSource getAResponseSource() { result.getRouteHandler() = this }
|
|
|
|
/**
|
|
* Gets an expression that contains a request object handled
|
|
* by this handler.
|
|
*/
|
|
RequestNode getARequestNode() { result.getRouteHandler() = this }
|
|
|
|
/**
|
|
* Gets an expression that contains a response object provided
|
|
* by this handler.
|
|
*/
|
|
ResponseNode getAResponseNode() { result.getRouteHandler() = this }
|
|
}
|
|
|
|
/**
|
|
* Holds if there exists a step from `pred` to `succ` for a RouteHandler - beyond the usual steps defined by TypeTracking.
|
|
*/
|
|
predicate routeHandlerStep(DataFlow::SourceNode pred, DataFlow::SourceNode succ) {
|
|
// A forwarding call
|
|
DataFlow::functionOneWayForwardingStep(pred.getALocalUse(), succ)
|
|
or
|
|
// a container containing route-handlers.
|
|
exists(Http::RouteHandlerCandidateContainer container | pred = container.getRouteHandler(succ))
|
|
or
|
|
// (function (req, res) {}).bind(this);
|
|
exists(DataFlow::PartialInvokeNode call |
|
|
succ = call.getBoundFunction(any(DataFlow::Node n | pred.flowsTo(n)), 0)
|
|
)
|
|
or
|
|
// references to class methods
|
|
succ = CallGraph::callgraphStep(pred, DataFlow::TypeTracker::end())
|
|
}
|
|
|
|
/**
|
|
* An expression that sets up a route on a server.
|
|
*/
|
|
abstract class RouteSetup extends DataFlow::Node { }
|
|
|
|
/** A dataflow node that may contain a request object. */
|
|
abstract class RequestNode extends DataFlow::Node {
|
|
/** Gets the route handler that handles this request. */
|
|
abstract RouteHandler getRouteHandler();
|
|
}
|
|
|
|
/** An dataflow node that may contain a response object. */
|
|
abstract class ResponseNode extends DataFlow::Node {
|
|
/** Gets the route handler that handles this request. */
|
|
abstract RouteHandler getRouteHandler();
|
|
}
|
|
|
|
/**
|
|
* Boiler-plate implementation of a `Server` and its associated classes.
|
|
* Made for easily defining new HTTP servers
|
|
*/
|
|
module Servers {
|
|
/**
|
|
* A standard server definition.
|
|
*/
|
|
abstract class StandardServerDefinition extends ServerDefinition {
|
|
override RouteHandler getARouteHandler() { result.(StandardRouteHandler).getServer() = this }
|
|
|
|
private DataFlow::SourceNode ref(DataFlow::TypeTracker t) {
|
|
t.start() and
|
|
result = this.getALocalSource()
|
|
or
|
|
exists(DataFlow::TypeTracker t2 | result = this.ref(t2).track(t2, t))
|
|
}
|
|
|
|
/** Gets a data flow node referring to this server. */
|
|
DataFlow::SourceNode ref() { result = this.ref(DataFlow::TypeTracker::end()) }
|
|
}
|
|
|
|
/**
|
|
* A standard route handler.
|
|
*/
|
|
abstract class StandardRouteHandler extends RouteHandler {
|
|
override HeaderDefinition getAResponseHeader(string name) {
|
|
result.getRouteHandler() = this and
|
|
result.getAHeaderName() = name
|
|
}
|
|
|
|
/**
|
|
* Gets the server this route handler is registered on.
|
|
*/
|
|
DataFlow::Node getServer() {
|
|
exists(StandardRouteSetup setup | setup.getARouteHandler() = this |
|
|
result = setup.getServer()
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A request source, that is, a data flow node through which
|
|
* a request object enters the flow graph, such as the request
|
|
* parameter of a route handler.
|
|
*/
|
|
abstract class RequestSource extends DataFlow::Node {
|
|
/**
|
|
* Gets the route handler that handles this request.
|
|
*/
|
|
abstract RouteHandler getRouteHandler();
|
|
|
|
private DataFlow::SourceNode ref(DataFlow::TypeTracker t) {
|
|
t.start() and
|
|
result = this
|
|
or
|
|
exists(DataFlow::TypeTracker t2 | result = this.ref(t2).track(t2, t))
|
|
}
|
|
|
|
/** Gets a `SourceNode` that refers to this request object. */
|
|
DataFlow::SourceNode ref() { result = this.ref(DataFlow::TypeTracker::end()) }
|
|
}
|
|
|
|
/**
|
|
* A response source, that is, a data flow node through which
|
|
* a response object enters the flow graph, such as the response
|
|
* parameter of a route handler.
|
|
*/
|
|
abstract class ResponseSource extends DataFlow::Node {
|
|
/**
|
|
* Gets the route handler that provides this response.
|
|
*/
|
|
abstract RouteHandler getRouteHandler();
|
|
|
|
private DataFlow::SourceNode ref(DataFlow::TypeTracker t) {
|
|
t.start() and
|
|
result = this
|
|
or
|
|
exists(DataFlow::TypeTracker t2 | result = this.ref(t2).track(t2, t))
|
|
}
|
|
|
|
/** Gets a `SourceNode` that refers to this response object. */
|
|
DataFlow::SourceNode ref() { result = this.ref(DataFlow::TypeTracker::end()) }
|
|
}
|
|
|
|
/**
|
|
* A request expression arising from a request source.
|
|
*/
|
|
class StandardRequestNode extends RequestNode {
|
|
RequestSource src;
|
|
|
|
StandardRequestNode() { src.ref().flowsTo(this) }
|
|
|
|
override RouteHandler getRouteHandler() { result = src.getRouteHandler() }
|
|
}
|
|
|
|
/**
|
|
* A response expression arising from a response source.
|
|
*/
|
|
class StandardResponseNode extends ResponseNode {
|
|
ResponseSource src;
|
|
|
|
StandardResponseNode() { src.ref().flowsTo(this) }
|
|
|
|
override RouteHandler getRouteHandler() { result = src.getRouteHandler() }
|
|
}
|
|
|
|
/**
|
|
* A standard header definition.
|
|
*/
|
|
abstract class StandardHeaderDefinition extends ExplicitHeaderDefinition,
|
|
DataFlow::MethodCallNode
|
|
{
|
|
override predicate definesHeaderValue(string headerName, DataFlow::Node headerValue) {
|
|
headerName = this.getNameNode().getStringValue().toLowerCase() and
|
|
headerValue = this.getArgument(1)
|
|
}
|
|
|
|
override DataFlow::Node getNameNode() { result = this.getArgument(0) }
|
|
}
|
|
|
|
/**
|
|
* A standard route setup on a server.
|
|
*/
|
|
abstract class StandardRouteSetup extends RouteSetup {
|
|
/**
|
|
* Gets a route handler that is defined by this setup.
|
|
*/
|
|
pragma[nomagic]
|
|
abstract DataFlow::SourceNode getARouteHandler();
|
|
|
|
/**
|
|
* Gets the server on which this route setup sets up routes.
|
|
*/
|
|
abstract DataFlow::Node getServer();
|
|
}
|
|
|
|
/**
|
|
* A parameter containing data received by a NodeJS HTTP server.
|
|
* E.g. `chunk` in: `http.createServer().on('request', (req, res) => req.on("data", (chunk) => ...))`.
|
|
*/
|
|
private class ServerRequestDataEvent extends RemoteFlowSource, DataFlow::ParameterNode {
|
|
ServerRequestDataEvent() {
|
|
exists(DataFlow::MethodCallNode mcn, RequestSource req |
|
|
mcn = req.ref().getAMethodCall(EventEmitter::on())
|
|
|
|
|
mcn.getArgument(0).mayHaveStringValue("data") and
|
|
this = mcn.getABoundCallbackParameter(1, 0)
|
|
)
|
|
}
|
|
|
|
override string getSourceType() { result = "NodeJS HTTP server data event" }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An access to a user-controlled HTTP request input.
|
|
*/
|
|
abstract class RequestInputAccess extends RemoteFlowSource {
|
|
override string getSourceType() { result = "Server request " + this.getKind() }
|
|
|
|
/**
|
|
* Gets the route handler whose request input is accessed.
|
|
*/
|
|
abstract RouteHandler getRouteHandler();
|
|
|
|
/**
|
|
* Gets the kind of the accessed input,
|
|
* Can be one of "parameter", "header", "body", "url", "cookie".
|
|
*
|
|
* Note that this predicate is functional.
|
|
*/
|
|
abstract string getKind();
|
|
|
|
/**
|
|
* Holds if this part of the request may be controlled by a third party,
|
|
* that is, an agent other than the one who sent the request.
|
|
*
|
|
* This is true for the URL, query parameters, and request body.
|
|
* These can be controlled by a malicious third party in the following scenarios:
|
|
*
|
|
* - The user clicks a malicious link or is otherwise redirected to a malicious URL.
|
|
* - The user visits a web site that initiates a form submission or AJAX request on their behalf.
|
|
*
|
|
* In these cases, the request is technically sent from the user's browser, but
|
|
* the user is not in direct control of the URL or POST body.
|
|
*
|
|
* Headers are never considered third-party controllable by this predicate, although the
|
|
* third party does have some control over the the Referer and Origin headers.
|
|
*/
|
|
predicate isThirdPartyControllable() { this.getKind() = ["parameter", "url", "body"] }
|
|
}
|
|
|
|
/**
|
|
* An access to a header on an incoming HTTP request.
|
|
*/
|
|
abstract class RequestHeaderAccess extends RequestInputAccess {
|
|
/**
|
|
* Gets the lower-case name of an HTTP header from which this input is derived,
|
|
* if this can be determined.
|
|
*
|
|
* When the name of the header is unknown, this has no result.
|
|
*/
|
|
abstract string getAHeaderName();
|
|
}
|
|
|
|
/**
|
|
* A node that looks like a route setup on a server.
|
|
*
|
|
* This is useful for tasks such as heuristic analyses
|
|
* and exploratory queries.
|
|
*/
|
|
abstract class RouteSetupCandidate extends DataFlow::ValueNode {
|
|
/**
|
|
* Gets an expression that contains a route handler of this setup.
|
|
*/
|
|
abstract DataFlow::ValueNode getARouteHandlerArg();
|
|
}
|
|
|
|
/**
|
|
* A function that looks like a route handler.
|
|
*
|
|
* This is useful for tasks such as heuristic analyses
|
|
* and exploratory queries.
|
|
*/
|
|
abstract class RouteHandlerCandidate extends DataFlow::FunctionNode { }
|
|
|
|
/**
|
|
* An expression that creates a route handler that parses cookies
|
|
*/
|
|
abstract class CookieMiddlewareInstance extends DataFlow::SourceNode {
|
|
/**
|
|
* Gets a secret key used for signed cookies.
|
|
*/
|
|
abstract DataFlow::Node getASecretKey();
|
|
}
|
|
|
|
/**
|
|
* A key used for signed cookies, viewed as a `CryptographicKey`.
|
|
*/
|
|
class CookieCryptographicKey extends CryptographicKey {
|
|
CookieCryptographicKey() { this = any(CookieMiddlewareInstance instance).getASecretKey() }
|
|
}
|
|
|
|
/**
|
|
* An object that contains one or more potential route handlers.
|
|
*/
|
|
class RouteHandlerCandidateContainer extends DataFlow::Node instanceof RouteHandlerCandidateContainer::Range
|
|
{
|
|
/**
|
|
* Gets the route handler in this container that is accessed at `access`.
|
|
*/
|
|
DataFlow::SourceNode getRouteHandler(DataFlow::SourceNode access) {
|
|
result = super.getRouteHandler(access)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Provides classes for working with objects that may contain one or more route handlers.
|
|
*/
|
|
module RouteHandlerCandidateContainer {
|
|
private DataFlow::SourceNode ref(DataFlow::TypeTracker t, RouteHandlerCandidateContainer c) {
|
|
t.start() and result = c
|
|
or
|
|
exists(DataFlow::TypeTracker t2 | result = ref(t2, c).track(t2, t))
|
|
}
|
|
|
|
private DataFlow::SourceNode ref(RouteHandlerCandidateContainer c) {
|
|
result = ref(DataFlow::TypeTracker::end(), c)
|
|
}
|
|
|
|
/**
|
|
* A container for one or more potential route handlers.
|
|
*
|
|
* Extend this class and implement its abstract member predicates to model additional
|
|
* containers.
|
|
*/
|
|
abstract class Range extends DataFlow::SourceNode {
|
|
/**
|
|
* Gets the route handler in this container that is accessed at `access`.
|
|
*/
|
|
abstract DataFlow::SourceNode getRouteHandler(DataFlow::SourceNode access);
|
|
}
|
|
|
|
/**
|
|
* An object that contains one or more potential route handlers.
|
|
*/
|
|
private class ContainerObject extends Range {
|
|
ContainerObject() {
|
|
(
|
|
this instanceof DataFlow::ObjectLiteralNode
|
|
or
|
|
exists(DataFlow::CallNode create | this = create |
|
|
create = DataFlow::globalVarRef("Object").getAMemberCall("create") and
|
|
create.getArgument(0).asExpr() instanceof NullLiteral
|
|
)
|
|
) and
|
|
getAPossiblyDecoratedHandler(_).flowsTo(this.getAPropertyWrite().getRhs())
|
|
}
|
|
|
|
override DataFlow::SourceNode getRouteHandler(DataFlow::SourceNode access) {
|
|
result instanceof RouteHandlerCandidate and
|
|
exists(DataFlow::PropWrite write, DataFlow::PropRead read |
|
|
access = read and
|
|
ref(this).getAPropertyRead() = read and
|
|
getAPossiblyDecoratedHandler(result).flowsTo(write.getRhs()) and
|
|
write = this.getAPropertyWrite()
|
|
|
|
|
write.getPropertyName() = read.getPropertyName()
|
|
or
|
|
exists(EnumeratedPropName prop | access = prop.getASourceProp())
|
|
or
|
|
read = DataFlow::lvalueNode(any(ForOfStmt stmt).getLValue())
|
|
or
|
|
// for forwarding calls to an element where the key is determined by the request.
|
|
getRequestParameterRead().flowsToExpr(read.getPropertyNameExpr())
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets a (chained) property-read/method-call on the request parameter of the route-handler `f`.
|
|
*/
|
|
private DataFlow::SourceNode getRequestParameterRead() {
|
|
result = any(RouteHandlerCandidate f).getParameter(0)
|
|
or
|
|
result = getRequestParameterRead().getAPropertyRead()
|
|
or
|
|
result = getRequestParameterRead().getAMethodCall()
|
|
}
|
|
|
|
/**
|
|
* Gets a node that is either `candidate`, or a call that decorates `candidate`.
|
|
*/
|
|
DataFlow::SourceNode getAPossiblyDecoratedHandler(RouteHandlerCandidate candidate) {
|
|
result = candidate
|
|
or
|
|
DataFlow::functionOneWayForwardingStep(candidate, result)
|
|
}
|
|
|
|
private string mapValueProp() {
|
|
result = [PseudoProperties::mapValueAll(), PseudoProperties::mapValueUnknownKey()]
|
|
}
|
|
|
|
/**
|
|
* A collection that contains one or more route potential handlers.
|
|
*/
|
|
private class ContainerCollection extends Http::RouteHandlerCandidateContainer::Range,
|
|
DataFlow::NewNode
|
|
{
|
|
ContainerCollection() {
|
|
this = DataFlow::globalVarRef("Map").getAnInstantiation() and // restrict to Map for now
|
|
exists(DataFlow::Node use |
|
|
DataFlow::SharedTypeTrackingStep::storeStep(use, this, mapValueProp()) and
|
|
use.getALocalSource() instanceof RouteHandlerCandidate
|
|
)
|
|
}
|
|
|
|
override DataFlow::SourceNode getRouteHandler(DataFlow::SourceNode access) {
|
|
exists(DataFlow::Node input, string key, DataFlow::Node loadFrom |
|
|
getAPossiblyDecoratedHandler(result).flowsTo(input) and
|
|
DataFlow::SharedTypeTrackingStep::storeStep(input, this, key) and
|
|
ref(this).flowsTo(loadFrom) and
|
|
DataFlow::SharedTypeTrackingStep::loadStep(loadFrom, access,
|
|
[key, PseudoProperties::mapValueAll()])
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|