Skip to content

Commit 00661b6

Browse files
committed
JS: Add isMiddlewareSetup() hook to Routing model
1 parent 5c3556d commit 00661b6

File tree

5 files changed

+77
-6
lines changed

5 files changed

+77
-6
lines changed

javascript/ql/lib/semmle/javascript/Routing.qll

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ module Routing {
139139
predicate mayResumeDispatch() {
140140
this.getLastChild().mayResumeDispatch()
141141
or
142+
isInMiddlewareSetup(this)
143+
or
142144
exists(this.(RouteHandler).getAContinuationInvocation())
143145
or
144146
// Leaf nodes that aren't functions are assumed to invoke their continuation
@@ -155,6 +157,8 @@ module Routing {
155157
predicate definitelyResumesDispatch() {
156158
this.getLastChild().definitelyResumesDispatch()
157159
or
160+
isInMiddlewareSetup(this)
161+
or
158162
exists(this.(RouteHandler).getAContinuationInvocation())
159163
or
160164
this instanceof MkRouter
@@ -325,6 +329,19 @@ module Routing {
325329
DataFlow::Node getValueImplicitlyStoredInAccessPath(int n, string path) { none() }
326330
}
327331

332+
/**
333+
* Holds if `node` is installed at a route handler that is declared to be a middleware setup,
334+
* and is therefore assume to resume dispatch.
335+
*/
336+
private predicate isInMiddlewareSetup(Node node) {
337+
exists(RouteSetup::Range range |
338+
node = getRouteSetupNode(range) and
339+
range.isMiddlewareSetup()
340+
)
341+
or
342+
isInMiddlewareSetup(node.getParent())
343+
}
344+
328345
/** Holds if `pred` and `succ` are adjacent siblings and `succ` is installed after `pred`. */
329346
private predicate areSiblings(Node pred, Node succ) {
330347
exists(ValueNode::Range base, int n |
@@ -612,6 +629,20 @@ module Routing {
612629
* Holds if this route setup targets `router` and occurs at the given `cfgNode`.
613630
*/
614631
abstract predicate isInstalledAt(Router::Range router, ControlFlowNode cfgNode);
632+
633+
/**
634+
* Holds if this is a middleware setup, meaning dispatch will resume after the
635+
* route handlers in this route setup have completed (usually meaning that they have returned a promise, which has resolved).
636+
*
637+
* This should only be overridden when the route setup itself determines whether subsequent
638+
* route handlers are invoked afterwards.
639+
* - For Express-like libraries, the route _handler_ determines whether to resume dispatch,
640+
* based on whether the `next` callback is invoked. For such libraries, do not override `isMiddlewareSetup`.
641+
* - For Fastify-like libraries, the route _setup_ determines whether to resume dispatch.
642+
* For example, `.addHook()` will resume dispatch whereas `.get()` will not. `isMiddlewareSetup()` should thus
643+
* hold for `.addHook()` but not for `.get()` calls.
644+
*/
645+
predicate isMiddlewareSetup() { none() }
615646
}
616647

617648
/**
@@ -892,10 +923,14 @@ module Routing {
892923
* based on `Node::Range::getValueAtAccessPath`.
893924
*/
894925
private DataFlow::Node getAnAccessPathRhs(Node base, int n, string path) {
895-
// Assigned in the body of a route handler function, whi
926+
// Assigned in the body of a route handler function, which is a middleware
896927
exists(RouteHandler handler | base = handler |
897928
result = AccessPath::getAnAssignmentTo(handler.getParameter(n).ref(), path) and
898-
exists(handler.getAContinuationInvocation())
929+
(
930+
exists(handler.getAContinuationInvocation())
931+
or
932+
isInMiddlewareSetup(handler)
933+
)
899934
)
900935
or
901936
// Implicit assignment contributed by framework model

javascript/ql/lib/semmle/javascript/frameworks/Fastify.qll

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,13 +164,19 @@ module Fastify {
164164

165165
private class ShorthandRoutingTreeSetup extends Routing::RouteSetup::MethodCall instanceof RouteSetup
166166
{
167-
ShorthandRoutingTreeSetup() { not this.getMethodName() = "route" }
167+
ShorthandRoutingTreeSetup() { not this.getMethodName() = ["route", "addHook"] }
168168

169169
override string getRelativePath() { result = this.getArgument(0).getStringValue() }
170170

171171
override Http::RequestMethodName getHttpMethod() { result = this.getMethodName().toUpperCase() }
172172
}
173173

174+
private class AddHookRouteSetup extends Routing::RouteSetup::MethodCall instanceof RouteSetup {
175+
AddHookRouteSetup() { this.getMethodName() = "addHook" }
176+
177+
override predicate isMiddlewareSetup() { any() }
178+
}
179+
174180
/** Gets the name of the `n`th handler function that can be installed a route setup, in order of execution. */
175181
private string getNthHandlerName(int n) {
176182
result =

javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/CodeInjection.expected

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,14 @@
5151
| fastify.js:58:44:58:52 | userInput | fastify.js:57:21:57:39 | request.query.input | fastify.js:58:44:58:52 | userInput | This code execution depends on a $@. | fastify.js:57:21:57:39 | request.query.input | user-provided value |
5252
| fastify.js:59:23:59:31 | userInput | fastify.js:57:21:57:33 | request.query | fastify.js:59:23:59:31 | userInput | This code execution depends on a $@. | fastify.js:57:21:57:33 | request.query | user-provided value |
5353
| fastify.js:59:23:59:31 | userInput | fastify.js:57:21:57:39 | request.query.input | fastify.js:59:23:59:31 | userInput | This code execution depends on a $@. | fastify.js:57:21:57:39 | request.query.input | user-provided value |
54+
| fastify.js:71:34:71:51 | request.storedCode | fastify.js:66:24:66:36 | request.query | fastify.js:71:34:71:51 | request.storedCode | This code execution depends on a $@. | fastify.js:66:24:66:36 | request.query | user-provided value |
55+
| fastify.js:71:34:71:51 | request.storedCode | fastify.js:66:24:66:47 | request ... redCode | fastify.js:71:34:71:51 | request.storedCode | This code execution depends on a $@. | fastify.js:66:24:66:47 | request ... redCode | user-provided value |
5456
| fastify.js:71:34:71:51 | request.storedCode | fastify.js:71:34:71:51 | request.storedCode | fastify.js:71:34:71:51 | request.storedCode | This code execution depends on a $@. | fastify.js:71:34:71:51 | request.storedCode | user-provided value |
57+
| fastify.js:84:30:84:43 | reply.userCode | fastify.js:79:20:79:32 | request.query | fastify.js:84:30:84:43 | reply.userCode | This code execution depends on a $@. | fastify.js:79:20:79:32 | request.query | user-provided value |
58+
| fastify.js:84:30:84:43 | reply.userCode | fastify.js:79:20:79:42 | request ... plyCode | fastify.js:84:30:84:43 | reply.userCode | This code execution depends on a $@. | fastify.js:79:20:79:42 | request ... plyCode | user-provided value |
5559
| fastify.js:84:30:84:43 | reply.userCode | fastify.js:84:30:84:43 | reply.userCode | fastify.js:84:30:84:43 | reply.userCode | This code execution depends on a $@. | fastify.js:84:30:84:43 | reply.userCode | user-provided value |
60+
| fastify.js:99:30:99:52 | reply.l ... tedCode | fastify.js:94:29:94:41 | request.query | fastify.js:99:30:99:52 | reply.l ... tedCode | This code execution depends on a $@. | fastify.js:94:29:94:41 | request.query | user-provided value |
61+
| fastify.js:99:30:99:52 | reply.l ... tedCode | fastify.js:94:29:94:51 | request ... plyCode | fastify.js:99:30:99:52 | reply.l ... tedCode | This code execution depends on a $@. | fastify.js:94:29:94:51 | request ... plyCode | user-provided value |
5662
| fastify.js:99:30:99:52 | reply.l ... tedCode | fastify.js:99:30:99:52 | reply.l ... tedCode | fastify.js:99:30:99:52 | reply.l ... tedCode | This code execution depends on a $@. | fastify.js:99:30:99:52 | reply.l ... tedCode | user-provided value |
5763
| module.js:9:16:9:29 | req.query.code | module.js:9:16:9:29 | req.query.code | module.js:9:16:9:29 | req.query.code | This code execution depends on a $@. | module.js:9:16:9:29 | req.query.code | user-provided value |
5864
| module.js:11:17:11:30 | req.query.code | module.js:11:17:11:30 | req.query.code | module.js:11:17:11:30 | req.query.code | This code execution depends on a $@. | module.js:11:17:11:30 | req.query.code | user-provided value |
@@ -136,6 +142,12 @@ edges
136142
| fastify.js:57:9:57:39 | userInput | fastify.js:59:23:59:31 | userInput | provenance | |
137143
| fastify.js:57:21:57:33 | request.query | fastify.js:57:9:57:39 | userInput | provenance | |
138144
| fastify.js:57:21:57:39 | request.query.input | fastify.js:57:9:57:39 | userInput | provenance | |
145+
| fastify.js:66:24:66:36 | request.query | fastify.js:66:24:66:47 | request ... redCode | provenance | |
146+
| fastify.js:66:24:66:47 | request ... redCode | fastify.js:71:34:71:51 | request.storedCode | provenance | |
147+
| fastify.js:79:20:79:32 | request.query | fastify.js:79:20:79:42 | request ... plyCode | provenance | |
148+
| fastify.js:79:20:79:42 | request ... plyCode | fastify.js:84:30:84:43 | reply.userCode | provenance | |
149+
| fastify.js:94:29:94:41 | request.query | fastify.js:94:29:94:51 | request ... plyCode | provenance | |
150+
| fastify.js:94:29:94:51 | request ... plyCode | fastify.js:99:30:99:52 | reply.l ... tedCode | provenance | |
139151
| react-native.js:7:7:7:33 | tainted | react-native.js:8:32:8:38 | tainted | provenance | |
140152
| react-native.js:7:7:7:33 | tainted | react-native.js:10:23:10:29 | tainted | provenance | |
141153
| react-native.js:7:17:7:33 | req.param("code") | react-native.js:7:7:7:33 | tainted | provenance | |
@@ -250,8 +262,14 @@ nodes
250262
| fastify.js:57:21:57:39 | request.query.input | semmle.label | request.query.input |
251263
| fastify.js:58:44:58:52 | userInput | semmle.label | userInput |
252264
| fastify.js:59:23:59:31 | userInput | semmle.label | userInput |
265+
| fastify.js:66:24:66:36 | request.query | semmle.label | request.query |
266+
| fastify.js:66:24:66:47 | request ... redCode | semmle.label | request ... redCode |
253267
| fastify.js:71:34:71:51 | request.storedCode | semmle.label | request.storedCode |
268+
| fastify.js:79:20:79:32 | request.query | semmle.label | request.query |
269+
| fastify.js:79:20:79:42 | request ... plyCode | semmle.label | request ... plyCode |
254270
| fastify.js:84:30:84:43 | reply.userCode | semmle.label | reply.userCode |
271+
| fastify.js:94:29:94:41 | request.query | semmle.label | request.query |
272+
| fastify.js:94:29:94:51 | request ... plyCode | semmle.label | request ... plyCode |
255273
| fastify.js:99:30:99:52 | reply.l ... tedCode | semmle.label | reply.l ... tedCode |
256274
| module.js:9:16:9:29 | req.query.code | semmle.label | req.query.code |
257275
| module.js:11:17:11:30 | req.query.code | semmle.label | req.query.code |

javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/HeuristicSourceCodeInjection.expected

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ edges
4545
| fastify.js:57:9:57:39 | userInput | fastify.js:59:23:59:31 | userInput | provenance | |
4646
| fastify.js:57:21:57:33 | request.query | fastify.js:57:9:57:39 | userInput | provenance | |
4747
| fastify.js:57:21:57:39 | request.query.input | fastify.js:57:9:57:39 | userInput | provenance | |
48+
| fastify.js:66:24:66:36 | request.query | fastify.js:66:24:66:47 | request ... redCode | provenance | |
49+
| fastify.js:66:24:66:47 | request ... redCode | fastify.js:71:34:71:51 | request.storedCode | provenance | |
50+
| fastify.js:79:20:79:32 | request.query | fastify.js:79:20:79:42 | request ... plyCode | provenance | |
51+
| fastify.js:79:20:79:42 | request ... plyCode | fastify.js:84:30:84:43 | reply.userCode | provenance | |
52+
| fastify.js:94:29:94:41 | request.query | fastify.js:94:29:94:51 | request ... plyCode | provenance | |
53+
| fastify.js:94:29:94:51 | request ... plyCode | fastify.js:99:30:99:52 | reply.l ... tedCode | provenance | |
4854
| react-native.js:7:7:7:33 | tainted | react-native.js:8:32:8:38 | tainted | provenance | |
4955
| react-native.js:7:7:7:33 | tainted | react-native.js:10:23:10:29 | tainted | provenance | |
5056
| react-native.js:7:17:7:33 | req.param("code") | react-native.js:7:7:7:33 | tainted | provenance | |
@@ -161,8 +167,14 @@ nodes
161167
| fastify.js:57:21:57:39 | request.query.input | semmle.label | request.query.input |
162168
| fastify.js:58:44:58:52 | userInput | semmle.label | userInput |
163169
| fastify.js:59:23:59:31 | userInput | semmle.label | userInput |
170+
| fastify.js:66:24:66:36 | request.query | semmle.label | request.query |
171+
| fastify.js:66:24:66:47 | request ... redCode | semmle.label | request ... redCode |
164172
| fastify.js:71:34:71:51 | request.storedCode | semmle.label | request.storedCode |
173+
| fastify.js:79:20:79:32 | request.query | semmle.label | request.query |
174+
| fastify.js:79:20:79:42 | request ... plyCode | semmle.label | request ... plyCode |
165175
| fastify.js:84:30:84:43 | reply.userCode | semmle.label | reply.userCode |
176+
| fastify.js:94:29:94:41 | request.query | semmle.label | request.query |
177+
| fastify.js:94:29:94:51 | request ... plyCode | semmle.label | request ... plyCode |
166178
| fastify.js:99:30:99:52 | reply.l ... tedCode | semmle.label | reply.l ... tedCode |
167179
| module.js:9:16:9:29 | req.query.code | semmle.label | req.query.code |
168180
| module.js:11:17:11:30 | req.query.code | semmle.label | req.query.code |

javascript/ql/test/query-tests/Security/CWE-094/CodeInjection/fastify.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ fastify.get('/dangerous', async (request, reply) => {
6363

6464
// Store user input in request object
6565
fastify.addHook('preHandler', async (request, reply) => {
66-
request.storedCode = request.query.storedCode;
66+
request.storedCode = request.query.storedCode; // $ Source[js/code-injection]
6767
});
6868
fastify.get('/flow-through-request', async (request, reply) => {
6969
// Use the stored code from previous hook
@@ -76,7 +76,7 @@ fastify.get('/flow-through-request', async (request, reply) => {
7676

7777
// Store user input in reply object
7878
fastify.addHook('onRequest', async (request, reply) => {
79-
reply.userCode = request.query.replyCode;
79+
reply.userCode = request.query.replyCode; // $ Source[js/code-injection]
8080
});
8181
fastify.get('/flow-through-reply', async (request, reply) => {
8282
// Use the code stored in reply object
@@ -91,7 +91,7 @@ fastify.get('/flow-through-reply', async (request, reply) => {
9191
// Store user input in reply object
9292
fastify.addHook('onRequest', async (request, reply) => {
9393
reply.locals = reply.locals || {};
94-
reply.locals.nestedCode = request.query.replyCode;
94+
reply.locals.nestedCode = request.query.replyCode; // $ Source[js/code-injection]
9595
});
9696
fastify.get('/flow-through-reply', async (request, reply) => {
9797
// Use the code stored in reply object

0 commit comments

Comments
 (0)