Skip to content

Commit 7904db0

Browse files
authored
Merge pull request #19132 from asgerf/js/guarded-route-handler-token
JS: Add GuardedRouteHandler access path component
2 parents eceeab1 + 951b48a commit 7904db0

File tree

5 files changed

+91
-2
lines changed

5 files changed

+91
-2
lines changed

docs/codeql/codeql-language-guides/customizing-library-models-for-javascript.rst

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,48 @@ Note that this flow is already recognized by the CodeQL JS analysis, but for thi
349349
- The last column, **value**, indicates the kind of flow to add. The value **value** means the input value is unchanged as
350350
it flows to the output.
351351

352+
353+
Example: Modeling properties injected by a middleware function
354+
--------------------------------------------------------------
355+
356+
In this example, we'll show how to model a hypothetical middleware function that adds a tainted value
357+
on the incoming request objects:
358+
359+
.. code-block:: js
360+
361+
const express = require('express')
362+
const app = express()
363+
364+
app.use(require('@example/middleware').injectData())
365+
366+
app.get('/foo', (req, res) => {
367+
req.data; // <-- mark 'req.data' as a taint source
368+
});
369+
370+
This can be achieved with the following data extension:
371+
372+
.. code-block:: yaml
373+
374+
extensions:
375+
- addsTo:
376+
pack: codeql/javascript-all
377+
extensible: sourceModel
378+
data:
379+
- [
380+
"@example/middleware",
381+
"Member[injectData].ReturnValue.GuardedRouteHandler.Parameter[0].Member[data]",
382+
"remote",
383+
]
384+
385+
- Since we're adding a new taint source, we add a tuple to the **sourceModel** extensible predicate.
386+
- The first column, **"@example/middleware"**, begins the search at imports of the hypothetical NPM package **@example/middleware**.
387+
- **Member[injectData]** selects accesses to the **injectData** member.
388+
- **ReturnValue** selects the return value of the call to **injectData**.
389+
- **GuardedRouteHandler** interprets the current value as a middleware function and selects all route handlers guarded by that middleware. Since the current value is passd to **app.use()**, the callback subsequently passed to **app.get()** is seen as a guarded route handler.
390+
- **Parameter[0]** selects the first parameter of the callback (the parameter named **req**).
391+
- **Member[data]** selects accesses to the **data** property of the **req** object.
392+
- Finally, the kind **remote** indicates that this is considered a source of remote flow.
393+
352394
Reference material
353395
------------------
354396

@@ -494,6 +536,12 @@ Components related to decorators:
494536
- **DecoratedParameter** selects a parameter that is decorated by the current value.
495537
- **DecoratedMember** selects a method, field, or accessor that is decorated by the current value.
496538

539+
Additionally there is a component related to middleware functions:
540+
541+
- **GuardedRouteHandler** interprets the current value as a middleware function, and selects any route handler function that comes after it in the routing hierarchy.
542+
This can be used to model properties injected onto request and response objects, such as **req.db** after a middleware that injects a database connection.
543+
Note that this currently over-approximates the set of route handlers but may be made more accurate in the future.
544+
497545
Additional notes about the syntax of operands:
498546

499547
- Multiple operands may be given to a single component, as a shorthand for the union of the operands. For example, **Member[foo,bar]** matches the union of **Member[foo]** and **Member[bar]**.

javascript/ql/lib/semmle/javascript/frameworks/data/internal/ApiGraphModelsSpecific.qll

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,20 @@ API::Node getExtraSuccessorFromNode(API::Node node, AccessPathTokenBase token) {
184184
or
185185
token.getName() = "DecoratedParameter" and
186186
result = node.getADecoratedParameter()
187+
or
188+
token.getName() = "GuardedRouteHandler" and
189+
result = getAGuardedRouteHandlerApprox(node)
190+
}
191+
192+
bindingset[node]
193+
pragma[inline_late]
194+
private API::Node getAGuardedRouteHandlerApprox(API::Node node) {
195+
// For now just get any routing node with the same root (i.e. the same web app), as
196+
// there are some known performance issues when checking if it is actually guarded by the given node.
197+
exists(JS::Routing::Node root |
198+
root = JS::Routing::getNode(node.getAValueReachableFromSource()).getRootNode() and
199+
root = JS::Routing::getNode(result.asSink()).getRootNode()
200+
)
187201
}
188202

