Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 3015dcd

Browse files
committed
JS: reformulate js/server-crash. Support promises and shorter paths.
1 parent 1bc7d68 commit 3015dcd

3 files changed

Lines changed: 281 additions & 170 deletions

File tree

javascript/ql/src/Security/CWE-730/ServerCrash.ql

Lines changed: 123 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* @description A server that can be forced to crash may be vulnerable to denial-of-service
44
* attacks.
55
* @kind path-problem
6-
* @problem.severity error
6+
* @problem.severity warning
77
* @precision high
88
* @id js/server-crash
99
* @tags security
@@ -13,144 +13,114 @@
1313
import javascript
1414

1515
/**
16-
* A call that appears to be asynchronous (heuristic).
16+
* Gets a function that indirectly invokes an asynchronous callback through `async`, where the callback throws an uncaught exception at `thrower`.
1717
*/
18-
class AsyncCall extends DataFlow::CallNode {
19-
DataFlow::FunctionNode callback;
20-
21-
AsyncCall() {
22-
callback.flowsTo(getLastArgument()) and
23-
callback.getParameter(0).getName() = ["e", "err", "error"] and
24-
callback.getNumParameter() = 2 and
25-
not exists(callback.getAReturn())
26-
}
18+
Function invokesCallbackThatThrowsUncaughtException(
19+
AsyncSentinelCall async, LikelyExceptionThrower thrower
20+
) {
21+
async.getAsyncCallee() = throwsUncaughtExceptionInAsyncContext(thrower) and
22+
result = async.getEnclosingFunction()
23+
or
24+
exists(DataFlow::InvokeNode invk, Function fun |
25+
fun = invokesCallbackThatThrowsUncaughtException(async, thrower) and
26+
// purposely not checking for `getEnclosingTryCatchStmt`. An async callback called from inside a try-catch can still crash the server.
27+
result = invk.getEnclosingFunction()
28+
|
29+
invk.getACallee() = fun
30+
or
31+
// traverse a slightly extended call graph to get additional TPs
32+
invk.(AsyncSentinelCall).getAsyncCallee() = fun
33+
)
34+
}
2735

28-
/**
29-
* Gets the callback that is invoked asynchronously.
30-
*/
31-
DataFlow::FunctionNode getCallback() { result = callback }
36+
/**
37+
* Gets a callee of an invocation `invk` that is not guarded by a try statement.
38+
*/
39+
Function getUncaughtExceptionRethrowerCallee(DataFlow::InvokeNode invk) {
40+
not exists(invk.asExpr().getEnclosingStmt().getEnclosingTryCatchStmt()) and
41+
result = invk.getACallee()
3242
}
3343

3444
/**
35-
* Gets a function that is invoked as a consequence of invoking a route handler `rh`.
45+
* Holds if `thrower` is not guarded by a try statement.
3646
*/
37-
Function invokedByRouteHandler(HTTP::RouteHandler rh) {
38-
rh = result.flow()
39-
or
40-
// follow the immediate call graph
41-
exists(DataFlow::InvokeNode invk |
42-
result = invk.getACallee() and
43-
// purposely not checking for `getEnclosingTryCatchStmt`. An async callback called from inside a try-catch can still crash the server.
44-
invk.getEnclosingFunction() = invokedByRouteHandler(rh)
45-
)
46-
// if new edges are added here, the `edges` predicate should be updated accordingly
47+
predicate isUncaughtExceptionThrower(LikelyExceptionThrower thrower) {
48+
not exists([thrower.(Expr).getEnclosingStmt(), thrower.(Stmt)].getEnclosingTryCatchStmt())
4749
}
4850

4951
/**
50-
* A callback provided to an asynchronous call.
52+
* Gets a function that may throw an uncaught exception originating at `thrower`, which then may escape in an asynchronous calling context.
5153
*/
52-
class AsyncCallback extends DataFlow::FunctionNode {
53-
AsyncCallback() { this = any(AsyncCall c).getCallback() }
54+
Function throwsUncaughtExceptionInAsyncContext(LikelyExceptionThrower thrower) {
55+
(
56+
isUncaughtExceptionThrower(thrower) and
57+
result = thrower.getContainer()
58+
or
59+
exists(DataFlow::InvokeNode invk |
60+
getUncaughtExceptionRethrowerCallee(invk) = throwsUncaughtExceptionInAsyncContext(thrower) and
61+
result = invk.getEnclosingFunction()
62+
)
63+
) and
64+
// Anti-case:
65+
// An exception from an `async` function results in a rejected promise.
66+
// Unhandled promises requires `node --unhandled-rejections=strict ...` to terminate the process
67+
// without that flag, the DEP0018 deprecation warning is printed instead (node.js version 14 and below)
68+
not result.isAsync() and
69+
// pruning optimization since this predicate always is related to `invokesCallbackThatThrowsUncaughtException`
70+
result = reachableFromAsyncCallback()
5471
}
5572

5673
/**
57-
* Gets a function that is in a call stack that starts at an asynchronous `callback`, calls in the call stack occur outside of `try` blocks.
74+
* Holds if `result` is reachable from a callback that is invoked asynchronously.
5875
*/
59-
Function inUnguardedAsyncCallStack(AsyncCallback callback) {
60-
callback = result.flow()
76+
Function reachableFromAsyncCallback() {
77+
result instanceof AsyncCallback
6178
or
6279
exists(DataFlow::InvokeNode invk |
63-
result = invk.getACallee() and
64-
not exists(invk.asExpr().getEnclosingStmt().getEnclosingTryCatchStmt()) and
65-
invk.getEnclosingFunction() = inUnguardedAsyncCallStack(callback)
80+
invk.getEnclosingFunction() = reachableFromAsyncCallback() and
81+
result = invk.getACallee()
6682
)
6783
}
6884

6985
/**
70-
* Gets a function that is invoked by `asyncCallback` without any try-block wrapping, `asyncCallback` is in turn is called indirectly by `routeHandler`.
71-
*
72-
* If the result throws an excection, the server of `routeHandler` will crash.
86+
* The main predicate of this query: used for both result display and path computation.
7387
*/
74-
Function getAPotentialServerCrasher(
75-
HTTP::RouteHandler routeHandler, AsyncCall asyncCall, AsyncCallback asyncCallback
88+
predicate main(
89+
HTTP::RouteHandler rh, AsyncSentinelCall async, AsyncCallback cb, LikelyExceptionThrower thrower
7690
) {
77-
// the route handler transitively calls an async function
78-
asyncCall.getEnclosingFunction() = invokedByRouteHandler(routeHandler) and
79-
asyncCallback = asyncCall.getCallback() and
80-
// the async function transitively calls a function that may throw an exception out of the the async function
81-
result = inUnguardedAsyncCallStack(asyncCallback)
91+
async.getAsyncCallee() = cb and
92+
rh.getAstNode() = invokesCallbackThatThrowsUncaughtException(async, thrower)
8293
}
8394

8495
/**
85-
* Gets a node that is likely to throw an uncaught exception in `fun`.
96+
* A call that may cause a function to be invoked in an asynchronous context outside of the visible source code.
8697
*/
87-
LikelyExceptionThrower getALikelyUncaughtExceptionThrower(Function fun) {
88-
result.getContainer() = fun and
89-
not exists([result.(Expr).getEnclosingStmt(), result.(Stmt)].getEnclosingTryCatchStmt())
90-
}
91-
92-
/**
93-
* Edges that builds an explanatory graph that follows the mental model of how the the exception flows.
94-
*
95-
* - step 1. exception is thrown
96-
* - step 2. exception exits the enclosing function
97-
* - step 3. exception follows the call graph backwards until an async callee is encountered
98-
* - step 4. (at this point, the program crashes)
99-
* - step 5. if the program had not crashed, the exception would conceptually follow the call graph backwards to a route handler
100-
*/
101-
query predicate edges(ASTNode pred, ASTNode succ) {
102-
nodes(pred) and
103-
nodes(succ) and
104-
(
105-
// the first step from the alert location to the enclosing function
106-
pred = getALikelyUncaughtExceptionThrower(_) and
107-
succ = pred.getContainer()
108-
or
109-
// ordinary flow graph
110-
exists(DataFlow::InvokeNode invoke, Function f |
111-
invoke.getACallee() = f and
112-
succ = invoke.getAstNode() and
113-
pred = f
114-
or
115-
invoke.getContainer() = f and
116-
succ = f and
117-
pred = invoke.getAstNode()
118-
)
119-
or
120-
// the async step
121-
exists(DataFlow::Node predNode, DataFlow::Node succNode |
122-
exists(getAPotentialServerCrasher(_, predNode, succNode)) and
123-
predNode.getAstNode() = succ and
124-
succNode.getAstNode() = pred
98+
class AsyncSentinelCall extends DataFlow::CallNode {
99+
Function asyncCallee;
100+
101+
AsyncSentinelCall() {
102+
exists(DataFlow::FunctionNode node | node.getAstNode() = asyncCallee |
103+
// manual models
104+
exists(string memberName |
105+
not "Sync" = memberName.suffix(memberName.length() - 4) and
106+
this = NodeJSLib::FS::moduleMember(memberName).getACall() and
107+
node = this.getCallback([1 .. 2])
108+
)
109+
// (add additional cases here to improve the query)
125110
)
126-
)
111+
}
112+
113+
/**
114+
* Gets the callee that is invoked in an asynchronous context.
115+
*/
116+
Function getAsyncCallee() { result = asyncCallee }
127117
}
128118

129119
/**
130-
* Nodes for building an explanatory graph that follows the mental model of how the the exception flows.
120+
* A callback provided to an asynchronous call (heuristic).
131121
*/
132-
query predicate nodes(ASTNode node) {
133-
exists(HTTP::RouteHandler rh, Function fun |
134-
main(rh, _, _) and
135-
fun = invokedByRouteHandler(rh)
136-
|
137-
node = any(DataFlow::InvokeNode invk | invk.getACallee() = fun).getAstNode() or
138-
node = fun
139-
)
140-
or
141-
exists(AsyncCallback cb, Function fun |
142-
main(_, cb, _) and
143-
fun = inUnguardedAsyncCallStack(cb)
144-
|
145-
node = any(DataFlow::InvokeNode invk | invk.getACallee() = fun).getAstNode() or
146-
node = fun
147-
)
148-
or
149-
main(_, _, node)
150-
}
151-
152-
predicate main(HTTP::RouteHandler rh, AsyncCallback asyncCallback, ExprOrStmt crasher) {
153-
crasher = getALikelyUncaughtExceptionThrower(getAPotentialServerCrasher(rh, _, asyncCallback))
122+
class AsyncCallback extends Function {
123+
AsyncCallback() { any(AsyncSentinelCall c).getAsyncCallee() = this }
154124
}
155125

156126
/**
@@ -172,8 +142,48 @@ class CompilerConfusingExceptionThrower extends LikelyExceptionThrower {
172142
CompilerConfusingExceptionThrower() { none() }
173143
}
174144

175-
from HTTP::RouteHandler rh, AsyncCallback asyncCallback, ExprOrStmt crasher
176-
where main(rh, asyncCallback, crasher)
177-
select crasher, crasher, rh.getAstNode(),
178-
"When an exception is thrown here and later escapes at $@, the server of $@ will crash.",
179-
asyncCallback, "this asynchronous callback", rh, "this route handler"
145+
/**
146+
* Edges that builds an explanatory graph that follows the mental model of how the the exception flows.
147+
*
148+
* - step 1. exception is thrown
149+
* - step 2. exception escapes the enclosing function
150+
* - step 3. exception follows the call graph backwards until an async callee is encountered
151+
* - step 4. (at this point, the program crashes)
152+
*/
153+
query predicate edges(ASTNode pred, ASTNode succ) {
154+
exists(LikelyExceptionThrower thrower | main(_, _, _, thrower) |
155+
pred = thrower and
156+
succ = thrower.getContainer()
157+
or
158+
exists(DataFlow::InvokeNode invk, Function fun |
159+
fun = throwsUncaughtExceptionInAsyncContext(thrower)
160+
|
161+
succ = invk.getAstNode() and
162+
pred = invk.getACallee() and
163+
pred = fun
164+
or
165+
succ = fun and
166+
succ = invk.getContainer() and
167+
pred = invk.getAstNode()
168+
)
169+
)
170+
}
171+
172+
/**
173+
* A node in the `edge/2` relation above.
174+
*/
175+
query predicate nodes(ASTNode node) {
176+
edges(node, _) or
177+
edges(_, node)
178+
}
179+
180+
from
181+
HTTP::RouteHandler rh, AsyncSentinelCall async, DataFlow::Node callbackArg, AsyncCallback cb,
182+
ExprOrStmt crasher
183+
where
184+
main(rh, async, cb, crasher) and
185+
callbackArg.getALocalSource().getAstNode() = cb and
186+
async.getAnArgument() = callbackArg
187+
select crasher, crasher, cb,
188+
"The server of $@ will terminate when an uncaught exception from here escapes this $@", rh,
189+
"this route handler", callbackArg, "asynchronous callback"

0 commit comments

Comments
 (0)