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
1313import 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