Files
codeql/javascript/ql/lib/semmle/javascript/frameworks/HTTP.qll
2024-01-22 09:11:35 +01:00

610 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();
}
/**
* 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()])
)
}
}
}
}