Skip to content

Commit bdb4142

Browse files
authored
Merge pull request github#5748 from asgerf/js/rate-limiting-fixes
Approved by erik-krogh
2 parents 2b8afe5 + fe8deea commit bdb4142

File tree

9 files changed

+201
-120
lines changed

9 files changed

+201
-120
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
lgtm,codescanning
2+
* Tracking of HTTP route handlers has improved, which may lead to additional
3+
security results, and fewer false-positive results from the `js/missing-rate-limiting` query.

javascript/ql/src/semmle/javascript/dataflow/DataFlow.qll

Lines changed: 1 addition & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1682,61 +1682,7 @@ module DataFlow {
16821682
import Configuration
16831683
import TrackedNodes
16841684
import TypeTracking
1685+
import internal.FunctionWrapperSteps
16851686

16861687
predicate localTaintStep = TaintTracking::localTaintStep/2;
1687-
1688-
/**
1689-
* Holds if the function in `succ` forwards all its arguments to a call to `pred` and returns
1690-
* its result. This can thus be seen as a step `pred -> succ` used for tracking function values
1691-
* through "wrapper functions", since the `succ` function partially replicates behavior of `pred`.
1692-
*
1693-
* Examples:
1694-
* ```js
1695-
* function f(x) {
1696-
* return g(x); // step: g -> f
1697-
* }
1698-
*
1699-
* function doExec(x) {
1700-
* console.log(x);
1701-
* return exec(x); // step: exec -> doExec
1702-
* }
1703-
*
1704-
* function doEither(x, y) {
1705-
* if (x > y) {
1706-
* return foo(x, y); // step: foo -> doEither
1707-
* } else {
1708-
* return bar(x, y); // step: bar -> doEither
1709-
* }
1710-
* }
1711-
*
1712-
* function wrapWithLogging(f) {
1713-
* return (x) => {
1714-
* console.log(x);
1715-
* return f(x); // step: f -> anonymous function
1716-
* }
1717-
* }
1718-
* wrapWithLogging(g); // step: g -> wrapWithLogging(g)
1719-
* ```
1720-
*/
1721-
predicate functionForwardingStep(DataFlow::Node pred, DataFlow::Node succ) {
1722-
exists(DataFlow::FunctionNode function, DataFlow::CallNode call |
1723-
call.flowsTo(function.getReturnNode()) and
1724-
forall(int i | exists([call.getArgument(i), function.getParameter(i)]) |
1725-
function.getParameter(i).flowsTo(call.getArgument(i))
1726-
) and
1727-
pred = call.getCalleeNode() and
1728-
succ = function
1729-
)
1730-
or
1731-
// Given a generic wrapper function like,
1732-
//
1733-
// function wrap(f) { return (x, y) => f(x, y) };
1734-
//
1735-
// add steps through calls to that function: `g -> wrap(g)`
1736-
exists(DataFlow::FunctionNode wrapperFunction, SourceNode param, Node paramUse |
1737-
FlowSteps::argumentPassing(succ, pred, wrapperFunction.getFunction(), param) and
1738-
param.flowsTo(paramUse) and
1739-
functionForwardingStep(paramUse, wrapperFunction.getReturnNode().getALocalSource())
1740-
)
1741-
}
17421688
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/** Provides predicates for tracking functions through wrapper functions. */
2+
3+
private import javascript
4+
private import FlowSteps as FlowSteps
5+
private import semmle.javascript.internal.CachedStages
6+
7+
cached
8+
private module Cached {
9+
private predicate forwardsParameter(
10+
DataFlow::FunctionNode function, int i, DataFlow::CallNode call
11+
) {
12+
exists(DataFlow::ParameterNode parameter | parameter = function.getParameter(i) |
13+
not parameter.isRestParameter() and
14+
parameter.flowsTo(call.getArgument(i))
15+
or
16+
parameter.isRestParameter() and
17+
parameter.flowsTo(call.getASpreadArgument())
18+
)
19+
}
20+
21+
cached
22+
private module Stage {
23+
// Forces the module to be computed as part of the type-tracking stage.
24+
cached
25+
predicate forceStage() { Stages::TypeTracking::ref() }
26+
}
27+
28+
/**
29+
* Holds if the function in `succ` forwards all its arguments to a call to `pred` and returns
30+
* its result. This can thus be seen as a step `pred -> succ` used for tracking function values
31+
* through "wrapper functions", since the `succ` function partially replicates behavior of `pred`.
32+
*
33+
* Examples:
34+
* ```js
35+
* function f(x) {
36+
* return g(x); // step: g -> f
37+
* }
38+
*
39+
* function doExec(x) {
40+
* console.log(x);
41+
* return exec(x); // step: exec -> doExec
42+
* }
43+
*
44+
* function doEither(x, y) {
45+
* if (x > y) {
46+
* return foo(x, y); // step: foo -> doEither
47+
* } else {
48+
* return bar(x, y); // step: bar -> doEither
49+
* }
50+
* }
51+
*
52+
* function wrapWithLogging(f) {
53+
* return (x) => {
54+
* console.log(x);
55+
* return f(x); // step: f -> anonymous function
56+
* }
57+
* }
58+
* wrapWithLogging(g); // step: g -> wrapWithLogging(g)
59+
* ```
60+
*/
61+
cached
62+
predicate functionForwardingStep(DataFlow::Node pred, DataFlow::Node succ) {
63+
exists(DataFlow::FunctionNode function, DataFlow::CallNode call |
64+
call.flowsTo(function.getReturnNode()) and
65+
forall(int i | exists([call.getArgument(i), function.getParameter(i)]) |
66+
forwardsParameter(function, i, call)
67+
) and
68+
pred = call.getCalleeNode() and
69+
succ = function
70+
)
71+
or
72+
// Given a generic wrapper function like,
73+
//
74+
// function wrap(f) { return (x, y) => f(x, y) };
75+
//
76+
// add steps through calls to that function: `g -> wrap(g)`
77+
exists(
78+
DataFlow::FunctionNode wrapperFunction, DataFlow::SourceNode param, DataFlow::Node paramUse
79+
|
80+
FlowSteps::argumentPassing(succ, pred, wrapperFunction.getFunction(), param) and
81+
param.flowsTo(paramUse) and
82+
functionForwardingStep(paramUse, wrapperFunction.getReturnNode().getALocalSource())
83+
)
84+
}
85+
86+
/**
87+
* Holds if the function in `succ` forwards all its arguments to a call to `pred`.
88+
* This can thus be seen as a step `pred -> succ` used for tracking function values
89+
* through "wrapper functions", since the `succ` function partially replicates behavior of `pred`.
90+
*
91+
* This is similar to `functionForwardingStep` except the innermost forwarding call does not
92+
* need flow to the return value; this can be useful for tracking callback-style functions
93+
* where the result tends to be unused.
94+
*
95+
* Examples:
96+
* ```js
97+
* function f(x, callback) {
98+
* g(x, callback); // step: g -> f
99+
* }
100+
*
101+
* function doExec(x, callback) {
102+
* console.log(x);
103+
* exec(x, callback); // step: exec -> doExec
104+
* }
105+
*
106+
* function doEither(x, y) {
107+
* if (x > y) {
108+
* return foo(x, y); // step: foo -> doEither
109+
* } else {
110+
* return bar(x, y); // step: bar -> doEither
111+
* }
112+
* }
113+
*
114+
* function wrapWithLogging(f) {
115+
* return (x) => {
116+
* console.log(x);
117+
* return f(x); // step: f -> anonymous function
118+
* }
119+
* }
120+
* wrapWithLogging(g); // step: g -> wrapWithLogging(g)
121+
* ```
122+
*/
123+
cached
124+
predicate functionOneWayForwardingStep(DataFlow::Node pred, DataFlow::Node succ) {
125+
exists(DataFlow::FunctionNode function, DataFlow::CallNode call |
126+
call.getContainer() = function.getFunction() and
127+
forall(int i | exists(function.getParameter(i)) | forwardsParameter(function, i, call)) and
128+
pred = call.getCalleeNode() and
129+
succ = function
130+
)
131+
or
132+
// Given a generic wrapper function like,
133+
//
134+
// function wrap(f) { return (x, y) => f(x, y) };
135+
//
136+
// add steps through calls to that function: `g -> wrap(g)`
137+
exists(
138+
DataFlow::FunctionNode wrapperFunction, DataFlow::SourceNode param, DataFlow::Node paramUse
139+
|
140+
FlowSteps::argumentPassing(succ, pred, wrapperFunction.getFunction(), param) and
141+
param.flowsTo(paramUse) and
142+
functionOneWayForwardingStep(paramUse, wrapperFunction.getReturnNode().getALocalSource())
143+
)
144+
}
145+
}
146+
147+
import Cached

javascript/ql/src/semmle/javascript/frameworks/Express.qll

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,13 @@ module Express {
224224
/**
225225
* Gets the function body of this handler, if it is defined locally.
226226
*/
227-
RouteHandler getBody() { result.(DataFlow::SourceNode).flowsToExpr(this) }
227+
RouteHandler getBody() {
228+
exists(DataFlow::SourceNode source | source = flow().getALocalSource() |
229+
result = source
230+
or
231+
DataFlow::functionOneWayForwardingStep(result.(DataFlow::SourceNode).getALocalUse(), source)
232+
)
233+
}
228234

229235
/**
230236
* Holds if this is not followed by more handlers.

javascript/ql/src/semmle/javascript/frameworks/HTTP.qll

Lines changed: 2 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -235,64 +235,12 @@ module HTTP {
235235
ResponseExpr getAResponseExpr() { result.getRouteHandler() = this }
236236
}
237237

238-
/**
239-
* Holds if `call` decorates the function `pred`.
240-
* This means that `call` returns a function that forwards its arguments to `pred`.
241-
* Only holds when the decorator looks like it is decorating a route-handler.
242-
*
243-
* Below is a code example relating `call`, `decoratee`, `outer`, `inner`.
244-
* ```
245-
* function outer(method) {
246-
* return function inner(req, res) {
247-
* return method.call(this, req, res);
248-
* };
249-
* }
250-
* var route = outer(function decoratee(req, res) { // <- call
251-
* res.end("foo");
252-
* });
253-
* ```
254-
*/
255-
private predicate isDecoratedCall(DataFlow::CallNode call, DataFlow::FunctionNode decoratee) {
256-
// indirect route-handler `result` is given to function `outer`, which returns function `inner` which calls the function `pred`.
257-
exists(int i, DataFlow::FunctionNode outer, HTTP::RouteHandlerCandidate inner |
258-
inner = outer.getAReturn().getALocalSource() and
259-
decoratee = call.getArgument(i).getALocalSource() and
260-
outer.getFunction() = call.getACallee() and
261-
hasForwardingHandlerParameter(i, outer, inner)
262-
)
263-
}
264-
265-
/**
266-
* Holds if the `i`th parameter of `outer` has a call that `inner` forwards its parameters to.
267-
*/
268-
private predicate hasForwardingHandlerParameter(
269-
int i, DataFlow::FunctionNode outer, HTTP::RouteHandlerCandidate inner
270-
) {
271-
isAForwardingRouteHandlerCall(outer.getParameter(i), inner)
272-
}
273-
274-
/**
275-
* Holds if `f` looks like a route-handler and a call to `callee` inside `f` forwards all of the parameters from `f` to that call.
276-
*/
277-
private predicate isAForwardingRouteHandlerCall(
278-
DataFlow::SourceNode callee, HTTP::RouteHandlerCandidate f
279-
) {
280-
exists(DataFlow::CallNode call | call = callee.getACall() |
281-
forall(int arg | arg = [0 .. f.getNumParameter() - 1] |
282-
f.getParameter(arg).flowsTo(call.getArgument(arg))
283-
) and
284-
call.getContainer() = f.getFunction()
285-
)
286-
}
287-
288238
/**
289239
* Holds if there exists a step from `pred` to `succ` for a RouteHandler - beyond the usual steps defined by TypeTracking.
290240
*/
291241
predicate routeHandlerStep(DataFlow::SourceNode pred, DataFlow::SourceNode succ) {
292-
isDecoratedCall(succ, pred)
293-
or
294242
// A forwarding call
295-
isAForwardingRouteHandlerCall(pred, succ)
243+
DataFlow::functionOneWayForwardingStep(pred.getALocalUse(), succ)
296244
or
297245
// a container containing route-handlers.
298246
exists(HTTP::RouteHandlerCandidateContainer container | pred = container.getRouteHandler(succ))
@@ -687,7 +635,7 @@ module HTTP {
687635
DataFlow::SourceNode getAPossiblyDecoratedHandler(RouteHandlerCandidate candidate) {
688636
result = candidate
689637
or
690-
isDecoratedCall(result, candidate)
638+
DataFlow::functionOneWayForwardingStep(candidate, result)
691639
}
692640

693641
private string mapValueProp() {

javascript/ql/src/semmle/javascript/internal/CachedStages.qll

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ module Stages {
196196
exists(any(DataFlow::TypeTracker t).append(_))
197197
or
198198
exists(any(DataFlow::TypeBackTracker t).prepend(_))
199+
or
200+
DataFlow::functionForwardingStep(_, _)
199201
}
200202
}
201203

javascript/ql/src/semmle/javascript/security/dataflow/MissingRateLimiting.qll

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -113,29 +113,53 @@ class DatabaseAccessAsExpensiveAction extends ExpensiveAction {
113113
* A route handler expression that is rate-limited by a rate-limiting middleware.
114114
*/
115115
class RouteHandlerExpressionWithRateLimiter extends RateLimitedRouteHandlerExpr {
116-
RouteHandlerExpressionWithRateLimiter() { getAMatchingAncestor() instanceof RateLimiter }
116+
RouteHandlerExpressionWithRateLimiter() {
117+
any(RateLimitingMiddleware m).ref().flowsToExpr(getAMatchingAncestor())
118+
}
117119
}
118120

119121
/**
122+
* DEPRECATED. Use `RateLimitingMiddleware` instead.
123+
*
120124
* A middleware that acts as a rate limiter.
121125
*/
122-
abstract class RateLimiter extends Express::RouteHandlerExpr { }
126+
deprecated class RateLimiter extends Express::RouteHandlerExpr {
127+
RateLimiter() { any(RateLimitingMiddleware m).ref().flowsToExpr(this) }
128+
}
129+
130+
/**
131+
* Creation of a middleware function that acts as a rate limiter.
132+
*/
133+
abstract class RateLimitingMiddleware extends DataFlow::SourceNode {
134+
/** Gets a data flow node referring to this middleware. */
135+
private DataFlow::SourceNode ref(DataFlow::TypeTracker t) {
136+
t.start() and
137+
result = this
138+
or
139+
DataFlow::functionOneWayForwardingStep(ref(t.continue()).getALocalUse(), result)
140+
or
141+
exists(DataFlow::TypeTracker t2 | result = ref(t2).track(t2, t))
142+
}
143+
144+
/** Gets a data flow node referring to this middleware. */
145+
DataFlow::SourceNode ref() { result = ref(DataFlow::TypeTracker::end()) }
146+
}
123147

124148
/**
125149
* A rate limiter constructed using the `express-rate-limit` package.
126150
*/
127-
class ExpressRateLimit extends RateLimiter {
151+
class ExpressRateLimit extends RateLimitingMiddleware {
128152
ExpressRateLimit() {
129-
this = API::moduleImport("express-rate-limit").getReturn().getAUse().asExpr()
153+
this = API::moduleImport("express-rate-limit").getReturn().getAnImmediateUse()
130154
}
131155
}
132156

133157
/**
134158
* A rate limiter constructed using the `express-brute` package.
135159
*/
136-
class BruteForceRateLimit extends RateLimiter {
160+
class BruteForceRateLimit extends RateLimitingMiddleware {
137161
BruteForceRateLimit() {
138-
this = API::moduleImport("express-brute").getInstance().getMember("prevent").getAUse().asExpr()
162+
this = API::moduleImport("express-brute").getInstance().getMember("prevent").getAnImmediateUse()
139163
}
140164
}
141165

@@ -183,8 +207,6 @@ class RateLimiterFlexibleRateLimiter extends DataFlow::FunctionNode {
183207
/**
184208
* A route-handler expression that is rate-limited by the `rate-limiter-flexible` package.
185209
*/
186-
class RouteHandlerLimitedByRateLimiterFlexible extends RateLimiter {
187-
RouteHandlerLimitedByRateLimiterFlexible() {
188-
any(RateLimiterFlexibleRateLimiter rl).flowsToExpr(this)
189-
}
210+
class RouteHandlerLimitedByRateLimiterFlexible extends RateLimitingMiddleware {
211+
RouteHandlerLimitedByRateLimiterFlexible() { this instanceof RateLimiterFlexibleRateLimiter }
190212
}

javascript/ql/test/query-tests/Security/CWE-770/MissingRateLimiting.expected

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
| tst.js:37:20:37:36 | expensiveHandler3 | This route handler performs $@, but is not rate-limited. | tst.js:16:40:16:70 | child_p ... /true") | a system command |
88
| tst.js:38:20:38:36 | expensiveHandler4 | This route handler performs $@, but is not rate-limited. | tst.js:17:40:17:83 | connect ... ution') | a database access |
99
| tst.js:64:25:64:63 | functio ... req); } | This route handler performs $@, but is not rate-limited. | tst.js:64:46:64:60 | verifyUser(req) | authorization |
10+
| tst.js:76:25:76:53 | catchAs ... ndler1) | This route handler performs $@, but is not rate-limited. | tst.js:14:40:14:46 | login() | authorization |

0 commit comments

Comments
 (0)