189203
/**
@@ -317,7 +331,7 @@ predicate isExtraValidTokenNameInIdentifyingAccessPath(string name) {
317331
[
318332
"Member", "AnyMember", "Instance", "Awaited", "ArrayElement", "Element", "MapValue",
319333
"NewCall", "Call", "DecoratedClass", "DecoratedMember", "DecoratedParameter",
320-
"WithStringArgument"
334+
"WithStringArgument", "GuardedRouteHandler"
321335
]
322336
}
323337

@@ -329,7 +343,7 @@ predicate isExtraValidNoArgumentTokenInIdentifyingAccessPath(string name) {
329343
name =
330344
[
331345
"AnyMember", "Instance", "Awaited", "ArrayElement", "Element", "MapValue", "NewCall", "Call",
332-
"DecoratedClass", "DecoratedMember", "DecoratedParameter"
346+
"DecoratedClass", "DecoratedMember", "DecoratedParameter", "GuardedRouteHandler"
333347
]
334348
}
335349

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const express = require('express');
2+
const app = express();
3+
const testlib = require('testlib');
4+
5+
app.get('/before', (req, res) => {
6+
sink(req.injectedReqData); // OK [INCONSISTENCY] - happens before middleware
7+
sink(req.injectedResData); // OK - wrong parameter
8+
9+
sink(res.injectedReqData); // OK - wrong parameter
10+
sink(res.injectedResData); // OK [INCONSISTENCY] - happens before middleware
11+
});
12+
13+
app.use(testlib.middleware());
14+
15+
app.get('/after', (req, res) => {
16+
sink(req.injectedReqData); // NOT OK
17+
sink(req.injectedResData); // OK - wrong parameter
18+
19+
sink(res.injectedReqData); // OK - wrong parameter
20+
sink(res.injectedResData); // NOT OK
21+
});

javascript/ql/test/library-tests/frameworks/data/test.expected

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
legacyDataFlowDifference
22
consistencyIssue
33
taintFlow
4+
| guardedRouteHandler.js:6:10:6:28 | req.injectedReqData | guardedRouteHandler.js:6:10:6:28 | req.injectedReqData |
5+
| guardedRouteHandler.js:10:10:10:28 | res.injectedResData | guardedRouteHandler.js:10:10:10:28 | res.injectedResData |
6+
| guardedRouteHandler.js:16:10:16:28 | req.injectedReqData | guardedRouteHandler.js:16:10:16:28 | req.injectedReqData |
7+
| guardedRouteHandler.js:20:10:20:28 | res.injectedResData | guardedRouteHandler.js:20:10:20:28 | res.injectedResData |
48
| paramDecorator.ts:6:54:6:54 | x | paramDecorator.ts:7:10:7:10 | x |
59
| test.js:5:30:5:37 | source() | test.js:5:8:5:38 | testlib ... urce()) |
610
| test.js:6:22:6:29 | source() | test.js:6:8:6:30 | preserv ... urce()) |

javascript/ql/test/library-tests/frameworks/data/test.ext.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ extensions:
1313
- ['testlib', 'Member[getSourceArray].ReturnValue.ArrayElement', 'test-source']
1414
- ['(testlib)', 'Member[parenthesizedPackageName].ReturnValue', 'test-source']
1515
- ['danger-constant', 'Member[danger]', 'test-source']
16+
- ['testlib', 'Member[middleware].ReturnValue.GuardedRouteHandler.Parameter[0].Member[injectedReqData]', 'test-source']
17+
- ['testlib', 'Member[middleware].ReturnValue.GuardedRouteHandler.Parameter[1].Member[injectedResData]', 'test-source']
1618

1719
- addsTo:
1820
pack: codeql/javascript-all

0 commit comments

Comments
 (0)