mirror of
https://github.com/github/codeql.git
synced 2026-01-09 20:50:21 +01:00
1002 lines
36 KiB
Plaintext
1002 lines
36 KiB
Plaintext
/**
|
|
* A model of routing trees, describing the composition of route handlers and middleware functions
|
|
* in a web server application. See `Routing::Node` for more details.
|
|
*/
|
|
|
|
private import javascript
|
|
private import semmle.javascript.dataflow.internal.FlowSteps as FlowSteps
|
|
private import semmle.javascript.dataflow.internal.StepSummary
|
|
private import semmle.javascript.DynamicPropertyAccess
|
|
|
|
/**
|
|
* A model of routing trees, describing the composition of route handlers and middleware functions
|
|
* in a web server application. See `Routing::Node` for more details.
|
|
*/
|
|
module Routing {
|
|
private newtype TNode =
|
|
/**
|
|
* A data flow node whose value corresponds to a route node.
|
|
*/
|
|
MkValueNode(ValueNode::Range range) or
|
|
/**
|
|
* A place where a route is installed on a mutable router object, or where a mutable
|
|
* router object escapes into a function leading to such route setups.
|
|
*
|
|
* The call performing the route setup usually returns the router itself, not
|
|
* the particular route installed here, which is why this is not a value node.
|
|
*/
|
|
MkRouteSetup(RouteSetup::Base range) or
|
|
/**
|
|
* A node representing the state of a mutable router object at the exit of `container`.
|
|
*
|
|
* When routers are passed around and mutated from multiple functions, we need to reason about the relative
|
|
* ordering of route setups in different functions. To simplify this, a separate node is generated for a each
|
|
* function that mutates the router.
|
|
*/
|
|
MkRouter(Router::Range range, StmtContainer container) {
|
|
routerIsLiveInContainer(range, container)
|
|
}
|
|
|
|
private predicate routerIsLiveInContainer(Router::Range router, StmtContainer container) {
|
|
container = router.getContainer()
|
|
or
|
|
exists(RouteSetup::Range setup, ControlFlowNode cfg |
|
|
setup.isInstalledAt(router, cfg) and
|
|
container = cfg.getContainer()
|
|
)
|
|
or
|
|
// 'container' contains a call to a function in which 'router' is live
|
|
exists(DataFlow::InvokeNode invoke, Function f |
|
|
routerIsLiveInContainer(router, f) and
|
|
not f = router.getContainer() and // 'f' does not contain the creation of the router
|
|
FlowSteps::calls(invoke, f) and
|
|
container = invoke.getContainer()
|
|
)
|
|
}
|
|
|
|
/** Gets the routing node corresponding to the value of `node`. */
|
|
Node getNode(DataFlow::Node node) { result = MkValueNode(node) }
|
|
|
|
/**
|
|
* Gets the routing node corresponding to the route installed at the given route setup.
|
|
*
|
|
* This is not generally the same as `getNode(call)`, since the route setup method can return a value
|
|
* that does not correspond to the route that was just installed.
|
|
* Typically this occurs when the route setup method is chainable and returns the router itself.
|
|
*/
|
|
Node getRouteSetupNode(DataFlow::Node call) { result = MkRouteSetup(call) }
|
|
|
|
/**
|
|
* A node in a routing tree modeling the composition of middleware functions and route handlers.
|
|
*
|
|
* More precisely, this is a node in a graph representing a set of possible routing trees, as the
|
|
* concrete shape of the routing tree may be depend on branching control flow.
|
|
*
|
|
* Each node represents a function that can receive an incoming request, though not necessarily
|
|
* a function with an explicit body in the source code.
|
|
*
|
|
* A node may either consume the request, dispatching to its first child, or pass it on to its successor
|
|
* in the tree. The successor is the next sibling, or in case there is no next sibling, it is the next sibling
|
|
* of the first ancestor that has a next sibling.
|
|
*/
|
|
class Node extends TNode {
|
|
/** Gets a textual representation of this element. */
|
|
string toString() { none() } // Overridden in subclass
|
|
|
|
/**
|
|
* Holds if this element is at the specified location.
|
|
* The location spans column `startcolumn` of line `startline` to
|
|
* column `endcolumn` of line `endline` in file `filepath`.
|
|
* For more information, see
|
|
* [Locations](https://codeql.github.com/docs/writing-codeql-queries/providing-locations-in-codeql-queries).
|
|
*/
|
|
predicate hasLocationInfo(
|
|
string filepath, int startline, int startcolumn, int endline, int endcolumn
|
|
) {
|
|
none()
|
|
} // overridden in subclass
|
|
|
|
/**
|
|
* Gets the next sibling of this node in the routing tree.
|
|
*/
|
|
final Node getNextSibling() { areSiblings(this, result) }
|
|
|
|
/**
|
|
* Gets the previous sibling of this node in the routing tree.
|
|
*/
|
|
final Node getPreviousSibling() { areSiblings(result, this) }
|
|
|
|
/**
|
|
* Gets a child of this node in the routing tree.
|
|
*/
|
|
Node getAChild() { none() } // Overridden in subclass
|
|
|
|
/**
|
|
* Gets the parent of this node in the routing tree.
|
|
*/
|
|
final Node getParent() { result.getAChild() = this }
|
|
|
|
/**
|
|
* Gets the first child of this node in the routing tree.
|
|
*/
|
|
Node getFirstChild() { none() } // Overridden in subclass
|
|
|
|
/**
|
|
* Gets the last child of this node in the routing tree.
|
|
*/
|
|
Node getLastChild() { none() } // Overridden in subclass
|
|
|
|
/**
|
|
* Gets the root node of this node in the routing tree.
|
|
*/
|
|
final RootNode getRootNode() { this = result.getADescendant() }
|
|
|
|
/**
|
|
* Holds if this node may invoke its continuation after having dispatched the
|
|
* request to its children, that is, the incoming request may be partially processed by
|
|
* this subtree, and subsequently passed on to the successor.
|
|
*/
|
|
predicate mayResumeDispatch() {
|
|
this.getLastChild().mayResumeDispatch()
|
|
or
|
|
exists(this.(RouteHandler).getAContinuationInvocation())
|
|
or
|
|
// Leaf nodes that aren't functions are assumed to invoke their continuation
|
|
not exists(this.getLastChild()) and
|
|
not this instanceof RouteHandler
|
|
or
|
|
this instanceof MkRouter
|
|
}
|
|
|
|
/**
|
|
* Like `mayResumeDispatch` but without the assumption that functions with an unknown
|
|
* implementation invoke their continuation.
|
|
*/
|
|
predicate definitelyResumesDispatch() {
|
|
this.getLastChild().definitelyResumesDispatch()
|
|
or
|
|
exists(this.(RouteHandler).getAContinuationInvocation())
|
|
or
|
|
this instanceof MkRouter
|
|
}
|
|
|
|
/** Gets the parent of this node, provided that this node may invoke its continuation. */
|
|
private Node getContinuationParent() {
|
|
result = this.getParent() and
|
|
result.mayResumeDispatch()
|
|
}
|
|
|
|
/**
|
|
* Gets a path prefix to be matched against the path of incoming requests.
|
|
*
|
|
* If the prefix matches, the request is dispatched to the first child, with a modified path
|
|
* where the matched prefix has been removed. For example, if the prefix is `/foo` and the incoming
|
|
* request has path `/foo/bar`, a request with path `/bar` is dispatched to the first child.
|
|
*
|
|
* If the prefix does not match, the request is passed on to the continuation.
|
|
*/
|
|
string getRelativePath() { none() } // Overridden in subclass
|
|
|
|
/**
|
|
* Holds if this is a node where the request can flow from one child to the next.
|
|
*/
|
|
private predicate isFork() {
|
|
exists(Node child |
|
|
child = this.getAChild() and
|
|
child.mayResumeDispatch() and
|
|
exists(child.getNextSibling())
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Gets the path prefix needed to reach this node from the given ancestor, that is, the concatenation
|
|
* of all relative paths between this node and the ancestor.
|
|
*
|
|
* To restrict the size of the predicate, this is only available for the ancestors that are "fork" nodes,
|
|
* that is, a node that has siblings (i.e. multiple children).
|
|
*/
|
|
private string getPathFromFork(Node fork) {
|
|
this.isFork() and
|
|
this = fork and
|
|
result = ""
|
|
or
|
|
exists(Node parent | parent = this.getParent() |
|
|
not exists(parent.getRelativePath()) and
|
|
result = parent.getPathFromFork(fork)
|
|
or
|
|
result = parent.getPathFromFork(fork) + parent.getRelativePath() and
|
|
result.length() < 100
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Gets an HTTP method required to reach this node from the given ancestor, or `*` if any method
|
|
* can be used.
|
|
*
|
|
* To restrict the size of the predicate, this is only available for the ancestors that are "fork" nodes,
|
|
* that is, a node that has siblings (i.e. multiple children).
|
|
*/
|
|
private string getHttpMethodFromFork(Node fork) {
|
|
this.isFork() and
|
|
this = fork and
|
|
(
|
|
result = this.getOwnHttpMethod()
|
|
or
|
|
not exists(this.getOwnHttpMethod()) and
|
|
result = "*"
|
|
)
|
|
or
|
|
result = this.getParent().getHttpMethodFromFork(fork) and
|
|
(
|
|
// Only the ancestor restricts the HTTP method
|
|
not exists(this.getOwnHttpMethod())
|
|
or
|
|
// Intersect permitted HTTP methods
|
|
result = this.getOwnHttpMethod()
|
|
)
|
|
or
|
|
// The ancestor allows any HTTP method, but this node restricts it
|
|
this.getParent().getHttpMethodFromFork(fork) = "*" and
|
|
result = this.getOwnHttpMethod()
|
|
}
|
|
|
|
/**
|
|
* Holds if `guard` has processed the incoming request strictly prior to this node.
|
|
*/
|
|
pragma[inline]
|
|
private predicate isGuardedByNodeInternal(Node guard) {
|
|
// Look for a common ancestor `fork` whose child leading to `guard` ("base1") precedes
|
|
// the child leading to `this` ("base2").
|
|
//
|
|
// Schematically:
|
|
// fork
|
|
// / \
|
|
// base1 base2
|
|
// | | <-- (zero or more steps)
|
|
// guard this
|
|
exists(Node base1, Node base2, Node fork |
|
|
base1 = guard.getContinuationParent*() and
|
|
base2 = base1.getNextSibling+() and
|
|
this = base2.getAChild*() and
|
|
fork = base1.getParent() and
|
|
isEitherPrefixOfTheOther(this.getPathFromFork(fork), guard.getPathFromFork(fork)) and
|
|
areHttpMethodsMatching(base1.getHttpMethodFromFork(fork), base2.getHttpMethodFromFork(fork))
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Holds if `node` has processed the incoming request strictly prior to this node.
|
|
*/
|
|
pragma[inline]
|
|
predicate isGuardedByNode(Node node) {
|
|
this.isGuardedByNodeInternal(pragma[only_bind_out](node))
|
|
}
|
|
|
|
/**
|
|
* Holds if the middleware corresponding to `node` has processed the incoming request strictly prior to this node.
|
|
*/
|
|
pragma[inline]
|
|
predicate isGuardedBy(DataFlow::Node node) { this.isGuardedByNode(getNode(node)) }
|
|
|
|
/**
|
|
* Gets an HTTP method name which this node will accept, or nothing if the node accepts all HTTP methods, not
|
|
* taking into account the context from ancestors or children nodes.
|
|
*/
|
|
Http::RequestMethodName getOwnHttpMethod() { none() } // Overridden in subclass
|
|
|
|
private Node getAUseSiteInRouteSetup() {
|
|
if this.getParent() instanceof RouteSetup
|
|
then result = this
|
|
else result = this.getParent().getAUseSiteInRouteSetup()
|
|
}
|
|
|
|
/** Gets a place where this route node is installed as a route handler. */
|
|
Node getRouteInstallation() {
|
|
result = this.getAUseSiteInRouteSetup()
|
|
or
|
|
not exists(this.getAUseSiteInRouteSetup()) and
|
|
result = this
|
|
}
|
|
|
|
/**
|
|
* Gets a node whose value can be accessed via the given access path on the `n`th route handler parameter,
|
|
* from any route handler that follows after this one.
|
|
*
|
|
* This predicate may be overridden by framework models and only accounts for assignments made by the framework;
|
|
* not necessarily assignments that are explicit in the application code.
|
|
*
|
|
* For example, in the context of Express, the `app` object is available as `req.app`:
|
|
* ```js
|
|
* app.get('/', (req, res) => {
|
|
* req.app; // alias for 'app'
|
|
* })
|
|
* ```
|
|
* This can be modeled by mapping `(0, "app")` to the `app` data-flow node (`n=0` corresponds
|
|
* to the `req` parameter).
|
|
*/
|
|
DataFlow::Node getValueImplicitlyStoredInAccessPath(int n, string path) { none() }
|
|
}
|
|
|
|
/** Holds if `pred` and `succ` are adjacent siblings and `succ` is installed after `pred`. */
|
|
private predicate areSiblings(Node pred, Node succ) {
|
|
exists(ValueNode::Range base, int n |
|
|
pred = base.getChild(n) and
|
|
succ = base.getChild(n + 1)
|
|
)
|
|
or
|
|
exists(RouteSetup::Range base, int n |
|
|
pred = base.getChild(n) and
|
|
succ = base.getChild(n + 1)
|
|
)
|
|
or
|
|
exists(Router::Range router, ControlFlowNode cfgNode |
|
|
isInstalledAt(succ, router, cfgNode) and
|
|
pred = getMostRecentRouteSetupAt(router, cfgNode.getAPredecessor()) and
|
|
pred != succ // simplify analysis of loops
|
|
)
|
|
}
|
|
|
|
/** Holds if `a` is a prefix of `b` or the other way around. */
|
|
bindingset[a, b]
|
|
private predicate isEitherPrefixOfTheOther(string a, string b) {
|
|
a = b + any(string s) or b = a + any(string s)
|
|
}
|
|
|
|
/** Holds if `a` and `b` are the same HTTP method name or either of them is `*`. */
|
|
bindingset[a, b]
|
|
private predicate areHttpMethodsMatching(string a, string b) { a = "*" or b = "*" or a = b }
|
|
|
|
/**
|
|
* Companion module to the `Node` class, containing abstract classes
|
|
* that can be used to extend the routing model.
|
|
*/
|
|
module ValueNode {
|
|
/**
|
|
* A node in the routing tree which corresponds to a data-flow node,
|
|
* and has a linear sequence of children (or no children).
|
|
*
|
|
* This class can be extended to contribute new kinds of nodes to tree,
|
|
* though in common cases it is preferrable to extend one of the more specialized classes:
|
|
* - `Routing::ValueNode::UseSite` to mark values that are used as a route handler,
|
|
* - `Routing::ValueNode::WithArguments` for nodes with an indexed sequence of children,
|
|
* - `Routing::RouteSetup::MethodCall` for nodes manipulating a router object
|
|
*/
|
|
abstract class Range extends DataFlow::Node {
|
|
/** Gets the `n`th child of this route node. */
|
|
Node getChild(int n) { none() }
|
|
|
|
/** Gets the number of children of this route node. */
|
|
final int getNumChild() { result = count(int n | exists(this.getChild(n))) }
|
|
|
|
/**
|
|
* Gets a path prefix to be matched against the path of incoming requests.
|
|
*
|
|
* If the prefix matches, the request is dispatched to the first child, with a modified path
|
|
* where the matched prefix has been removed. For example, if the prefix is `/foo` and the incoming
|
|
* request has path `/foo/bar`, a request with path `/bar` is dispatched to the first child.
|
|
*
|
|
* If the prefix does not match, the request is passed on to the continuation.
|
|
*/
|
|
string getRelativePath() { none() }
|
|
|
|
/**
|
|
* Gets an HTTP request method name (in upper case) matched by this node, or nothing
|
|
* if all HTTP request method names are accepted.
|
|
*/
|
|
Http::RequestMethodName getHttpMethod() { none() }
|
|
}
|
|
|
|
private class ValueNodeImpl extends Node, MkValueNode {
|
|
Range range;
|
|
|
|
ValueNodeImpl() { this = MkValueNode(range) }
|
|
|
|
override string toString() { result = range.toString() }
|
|
|
|
override predicate hasLocationInfo(
|
|
string filepath, int startline, int startcolumn, int endline, int endcolumn
|
|
) {
|
|
range.hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
|
|
}
|
|
|
|
override Node getAChild() { result = range.getChild(_) }
|
|
|
|
override Node getFirstChild() { result = range.getChild(0) }
|
|
|
|
override Node getLastChild() { result = range.getChild(range.getNumChild() - 1) }
|
|
|
|
override string getRelativePath() { result = range.getRelativePath() }
|
|
|
|
override Http::RequestMethodName getOwnHttpMethod() { result = range.getHttpMethod() }
|
|
}
|
|
|
|
private StepSummary routeStepSummary() {
|
|
// Do not allow call steps as they lead to loss of context.
|
|
// Such steps are usually handled by the 'ImpliedRouteHandler' class.
|
|
result = LevelStep() or result = ReturnStep()
|
|
}
|
|
|
|
/**
|
|
* A node that is used as a route handler.
|
|
*
|
|
* Values that flow to this use site are themselves considered use sites, and are
|
|
* considered children of this one. (Intuitively, requests dispatched to this use-site
|
|
* are delagated to any node that flows here.)
|
|
*
|
|
* Framework models may extend this class to mark nodes as being use sites.
|
|
*/
|
|
abstract class UseSite extends Range {
|
|
/**
|
|
* Gets a data flow node that flows to this use-site in one step.
|
|
*/
|
|
DataFlow::Node getSource() {
|
|
result = this.getALocalSource()
|
|
or
|
|
StepSummary::smallstep(result, this, routeStepSummary())
|
|
or
|
|
Http::routeHandlerStep(result, this)
|
|
or
|
|
RouteHandlerTrackingStep::step(result, this)
|
|
or
|
|
exists(string prop |
|
|
StepSummary::smallstep(result, this.getSourceProp(prop).getALocalUse(), StoreStep(prop))
|
|
)
|
|
or
|
|
this = getAnEnumeratedArrayElement(result)
|
|
}
|
|
|
|
/** Gets a node whose `prop` property flows to this use site. */
|
|
private DataFlow::SourceNode getSourceProp(string prop) {
|
|
StepSummary::step(result, this, LoadStep(prop))
|
|
or
|
|
StepSummary::step(result, this.getSourceProp(prop), routeStepSummary())
|
|
or
|
|
StepSummary::step(result, this.getSourceProp(prop), CopyStep(prop))
|
|
or
|
|
exists(string oldProp |
|
|
StepSummary::step(result, this.getSourceProp(oldProp), LoadStoreStep(prop, oldProp))
|
|
)
|
|
}
|
|
|
|
private DataFlow::Node getStrictSource() {
|
|
result = this.getSource() and
|
|
result != this
|
|
}
|
|
|
|
final override Routing::Node getChild(int n) {
|
|
n = 0 and
|
|
result = MkValueNode(this.getStrictSource())
|
|
or
|
|
// If we cannot find the source of the use-site, but we know it's somehow a reference to a router,
|
|
// treat the router as the source. This is needed to handle chaining calls on the router, as the
|
|
// specific framework model knows about chaining steps, but the general `getSource()` predicate doesn't.
|
|
n = 0 and
|
|
not exists(this.getStrictSource()) and
|
|
exists(Router::Range router |
|
|
this = router.getAReference().getALocalUse() and
|
|
result = MkRouter(router, this.getContainer())
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A node flowing into a use site, modeled as a child of the use site.
|
|
*/
|
|
private class UseSiteSource extends UseSite {
|
|
UseSiteSource() { this = any(UseSite use).getSource() }
|
|
}
|
|
|
|
/**
|
|
* A node that has a linear sequence of children, which should all be marked as route objects.
|
|
*/
|
|
abstract class WithArguments extends Range {
|
|
/**
|
|
* Gets a data flow node that should be seen as the `n`th child of this node.
|
|
*
|
|
* Overriding this predicate ensures that a routing node is generated for the child.
|
|
*/
|
|
abstract DataFlow::Node getArgumentNode(int n);
|
|
|
|
final override Node getChild(int n) { result = MkValueNode(this.getArgumentNode(n)) }
|
|
}
|
|
|
|
/** An argument to a `WithArguments` instance, seen as a use site. */
|
|
private class Argument extends UseSite {
|
|
Argument() { this = any(WithArguments n).getArgumentNode(_) }
|
|
}
|
|
|
|
/**
|
|
* An array which has been determined to be a route node, seen as a route node with arguments.
|
|
*/
|
|
private class ImpliedArrayRoute extends ValueNode::WithArguments, DataFlow::ArrayCreationNode instanceof ValueNode::UseSite
|
|
{
|
|
override DataFlow::Node getArgumentNode(int n) { result = this.getElement(n) }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* An edge that should be used for tracking route handler definitions to their use-sites.
|
|
*
|
|
* This may be subclassed by framework models to contribute additional steps.
|
|
*/
|
|
class RouteHandlerTrackingStep extends Unit {
|
|
/** Holds if route handlers should be propagated along the edge `pred -> succ`. */
|
|
predicate step(DataFlow::Node pred, DataFlow::Node succ) { none() }
|
|
}
|
|
|
|
private module RouteHandlerTrackingStep {
|
|
predicate step(DataFlow::Node pred, DataFlow::Node succ) {
|
|
any(RouteHandlerTrackingStep s).step(pred, succ)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A node in the routing tree which has no parent.
|
|
*/
|
|
class RootNode extends Node {
|
|
RootNode() { not exists(this.getParent()) }
|
|
|
|
/** Gets a node that is part of this subtree. */
|
|
final Node getADescendant() { result = this.getAChild*() }
|
|
}
|
|
|
|
/**
|
|
* A node representing a place where one or more routes are installed onto a mutable
|
|
* router object.
|
|
*
|
|
* The children of this node are the individual route handlers installed here.
|
|
*
|
|
* The siblings of this node are the other route setups locally affecting the same router,
|
|
* in the order in which they are installed.
|
|
*
|
|
* In case of branching control flow, the siblings are non-linear, that is, some route setups
|
|
* will have multiple previous/next siblings, reflecting the different paths the program may take
|
|
* during setup.
|
|
*/
|
|
class RouteSetup extends Node, MkRouteSetup {
|
|
/**
|
|
* Gets the router affected by this route setup.
|
|
*
|
|
* This is an alias for `getParent`, but may be preferred for readability.
|
|
*/
|
|
final Node getRouter() { result = this.getParent() }
|
|
}
|
|
|
|
/**
|
|
* Companion module to the `RouteSetup` class, containing classes that can be use to contribute
|
|
* new kinds of route setups.
|
|
*/
|
|
module RouteSetup {
|
|
// To avoid negative recursion, the route setup range class is split into 'Base' and 'Range', where
|
|
// 'Range' contains those contributed by frameworks, and 'Base' contains some additional route setups
|
|
// where the router escapes into a function that contains other route setups.
|
|
/**
|
|
* INTERNAL. Use `RouteSetup::Range` instead.
|
|
*
|
|
* Class containing explicit route setups in addition to implied route setups, that is,
|
|
* places where a router escapes into a function containing route setups.
|
|
*/
|
|
abstract class Base extends DataFlow::Node {
|
|
/** Gets the `n`th child of this route node. */
|
|
Node getChild(int n) { none() }
|
|
|
|
/** Gets the number of children of this route node. */
|
|
final int getNumChild() { result = count(int n | exists(this.getChild(n))) }
|
|
|
|
/**
|
|
* Gets a path prefix to be matched against the path of incoming requests.
|
|
*
|
|
* If the prefix matches, the request is dispatched to the first child, with a modified path
|
|
* where the matched prefix has been removed. For example, if the prefix is `/foo` and the incoming
|
|
* request has path `/foo/bar`, a request with path `/bar` is dispatched to the first child.
|
|
*
|
|
* If the prefix does not match, the request is passed on to the continuation.
|
|
*/
|
|
string getRelativePath() { none() }
|
|
|
|
/**
|
|
* Gets an HTTP request method name (in upper case) matched by this node, or nothing
|
|
* if all HTTP request method names are accepted.
|
|
*/
|
|
Http::RequestMethodName getHttpMethod() { none() }
|
|
|
|
/**
|
|
* Holds if this route setup targets `router` and occurs at the given `cfgNode`.
|
|
*/
|
|
abstract predicate isInstalledAt(Router::Range router, ControlFlowNode cfgNode);
|
|
}
|
|
|
|
/**
|
|
* This class can be extended to contribute new kinds of route setups.
|
|
*/
|
|
abstract class Range extends Base {
|
|
// Note: all member predicates are defined in RouteSetup::Base, declared above.
|
|
}
|
|
|
|
private class RouteSetupImpl extends Node, MkRouteSetup {
|
|
Base range;
|
|
|
|
RouteSetupImpl() { this = MkRouteSetup(range) }
|
|
|
|
override string toString() { result = range.toString() }
|
|
|
|
override predicate hasLocationInfo(
|
|
string filepath, int startline, int startcolumn, int endline, int endcolumn
|
|
) {
|
|
range.hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
|
|
}
|
|
|
|
override Node getAChild() { result = range.getChild(_) }
|
|
|
|
override Node getFirstChild() { result = range.getChild(0) }
|
|
|
|
override Node getLastChild() { result = range.getChild(range.getNumChild() - 1) }
|
|
|
|
override string getRelativePath() { result = range.getRelativePath() }
|
|
|
|
override Http::RequestMethodName getOwnHttpMethod() { result = range.getHttpMethod() }
|
|
}
|
|
|
|
/**
|
|
* A route setup that is method call on a router object, installing its arguments as route handlers.
|
|
*
|
|
* This class can be extended to contribute new kinds of route handlers.
|
|
*/
|
|
abstract class MethodCall extends RouteSetup::Range, DataFlow::MethodCallNode {
|
|
override Node getChild(int n) { result = MkValueNode(this.getChildNode(n)) }
|
|
|
|
/** Gets the `n`th child of this route setup. */
|
|
DataFlow::Node getChildNode(int n) { result = this.getArgument(n) }
|
|
|
|
override predicate isInstalledAt(Router::Range router, ControlFlowNode cfgNode) {
|
|
this = router.getAReference().getAMethodCall() and cfgNode = this.getEnclosingExpr()
|
|
}
|
|
}
|
|
|
|
private class RouteSetupArgument extends ValueNode::UseSite {
|
|
RouteSetupArgument() { this = any(RouteSetup::MethodCall c).getChildNode(_) }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Provides classes for generating `Router` nodes, to be subclassed by framework models.
|
|
*/
|
|
module Router {
|
|
/**
|
|
* The creation of a mutable router object.
|
|
*/
|
|
abstract class Range extends DataFlow::Node {
|
|
/** Gets a reference to this router. */
|
|
abstract DataFlow::SourceNode getAReference();
|
|
}
|
|
|
|
private class RouterImpl extends Node, MkRouter {
|
|
Router::Range router;
|
|
StmtContainer container;
|
|
|
|
RouterImpl() { this = MkRouter(router, container) }
|
|
|
|
override string toString() {
|
|
result =
|
|
router.toString() + " in " +
|
|
[container.(Function).describe(), container.(TopLevel).getFile().getRelativePath()]
|
|
}
|
|
|
|
override predicate hasLocationInfo(
|
|
string filepath, int startline, int startcolumn, int endline, int endcolumn
|
|
) {
|
|
router.hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
|
|
}
|
|
|
|
private RouteSetup::Base getARouteSetup() {
|
|
result.isInstalledAt(router, any(ControlFlowNode cfg | cfg.getContainer() = container))
|
|
}
|
|
|
|
override Node getAChild() { result = MkRouteSetup(this.getARouteSetup()) }
|
|
|
|
override Node getLastChild() {
|
|
result = getMostRecentRouteSetupAt(router, container.getExit())
|
|
}
|
|
|
|
override Node getFirstChild() {
|
|
result = this.getAChild() and not exists(result.getPreviousSibling())
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Like `RouteSetup::Base.isInstalledAt` but with the route setup call mapped to the `MkRouteSetup` node.
|
|
*/
|
|
private predicate isInstalledAt(
|
|
RouteSetup setupNode, Router::Range router, ControlFlowNode cfgNode
|
|
) {
|
|
exists(RouteSetup::Base setup |
|
|
setup.isInstalledAt(router, cfgNode) and
|
|
setupNode = MkRouteSetup(setup)
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Gets the route setup most recently performed on `router` at `node`, or in the case of branching control flow,
|
|
* gets any route setup that could be the most recent one.
|
|
*/
|
|
private RouteSetup getMostRecentRouteSetupAt(Router::Range router, ControlFlowNode node) {
|
|
isInstalledAt(result, router, node)
|
|
or
|
|
result = getMostRecentRouteSetupAt(router, node.getAPredecessor()) and
|
|
not isInstalledAt(_, router, node)
|
|
}
|
|
|
|
/**
|
|
* A call where a mutable router object escapes into a parameter or is returned from a function.
|
|
*
|
|
* This is modeled as a route setup targeting the "local router" value and having
|
|
* the "target router" as its only child.
|
|
*
|
|
* For example,
|
|
* ```js
|
|
* function addMiddleware(r) {
|
|
* r.use(bodyParser());
|
|
* r.use(auth());
|
|
* }
|
|
*
|
|
* let app = express();
|
|
* addMiddleware(app); // <-- implied route setup
|
|
* app.get('/', handleRequest);
|
|
* ```
|
|
* here the call to `addMiddleware` is an implied route setup with `app`
|
|
* as the "local router" and `r` as the "target router".
|
|
*
|
|
* The routing tree ends up having the following shape:
|
|
* - `app`
|
|
* - `addMiddleware(app)`
|
|
* - `r`
|
|
* - `r.use()`
|
|
* - `bodyParser()`
|
|
* - `r.use()`
|
|
* - `auth()`
|
|
* - `app.get(...)`
|
|
* - `'/'`
|
|
* - `handleRequest`
|
|
*/
|
|
private class ImpliedRouteSetup extends RouteSetup::Base, DataFlow::InvokeNode {
|
|
Router::Range router;
|
|
Function target;
|
|
|
|
ImpliedRouteSetup() {
|
|
FlowSteps::calls(this, target) and
|
|
routerIsLiveInContainer(router, target) and
|
|
routerIsLiveInContainer(router, this.getContainer())
|
|
}
|
|
|
|
override Routing::Node getChild(int n) { result = MkRouter(router, target) and n = 0 }
|
|
|
|
override predicate isInstalledAt(Router::Range r, ControlFlowNode cfgNode) {
|
|
r = router and
|
|
cfgNode = this.getEnclosingExpr()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A function that handles an incoming request.
|
|
*/
|
|
class RouteHandler extends Node {
|
|
DataFlow::FunctionNode function;
|
|
|
|
RouteHandler() { this = MkValueNode(function) }
|
|
|
|
/**
|
|
* Gets the `i`th parameter of this route handler.
|
|
*
|
|
* To find all references to this parameter, use `getParameter(n).ref()`.
|
|
*/
|
|
final RouteHandlerParameter getParameter(int n) { result = function.getParameter(n) }
|
|
|
|
/**
|
|
* Gets a parameter of this route handler.
|
|
*
|
|
* To find all references to a parameter, use `getAParameter().ref()`.
|
|
*/
|
|
final RouteHandlerParameter getAParameter() { result = function.getAParameter() }
|
|
|
|
/** Gets the function implementing this route handler. */
|
|
DataFlow::FunctionNode getFunction() { result = function }
|
|
|
|
/**
|
|
* Gets a call that delegates the incoming request to the next route handler in the stack,
|
|
* usually a call of the form `next()`.
|
|
*
|
|
* By default, any 0-argument invocation of one of the route handler's parameters
|
|
* is considered a continuation invocation, since the other parameters (request and response)
|
|
* will generally not be invoked as a function. Framework models may override this method
|
|
* if the default behavior is inadequate for that framework.
|
|
*/
|
|
DataFlow::CallNode getAContinuationInvocation() {
|
|
result = this.getAParameter().ref().getAnInvocation() and
|
|
result.getNumArgument() = 0
|
|
or
|
|
result.(DataFlow::MethodCallNode).getMethodName() = "then" and
|
|
result.getArgument(0) = this.getAParameter().ref().getALocalUse()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the `RouteHandler` node corresponding to the given function.
|
|
*
|
|
* This has the same result as `getNode(function)` but is declared with a different return type.
|
|
*/
|
|
RouteHandler getRouteHandler(DataFlow::FunctionNode function) { result.getFunction() = function }
|
|
|
|
/**
|
|
* A parameter to a route handler function.
|
|
*/
|
|
class RouteHandlerParameter extends DataFlow::ParameterNode {
|
|
private RouteHandler handler;
|
|
|
|
RouteHandlerParameter() { this = handler.getFunction().getAParameter() }
|
|
|
|
/** Gets a data flow node referring to this route handler parameter. */
|
|
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 data flow node referring to this route handler parameter. */
|
|
DataFlow::SourceNode ref() { result = this.ref(DataFlow::TypeTracker::end()) }
|
|
|
|
/**
|
|
* Gets the corresponding route handler, that is, the function on which this is a parameter.
|
|
*/
|
|
final RouteHandler getRouteHandler() { result = handler }
|
|
|
|
/**
|
|
* Gets a node that is stored in the given access path on this route handler parameter, either
|
|
* during execution of this router handler, or in one of the preceding ones.
|
|
*/
|
|
pragma[inline]
|
|
DataFlow::Node getValueFromAccessPath(string path) {
|
|
exists(int i, Node predecessor |
|
|
pragma[only_bind_out](this) = handler.getFunction().getParameter(i) and
|
|
result = getAnAccessPathRhs(predecessor, i, path) and
|
|
(handler.isGuardedByNode(predecessor) or predecessor = handler)
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets a value that flows into the given access path of the `n`th route handler parameter of `base`.
|
|
*
|
|
* For example,
|
|
* ```js
|
|
* function handler(req, res, next) {
|
|
* res.locals.foo = 123;
|
|
* next();
|
|
* }
|
|
* ```
|
|
* the node `123` flows into the `locals.foo` access path on the `res` parameter (`n=1`) of `handler`.
|
|
*
|
|
* Only route handlers that may invoke the continuation (`next()`) are considered, as the effect
|
|
* is otherwise not observable by other route handlers.
|
|
*
|
|
* In addition to the above, also contains implicit assignments contributed by framework models,
|
|
* based on `Node::Range::getValueAtAccessPath`.
|
|
*/
|
|
private DataFlow::Node getAnAccessPathRhs(Node base, int n, string path) {
|
|
// Assigned in the body of a route handler function, whi
|
|
exists(RouteHandler handler | base = handler |
|
|
result = AccessPath::getAnAssignmentTo(handler.getParameter(n).ref(), path) and
|
|
exists(handler.getAContinuationInvocation())
|
|
)
|
|
or
|
|
// Implicit assignment contributed by framework model
|
|
exists(DataFlow::Node value, string path1 |
|
|
value = base.getValueImplicitlyStoredInAccessPath(n, path1)
|
|
|
|
|
result = value and path = path1
|
|
or
|
|
exists(string path2 |
|
|
result = AccessPath::getAnAssignmentTo(value.getALocalSource(), path2) and
|
|
path = path1 + "." + path2
|
|
)
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Gets a value that refers to the given access path of the `n`th route handler parameter of `base`.
|
|
*
|
|
* For example,
|
|
* ```js
|
|
* function handler2(req, res) {
|
|
* res.send(res.locals.foo);
|
|
* }
|
|
* ```
|
|
* here the `res.locals.foo` expression refers to the `locals.foo` path on the `res` parameter (`n=1`)
|
|
* of `handler2`.
|
|
*/
|
|
private DataFlow::SourceNode getAnAccessPathRead(RouteHandler base, int n, string path) {
|
|
result = AccessPath::getAReferenceTo(base.getParameter(n).ref(), path) and
|
|
not AccessPath::DominatingPaths::hasDominatingWrite(result)
|
|
}
|
|
|
|
/**
|
|
* Like `getAnAccessPathRhs` but with `base` mapped to its root node.
|
|
*/
|
|
pragma[nomagic]
|
|
private DataFlow::Node getAnAccessPathRhsUnderRoot(RootNode root, int n, string path) {
|
|
result = getAnAccessPathRhs(root.getADescendant(), n, path)
|
|
}
|
|
|
|
/**
|
|
* Like `getAnAccessPathRead` but with `base` mapped to its root node.
|
|
*/
|
|
pragma[nomagic]
|
|
private DataFlow::SourceNode getAnAccessPathReadUnderRoot(RootNode root, int n, string path) {
|
|
result = getAnAccessPathRead(root.getADescendant(), n, path)
|
|
}
|
|
|
|
/**
|
|
* Holds if `pred -> succ` is an API-graph step between access paths on request input objects.
|
|
*
|
|
* Since API graphs are mainly used to propagate type-like information, we do not require
|
|
* a happens-before relation for this step. We only require that we stay within the same
|
|
* web application, which is ensured by having a common root node.
|
|
*/
|
|
private predicate middlewareApiStep(DataFlow::SourceNode pred, DataFlow::SourceNode succ) {
|
|
exists(RootNode root, int n, string path |
|
|
pred = getAnAccessPathRhsUnderRoot(root, n, path) and
|
|
succ = getAnAccessPathReadUnderRoot(root, n, pragma[only_bind_out](path))
|
|
)
|
|
or
|
|
// We can't augment the call graph as this depends on type tracking, so just
|
|
// manually add steps out of functions stored on a request input.
|
|
exists(DataFlow::FunctionNode function, DataFlow::CallNode call |
|
|
middlewareApiStep(function, call.getCalleeNode().getALocalSource()) and
|
|
pred = function.getReturnNode() and
|
|
succ = call
|
|
)
|
|
}
|
|
|
|
/** Contributes `middlewareApiStep` as an API graph step. */
|
|
private class MiddlewareApiStep extends API::AdditionalUseStep {
|
|
override predicate step(DataFlow::SourceNode pred, DataFlow::SourceNode succ) {
|
|
middlewareApiStep(pred, succ)
|
|
}
|
|
}
|
|
|
|
pragma[nomagic]
|
|
private predicate potentialAccessPathStep(
|
|
Node writer, DataFlow::SourceNode pred, Node reader, DataFlow::SourceNode succ, int n,
|
|
string path
|
|
) {
|
|
pred = getAnAccessPathRhs(writer, n, path) and
|
|
succ = getAnAccessPathRead(reader, n, pragma[only_bind_out](path))
|
|
}
|
|
|
|
/**
|
|
* Holds if `pred -> succ` is a data-flow step between access paths on request input objects.
|
|
*/
|
|
private predicate middlewareDataFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
|
|
exists(Node writer, Node reader |
|
|
potentialAccessPathStep(writer, pred, reader, succ, _, _) and
|
|
pragma[only_bind_out](reader).isGuardedByNode(pragma[only_bind_out](writer))
|
|
)
|
|
or
|
|
// Same as in `apiStep`: we can't augment the call graph, so just add flow out
|
|
// of functions stored on a request input.
|
|
exists(DataFlow::FunctionNode function, DataFlow::CallNode call |
|
|
middlewareDataFlowStep(function.getALocalUse(), call.getCalleeNode().getALocalSource()) and
|
|
pred = function.getReturnNode() and
|
|
succ = call
|
|
)
|
|
}
|
|
|
|
/** Contributes `middlewareDataFlowStep` as a value-preserving data flow step. */
|
|
private class MiddlewareFlowStep extends DataFlow::SharedFlowStep {
|
|
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
|
|
middlewareDataFlowStep(pred, succ)
|
|
}
|
|
}
|
|
}
|