|
2 | 2 | * @name Server crash |
3 | 3 | * @description A server that can be forced to crash may be vulnerable to denial-of-service |
4 | 4 | * attacks. |
5 | | - * @kind problem |
| 5 | + * @kind path-problem |
6 | 6 | * @problem.severity error |
7 | 7 | * @precision high |
8 | 8 | * @id js/server-crash |
|
13 | 13 | import javascript |
14 | 14 |
|
15 | 15 | /** |
16 | | - * Gets a function that `caller` invokes. |
| 16 | + * A call that appears to be asynchronous (heuristic). |
17 | 17 | */ |
18 | | -Function getACallee(Function caller) { |
19 | | - exists(DataFlow::InvokeNode invk | |
20 | | - invk.getEnclosingFunction() = caller and result = invk.getACallee() |
21 | | - ) |
| 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 | + } |
| 27 | + |
| 28 | + /** |
| 29 | + * Gets the callback that is invoked asynchronously. |
| 30 | + */ |
| 31 | + DataFlow::FunctionNode getCallback() { result = callback } |
22 | 32 | } |
23 | 33 |
|
24 | 34 | /** |
25 | | - * Gets a function that `caller` invokes, excluding calls guarded in `try`-blocks. |
| 35 | + * Gets a function that is invoked as a consequence of invoking a route handler `rh`. |
26 | 36 | */ |
27 | | -Function getAnUnguardedCallee(Function caller) { |
| 37 | +Function invokedByRouteHandler(HTTP::RouteHandler rh) { |
| 38 | + rh = result.flow() |
| 39 | + or |
| 40 | + // follow the immediate call graph |
28 | 41 | exists(DataFlow::InvokeNode invk | |
29 | | - invk.getEnclosingFunction() = caller and |
30 | 42 | result = invk.getACallee() and |
31 | | - not exists(invk.asExpr().getEnclosingStmt().getEnclosingTryCatchStmt()) |
| 43 | + invk.getEnclosingFunction() = invokedByRouteHandler(rh) |
32 | 44 | ) |
| 45 | + // if new edges are added here, the `edges` predicate should be updated accordingly |
33 | 46 | } |
34 | 47 |
|
35 | | -predicate isHeaderValue(HTTP::ExplicitHeaderDefinition def, DataFlow::Node node) { |
36 | | - def.definesExplicitly(_, node.asExpr()) |
| 48 | +/** |
| 49 | + * A callback provided to an asynchronous call. |
| 50 | + */ |
| 51 | +class AsyncCallback extends DataFlow::FunctionNode { |
| 52 | + AsyncCallback() { this = any(AsyncCall c).getCallback() } |
37 | 53 | } |
38 | 54 |
|
39 | | -class Configuration extends TaintTracking::Configuration { |
40 | | - Configuration() { this = "Configuration" } |
41 | | - |
42 | | - override predicate isSource(DataFlow::Node node) { node instanceof RemoteFlowSource } |
| 55 | +/** |
| 56 | + * 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. |
| 57 | + */ |
| 58 | +Function inUnguardedAsyncCallStack(AsyncCallback callback) { |
| 59 | + callback = result.flow() |
| 60 | + or |
| 61 | + exists(DataFlow::InvokeNode invk | |
| 62 | + result = invk.getACallee() and |
| 63 | + not exists(invk.asExpr().getEnclosingStmt().getEnclosingTryCatchStmt()) and |
| 64 | + invk.getEnclosingFunction() = inUnguardedAsyncCallStack(callback) |
| 65 | + ) |
| 66 | +} |
43 | 67 |
|
44 | | - override predicate isSink(DataFlow::Node node) { |
45 | | - // using control characters in a header value will cause an exception |
46 | | - isHeaderValue(_, node) |
47 | | - } |
| 68 | +/** |
| 69 | + * Gets a function that is invoked by `asyncCallback` without any try-block wrapping, `asyncCallback` is in turn is called indirectly by `routeHandler`. |
| 70 | + * |
| 71 | + * If the result throws an excection, the server of `routeHandler` will crash. |
| 72 | + */ |
| 73 | +Function getAPotentialServerCrasher( |
| 74 | + HTTP::RouteHandler routeHandler, AsyncCall asyncCall, AsyncCallback asyncCallback |
| 75 | +) { |
| 76 | + // the route handler transitively calls an async function |
| 77 | + asyncCall.getEnclosingFunction() = invokedByRouteHandler(routeHandler) and |
| 78 | + asyncCallback = asyncCall.getCallback() and |
| 79 | + // the async function transitively calls a function that may throw an exception out of the the async function |
| 80 | + result = inUnguardedAsyncCallStack(asyncCallback) |
48 | 81 | } |
49 | 82 |
|
50 | | -predicate isLikelyToThrow(DataFlow::Node crash) { |
51 | | - exists(Configuration cfg, DataFlow::Node sink | cfg.hasFlow(_, sink) | isHeaderValue(crash, sink)) |
| 83 | +/** |
| 84 | + * Gets a node that is likely to throw an uncaught exception in `fun`. |
| 85 | + */ |
| 86 | +LikelyExceptionThrower getALikelyUncaughtExceptionThrower(Function fun) { |
| 87 | + result.getContainer() = fun and |
| 88 | + not exists([result.(Expr).getEnclosingStmt(), result.(Stmt)].getEnclosingTryCatchStmt()) |
52 | 89 | } |
53 | 90 |
|
54 | 91 | /** |
55 | | - * A call that looks like it is asynchronous. |
| 92 | + * Edges that builds an explanatory graph that follows the mental model of how the the exception flows. |
| 93 | + * |
| 94 | + * - step 1. exception is thrown |
| 95 | + * - step 2. exception exits the enclosing function |
| 96 | + * - step 3. exception follows the call graph backwards until an async callee is encountered |
| 97 | + * - step 4. (at this point, the program crashes) |
| 98 | + * - step 5. if the program had not crashed, the exception would conceptually follow the call graph backwards to a route handler |
56 | 99 | */ |
57 | | -class AsyncCall extends DataFlow::CallNode { |
58 | | - DataFlow::FunctionNode callback; |
| 100 | +query predicate edges(ASTNode pred, ASTNode succ) { |
| 101 | + nodes(pred) and |
| 102 | + nodes(succ) and |
| 103 | + ( |
| 104 | + // the first step from the alert location to the enclosing function |
| 105 | + pred = getALikelyUncaughtExceptionThrower(_) and |
| 106 | + succ = pred.getContainer() |
| 107 | + or |
| 108 | + // ordinary flow graph |
| 109 | + exists(DataFlow::InvokeNode invoke, Function f | |
| 110 | + invoke.getACallee() = f and |
| 111 | + succ = invoke.getAstNode() and |
| 112 | + pred = f |
| 113 | + or |
| 114 | + invoke.getContainer() = f and |
| 115 | + succ = f and |
| 116 | + pred = invoke.getAstNode() |
| 117 | + ) |
| 118 | + or |
| 119 | + // the async step |
| 120 | + exists(DataFlow::Node predNode, DataFlow::Node succNode | |
| 121 | + exists(getAPotentialServerCrasher(_, predNode, succNode)) and |
| 122 | + predNode.getAstNode() = succ and |
| 123 | + succNode.getAstNode() = pred |
| 124 | + ) |
| 125 | + ) |
| 126 | +} |
59 | 127 |
|
60 | | - AsyncCall() { |
61 | | - callback.flowsTo(getLastArgument()) and |
62 | | - callback.getParameter(0).getName() = ["e", "err", "error"] and |
63 | | - callback.getNumParameter() = 2 and |
64 | | - not exists(callback.getAReturn()) |
65 | | - } |
| 128 | +/** |
| 129 | + * Nodes for building an explanatory graph that follows the mental model of how the the exception flows. |
| 130 | + */ |
| 131 | +query predicate nodes(ASTNode node) { |
| 132 | + exists(HTTP::RouteHandler rh, Function fun | |
| 133 | + main(rh, _, _) and |
| 134 | + fun = invokedByRouteHandler(rh) |
| 135 | + | |
| 136 | + node = any(DataFlow::InvokeNode invk | invk.getACallee() = fun).getAstNode() or |
| 137 | + node = fun |
| 138 | + ) |
| 139 | + or |
| 140 | + exists(AsyncCallback cb, Function fun | |
| 141 | + main(_, cb, _) and |
| 142 | + fun = inUnguardedAsyncCallStack(cb) |
| 143 | + | |
| 144 | + node = any(DataFlow::InvokeNode invk | invk.getACallee() = fun).getAstNode() or |
| 145 | + node = fun |
| 146 | + ) |
| 147 | + or |
| 148 | + main(_, _, node) |
| 149 | +} |
66 | 150 |
|
67 | | - DataFlow::FunctionNode getCallback() { result = callback } |
| 151 | +predicate main(HTTP::RouteHandler rh, AsyncCallback asyncCallback, ExprOrStmt crasher) { |
| 152 | + crasher = getALikelyUncaughtExceptionThrower(getAPotentialServerCrasher(rh, _, asyncCallback)) |
68 | 153 | } |
69 | 154 |
|
70 | 155 | /** |
71 | | - * Gets a function that is invoked by `asyncCallback` without any try-block wrapping, `asyncCallback` is in turn is called indirectly by `routeHandler`. |
| 156 | + * A node that is likely to throw an exception. |
72 | 157 | * |
73 | | - * If the result throws an excection, the server of `routeHandler` will crash. |
| 158 | + * This is the primary extension point for this query. |
74 | 159 | */ |
75 | | -Function getAPotentialServerCrasher( |
76 | | - HTTP::RouteHandler routeHandler, DataFlow::FunctionNode asyncCallback |
77 | | -) { |
78 | | - exists(AsyncCall asyncCall | |
79 | | - // the route handler transitively calls an async function |
80 | | - asyncCall.getEnclosingFunction() = |
81 | | - getACallee*(routeHandler.(DataFlow::FunctionNode).getFunction()) and |
82 | | - asyncCallback = asyncCall.getCallback() and |
83 | | - // the async function transitively calls a function that may throw an exception out of the the async function |
84 | | - result = getAnUnguardedCallee*(asyncCallback.getFunction()) |
85 | | - ) |
86 | | -} |
| 160 | +abstract class LikelyExceptionThrower extends ASTNode { } |
87 | 161 |
|
88 | 162 | /** |
89 | | - * Gets an AST node that is likely to throw an uncaught exception in `fun`. |
| 163 | + * A `throw` statement. |
90 | 164 | */ |
91 | | -ExprOrStmt getALikelyExceptionThrower(Function fun) { |
92 | | - result.getContainer() = fun and |
93 | | - not exists([result.(Expr).getEnclosingStmt(), result.(Stmt)].getEnclosingTryCatchStmt()) and |
94 | | - (isLikelyToThrow(result.(Expr).flow()) or result instanceof ThrowStmt) |
| 165 | +class TrivialThrowStatement extends LikelyExceptionThrower, ThrowStmt { } |
| 166 | + |
| 167 | +/** |
| 168 | + * Empty class for avoiding emptiness checks from the compiler when there are no Expr-typed instances of the LikelyExceptionThrower type. |
| 169 | + */ |
| 170 | +class CompilerConfusingExceptionThrower extends LikelyExceptionThrower { |
| 171 | + CompilerConfusingExceptionThrower() { none() } |
95 | 172 | } |
96 | 173 |
|
97 | | -from HTTP::RouteHandler routeHandler, DataFlow::FunctionNode asyncCallback, ExprOrStmt crasher |
98 | | -where crasher = getALikelyExceptionThrower(getAPotentialServerCrasher(routeHandler, asyncCallback)) |
99 | | -select crasher, "When an exception is thrown here and later exits $@, the server of $@ will crash.", |
100 | | - asyncCallback, "this asynchronous callback", routeHandler, "this route handler" |
| 174 | +from HTTP::RouteHandler rh, AsyncCallback asyncCallback, ExprOrStmt crasher |
| 175 | +where main(rh, asyncCallback, crasher) |
| 176 | +select crasher, crasher, rh.getAstNode(), |
| 177 | + "When an exception is thrown here and later escapes at $@, the server of $@ will crash.", |
| 178 | + asyncCallback, "this asynchronous callback", rh, "this route handler" |
0 commit comments