JS: reformulate js/server-crash as a path problem

This commit is contained in:
Esben Sparre Andreasen
2021-01-12 15:11:52 +01:00
parent 2dbd762bd9
commit d591c519a8
3 changed files with 185 additions and 65 deletions

View File

@@ -2,7 +2,7 @@
* @name Server crash
* @description A server that can be forced to crash may be vulnerable to denial-of-service
* attacks.
* @kind problem
* @kind path-problem
* @problem.severity error
* @precision high
* @id js/server-crash
@@ -13,46 +13,7 @@
import javascript
/**
* Gets a function that `caller` invokes.
*/
Function getACallee(Function caller) {
exists(DataFlow::InvokeNode invk |
invk.getEnclosingFunction() = caller and result = invk.getACallee()
)
}
/**
* Gets a function that `caller` invokes, excluding calls guarded in `try`-blocks.
*/
Function getAnUnguardedCallee(Function caller) {
exists(DataFlow::InvokeNode invk |
invk.getEnclosingFunction() = caller and
result = invk.getACallee() and
not exists(invk.asExpr().getEnclosingStmt().getEnclosingTryCatchStmt())
)
}
predicate isHeaderValue(HTTP::ExplicitHeaderDefinition def, DataFlow::Node node) {
def.definesExplicitly(_, node.asExpr())
}
class Configuration extends TaintTracking::Configuration {
Configuration() { this = "Configuration" }
override predicate isSource(DataFlow::Node node) { node instanceof RemoteFlowSource }
override predicate isSink(DataFlow::Node node) {
// using control characters in a header value will cause an exception
isHeaderValue(_, node)
}
}
predicate isLikelyToThrow(DataFlow::Node crash) {
exists(Configuration cfg, DataFlow::Node sink | cfg.hasFlow(_, sink) | isHeaderValue(crash, sink))
}
/**
* A call that looks like it is asynchronous.
* A call that appears to be asynchronous (heuristic).
*/
class AsyncCall extends DataFlow::CallNode {
DataFlow::FunctionNode callback;
@@ -64,37 +25,154 @@ class AsyncCall extends DataFlow::CallNode {
not exists(callback.getAReturn())
}
/**
* Gets the callback that is invoked asynchronously.
*/
DataFlow::FunctionNode getCallback() { result = callback }
}
/**
* Gets a function that is invoked as a consequence of invoking a route handler `rh`.
*/
Function invokedByRouteHandler(HTTP::RouteHandler rh) {
rh = result.flow()
or
// follow the immediate call graph
exists(DataFlow::InvokeNode invk |
result = invk.getACallee() and
invk.getEnclosingFunction() = invokedByRouteHandler(rh)
)
// if new edges are added here, the `edges` predicate should be updated accordingly
}
/**
* A callback provided to an asynchronous call.
*/
class AsyncCallback extends DataFlow::FunctionNode {
AsyncCallback() { this = any(AsyncCall c).getCallback() }
}
/**
* 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.
*/
Function inUnguardedAsyncCallStack(AsyncCallback callback) {
callback = result.flow()
or
exists(DataFlow::InvokeNode invk |
result = invk.getACallee() and
not exists(invk.asExpr().getEnclosingStmt().getEnclosingTryCatchStmt()) and
invk.getEnclosingFunction() = inUnguardedAsyncCallStack(callback)
)
}
/**
* Gets a function that is invoked by `asyncCallback` without any try-block wrapping, `asyncCallback` is in turn is called indirectly by `routeHandler`.
*
* If the result throws an excection, the server of `routeHandler` will crash.
*/
Function getAPotentialServerCrasher(
HTTP::RouteHandler routeHandler, DataFlow::FunctionNode asyncCallback
HTTP::RouteHandler routeHandler, AsyncCall asyncCall, AsyncCallback asyncCallback
) {
exists(AsyncCall asyncCall |
// the route handler transitively calls an async function
asyncCall.getEnclosingFunction() =
getACallee*(routeHandler.(DataFlow::FunctionNode).getFunction()) and
asyncCallback = asyncCall.getCallback() and
// the async function transitively calls a function that may throw an exception out of the the async function
result = getAnUnguardedCallee*(asyncCallback.getFunction())
// the route handler transitively calls an async function
asyncCall.getEnclosingFunction() = invokedByRouteHandler(routeHandler) and
asyncCallback = asyncCall.getCallback() and
// the async function transitively calls a function that may throw an exception out of the the async function
result = inUnguardedAsyncCallStack(asyncCallback)
}
/**
* Gets a node that is likely to throw an uncaught exception in `fun`.
*/
LikelyExceptionThrower getALikelyUncaughtExceptionThrower(Function fun) {
result.getContainer() = fun and
not exists([result.(Expr).getEnclosingStmt(), result.(Stmt)].getEnclosingTryCatchStmt())
}
/**
* Edges that builds an explanatory graph that follows the mental model of how the the exception flows.
*
* - step 1. exception is thrown
* - step 2. exception exits the enclosing function
* - step 3. exception follows the call graph backwards until an async callee is encountered
* - step 4. (at this point, the program crashes)
* - step 5. if the program had not crashed, the exception would conceptually follow the call graph backwards to a route handler
*/
query predicate edges(ASTNode pred, ASTNode succ) {
nodes(pred) and
nodes(succ) and
(
// the first step from the alert location to the enclosing function
pred = getALikelyUncaughtExceptionThrower(_) and
succ = pred.getContainer()
or
// ordinary flow graph
exists(DataFlow::InvokeNode invoke, Function f |
invoke.getACallee() = f and
succ = invoke.getAstNode() and
pred = f
or
invoke.getContainer() = f and
succ = f and
pred = invoke.getAstNode()
)
or
// the async step
exists(DataFlow::Node predNode, DataFlow::Node succNode |
exists(getAPotentialServerCrasher(_, predNode, succNode)) and
predNode.getAstNode() = succ and
succNode.getAstNode() = pred
)
)
}
/**
* Gets an AST node that is likely to throw an uncaught exception in `fun`.
* Nodes for building an explanatory graph that follows the mental model of how the the exception flows.
*/
ExprOrStmt getALikelyExceptionThrower(Function fun) {
result.getContainer() = fun and
not exists([result.(Expr).getEnclosingStmt(), result.(Stmt)].getEnclosingTryCatchStmt()) and
(isLikelyToThrow(result.(Expr).flow()) or result instanceof ThrowStmt)
query predicate nodes(ASTNode node) {
exists(HTTP::RouteHandler rh, Function fun |
main(rh, _, _) and
fun = invokedByRouteHandler(rh)
|
node = any(DataFlow::InvokeNode invk | invk.getACallee() = fun).getAstNode() or
node = fun
)
or
exists(AsyncCallback cb, Function fun |
main(_, cb, _) and
fun = inUnguardedAsyncCallStack(cb)
|
node = any(DataFlow::InvokeNode invk | invk.getACallee() = fun).getAstNode() or
node = fun
)
or
main(_, _, node)
}
from HTTP::RouteHandler routeHandler, DataFlow::FunctionNode asyncCallback, ExprOrStmt crasher
where crasher = getALikelyExceptionThrower(getAPotentialServerCrasher(routeHandler, asyncCallback))
select crasher, "When an exception is thrown here and later exits $@, the server of $@ will crash.",
asyncCallback, "this asynchronous callback", routeHandler, "this route handler"
predicate main(HTTP::RouteHandler rh, AsyncCallback asyncCallback, ExprOrStmt crasher) {
crasher = getALikelyUncaughtExceptionThrower(getAPotentialServerCrasher(rh, _, asyncCallback))
}
/**
* A node that is likely to throw an exception.
*
* This is the primary extension point for this query.
*/
abstract class LikelyExceptionThrower extends ASTNode { }
/**
* A `throw` statement.
*/
class TrivialThrowStatement extends LikelyExceptionThrower, ThrowStmt { }
/**
* Empty class for avoiding emptiness checks from the compiler when there are no Expr-typed instances of the LikelyExceptionThrower type.
*/
class CompilerConfusingExceptionThrower extends LikelyExceptionThrower {
CompilerConfusingExceptionThrower() { none() }
}
from HTTP::RouteHandler rh, AsyncCallback asyncCallback, ExprOrStmt crasher
where main(rh, asyncCallback, crasher)
select crasher, crasher, rh.getAstNode(),
"When an exception is thrown here and later escapes at $@, the server of $@ will crash.",
asyncCallback, "this asynchronous callback", rh, "this route handler"

View File

@@ -1,6 +1,48 @@
| server-crash.js:7:5:7:14 | throw err; | When an exception is thrown here and later exits $@, the server of $@ will crash. | server-crash.js:6:28:8:3 | (err, x ... OK\\n } | this asynchronous callback | server-crash.js:31:25:73:1 | (req, r ... });\\n} | this route handler |
| server-crash.js:11:3:11:11 | throw 42; | When an exception is thrown here and later exits $@, the server of $@ will crash. | server-crash.js:50:28:52:3 | (err, x ... ();\\n } | this asynchronous callback | server-crash.js:31:25:73:1 | (req, r ... });\\n} | this route handler |
| server-crash.js:16:7:16:16 | throw err; | When an exception is thrown here and later exits $@, the server of $@ will crash. | server-crash.js:15:30:17:5 | (err, x ... K\\n } | this asynchronous callback | server-crash.js:31:25:73:1 | (req, r ... });\\n} | this route handler |
| server-crash.js:28:5:28:14 | throw err; | When an exception is thrown here and later exits $@, the server of $@ will crash. | server-crash.js:27:28:29:3 | (err, x ... OK\\n } | this asynchronous callback | server-crash.js:31:25:73:1 | (req, r ... });\\n} | this route handler |
| server-crash.js:33:5:33:14 | throw err; | When an exception is thrown here and later exits $@, the server of $@ will crash. | server-crash.js:32:28:34:3 | (err, x ... OK\\n } | this asynchronous callback | server-crash.js:31:25:73:1 | (req, r ... });\\n} | this route handler |
| server-crash.js:41:5:41:48 | res.set ... header) | When an exception is thrown here and later exits $@, the server of $@ will crash. | server-crash.js:40:28:42:3 | (err, x ... OK\\n } | this asynchronous callback | server-crash.js:31:25:73:1 | (req, r ... });\\n} | this route handler |
edges
| server-crash.js:5:1:9:1 | functio ... });\\n} | server-crash.js:49:3:49:16 | indirection1() |
| server-crash.js:7:5:7:14 | throw err; | server-crash.js:6:28:8:3 | (err, x ... OK\\n } |
| server-crash.js:10:1:12:1 | functio ... OT OK\\n} | server-crash.js:51:5:51:18 | indirection2() |
| server-crash.js:11:3:11:11 | throw 42; | server-crash.js:10:1:12:1 | functio ... OT OK\\n} |
| server-crash.js:13:1:19:1 | functio ... e) {}\\n} | server-crash.js:54:3:54:16 | indirection3() |
| server-crash.js:16:7:16:16 | throw err; | server-crash.js:15:30:17:5 | (err, x ... K\\n } |
| server-crash.js:20:1:22:1 | functio ... aller\\n} | server-crash.js:56:5:56:18 | indirection4() |
| server-crash.js:23:1:25:1 | functio ... n6();\\n} | server-crash.js:58:3:58:16 | indirection5() |
| server-crash.js:24:3:24:16 | indirection6() | server-crash.js:23:1:25:1 | functio ... n6();\\n} |
| server-crash.js:26:1:30:1 | functio ... });\\n} | server-crash.js:24:3:24:16 | indirection6() |
| server-crash.js:28:5:28:14 | throw err; | server-crash.js:27:28:29:3 | (err, x ... OK\\n } |
| server-crash.js:33:5:33:14 | throw err; | server-crash.js:32:28:34:3 | (err, x ... OK\\n } |
| server-crash.js:49:3:49:16 | indirection1() | server-crash.js:31:25:73:1 | (req, r ... });\\n} |
| server-crash.js:51:5:51:18 | indirection2() | server-crash.js:50:28:52:3 | (err, x ... ();\\n } |
| server-crash.js:54:3:54:16 | indirection3() | server-crash.js:31:25:73:1 | (req, r ... });\\n} |
| server-crash.js:56:5:56:18 | indirection4() | server-crash.js:31:25:73:1 | (req, r ... });\\n} |
| server-crash.js:58:3:58:16 | indirection5() | server-crash.js:31:25:73:1 | (req, r ... });\\n} |
nodes
| server-crash.js:5:1:9:1 | functio ... });\\n} |
| server-crash.js:6:28:8:3 | (err, x ... OK\\n } |
| server-crash.js:7:5:7:14 | throw err; |
| server-crash.js:10:1:12:1 | functio ... OT OK\\n} |
| server-crash.js:11:3:11:11 | throw 42; |
| server-crash.js:13:1:19:1 | functio ... e) {}\\n} |
| server-crash.js:15:30:17:5 | (err, x ... K\\n } |
| server-crash.js:16:7:16:16 | throw err; |
| server-crash.js:20:1:22:1 | functio ... aller\\n} |
| server-crash.js:23:1:25:1 | functio ... n6();\\n} |
| server-crash.js:24:3:24:16 | indirection6() |
| server-crash.js:26:1:30:1 | functio ... });\\n} |
| server-crash.js:27:28:29:3 | (err, x ... OK\\n } |
| server-crash.js:28:5:28:14 | throw err; |
| server-crash.js:31:25:73:1 | (req, r ... });\\n} |
| server-crash.js:32:28:34:3 | (err, x ... OK\\n } |
| server-crash.js:33:5:33:14 | throw err; |
| server-crash.js:49:3:49:16 | indirection1() |
| server-crash.js:50:28:52:3 | (err, x ... ();\\n } |
| server-crash.js:51:5:51:18 | indirection2() |
| server-crash.js:54:3:54:16 | indirection3() |
| server-crash.js:56:5:56:18 | indirection4() |
| server-crash.js:58:3:58:16 | indirection5() |
#select
| server-crash.js:7:5:7:14 | throw err; | server-crash.js:7:5:7:14 | throw err; | server-crash.js:31:25:73:1 | (req, r ... });\\n} | When an exception is thrown here and later escapes at $@, the server of $@ will crash. | server-crash.js:6:28:8:3 | (err, x ... OK\\n } | this asynchronous callback | server-crash.js:31:25:73:1 | (req, r ... });\\n} | this route handler |
| server-crash.js:11:3:11:11 | throw 42; | server-crash.js:11:3:11:11 | throw 42; | server-crash.js:31:25:73:1 | (req, r ... });\\n} | When an exception is thrown here and later escapes at $@, the server of $@ will crash. | server-crash.js:50:28:52:3 | (err, x ... ();\\n } | this asynchronous callback | server-crash.js:31:25:73:1 | (req, r ... });\\n} | this route handler |
| server-crash.js:16:7:16:16 | throw err; | server-crash.js:16:7:16:16 | throw err; | server-crash.js:31:25:73:1 | (req, r ... });\\n} | When an exception is thrown here and later escapes at $@, the server of $@ will crash. | server-crash.js:15:30:17:5 | (err, x ... K\\n } | this asynchronous callback | server-crash.js:31:25:73:1 | (req, r ... });\\n} | this route handler |
| server-crash.js:28:5:28:14 | throw err; | server-crash.js:28:5:28:14 | throw err; | server-crash.js:31:25:73:1 | (req, r ... });\\n} | When an exception is thrown here and later escapes at $@, the server of $@ will crash. | server-crash.js:27:28:29:3 | (err, x ... OK\\n } | this asynchronous callback | server-crash.js:31:25:73:1 | (req, r ... });\\n} | this route handler |
| server-crash.js:33:5:33:14 | throw err; | server-crash.js:33:5:33:14 | throw err; | server-crash.js:31:25:73:1 | (req, r ... });\\n} | When an exception is thrown here and later escapes at $@, the server of $@ will crash. | server-crash.js:32:28:34:3 | (err, x ... OK\\n } | this asynchronous callback | server-crash.js:31:25:73:1 | (req, r ... });\\n} | this route handler |

View File

@@ -38,7 +38,7 @@ app.get("/async-throw", (req, res) => {
} catch (e) {}
});
fs.readFile("/WHATEVER", (err, x) => {
res.setHeader("reflected", req.query.header); // NOT OK
res.setHeader("reflected", req.query.header); // NOT OK [INCONSISTENCY]
});
fs.readFile("/WHATEVER", (err, x) => {
try {