From 2dbd762bd964650efe4cd409646490e3882fc267 Mon Sep 17 00:00:00 2001
From: Esben Sparre Andreasen
Date: Mon, 11 Jan 2021 14:13:41 +0100
Subject: [PATCH 1/9] JS: reintroduce reverted js/server-crash
This reverts commit 0a8d15ccc4acad0ca63af6d39801aa90c7c41588.
---
.../ql/src/Security/CWE-730/ServerCrash.qhelp | 22 ++++
.../ql/src/Security/CWE-730/ServerCrash.ql | 100 ++++++++++++++++++
.../Security/CWE-730/ServerCrash.expected | 6 ++
.../Security/CWE-730/ServerCrash.qlref | 1 +
.../Security/CWE-730/server-crash.js | 73 +++++++++++++
5 files changed, 202 insertions(+)
create mode 100644 javascript/ql/src/Security/CWE-730/ServerCrash.qhelp
create mode 100644 javascript/ql/src/Security/CWE-730/ServerCrash.ql
create mode 100644 javascript/ql/test/query-tests/Security/CWE-730/ServerCrash.expected
create mode 100644 javascript/ql/test/query-tests/Security/CWE-730/ServerCrash.qlref
create mode 100644 javascript/ql/test/query-tests/Security/CWE-730/server-crash.js
diff --git a/javascript/ql/src/Security/CWE-730/ServerCrash.qhelp b/javascript/ql/src/Security/CWE-730/ServerCrash.qhelp
new file mode 100644
index 00000000000..c3258c4e5f1
--- /dev/null
+++ b/javascript/ql/src/Security/CWE-730/ServerCrash.qhelp
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/javascript/ql/src/Security/CWE-730/ServerCrash.ql b/javascript/ql/src/Security/CWE-730/ServerCrash.ql
new file mode 100644
index 00000000000..8db7574d6c9
--- /dev/null
+++ b/javascript/ql/src/Security/CWE-730/ServerCrash.ql
@@ -0,0 +1,100 @@
+/**
+ * @name Server crash
+ * @description A server that can be forced to crash may be vulnerable to denial-of-service
+ * attacks.
+ * @kind problem
+ * @problem.severity error
+ * @precision high
+ * @id js/server-crash
+ * @tags security
+ * external/cwe/cwe-730
+ */
+
+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.
+ */
+class AsyncCall extends DataFlow::CallNode {
+ DataFlow::FunctionNode callback;
+
+ AsyncCall() {
+ callback.flowsTo(getLastArgument()) and
+ callback.getParameter(0).getName() = ["e", "err", "error"] and
+ callback.getNumParameter() = 2 and
+ not exists(callback.getAReturn())
+ }
+
+ DataFlow::FunctionNode getCallback() { result = 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
+) {
+ 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())
+ )
+}
+
+/**
+ * Gets an AST node that is likely to throw an uncaught exception in `fun`.
+ */
+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)
+}
+
+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"
diff --git a/javascript/ql/test/query-tests/Security/CWE-730/ServerCrash.expected b/javascript/ql/test/query-tests/Security/CWE-730/ServerCrash.expected
new file mode 100644
index 00000000000..b4e8de623a6
--- /dev/null
+++ b/javascript/ql/test/query-tests/Security/CWE-730/ServerCrash.expected
@@ -0,0 +1,6 @@
+| 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 |
diff --git a/javascript/ql/test/query-tests/Security/CWE-730/ServerCrash.qlref b/javascript/ql/test/query-tests/Security/CWE-730/ServerCrash.qlref
new file mode 100644
index 00000000000..294d08cdcbb
--- /dev/null
+++ b/javascript/ql/test/query-tests/Security/CWE-730/ServerCrash.qlref
@@ -0,0 +1 @@
+Security/CWE-730/ServerCrash.ql
diff --git a/javascript/ql/test/query-tests/Security/CWE-730/server-crash.js b/javascript/ql/test/query-tests/Security/CWE-730/server-crash.js
new file mode 100644
index 00000000000..96b5fc71187
--- /dev/null
+++ b/javascript/ql/test/query-tests/Security/CWE-730/server-crash.js
@@ -0,0 +1,73 @@
+const express = require("express");
+const app = express();
+const fs = require("fs");
+
+function indirection1() {
+ fs.readFile("/WHATEVER", (err, x) => {
+ throw err; // NOT OK
+ });
+}
+function indirection2() {
+ throw 42; // NOT OK
+}
+function indirection3() {
+ try {
+ fs.readFile("/WHATEVER", (err, x) => {
+ throw err; // NOT OK
+ });
+ } catch (e) {}
+}
+function indirection4() {
+ throw 42; // OK: guarded caller
+}
+function indirection5() {
+ indirection6();
+}
+function indirection6() {
+ fs.readFile("/WHATEVER", (err, x) => {
+ throw err; // NOT OK
+ });
+}
+app.get("/async-throw", (req, res) => {
+ fs.readFile("/WHATEVER", (err, x) => {
+ throw err; // NOT OK
+ });
+ fs.readFile("/WHATEVER", (err, x) => {
+ try {
+ throw err; // OK: guarded throw
+ } catch (e) {}
+ });
+ fs.readFile("/WHATEVER", (err, x) => {
+ res.setHeader("reflected", req.query.header); // NOT OK
+ });
+ fs.readFile("/WHATEVER", (err, x) => {
+ try {
+ res.setHeader("reflected", req.query.header); // OK: guarded call
+ } catch (e) {}
+ });
+
+ indirection1();
+ fs.readFile("/WHATEVER", (err, x) => {
+ indirection2();
+ });
+
+ indirection3();
+ try {
+ indirection4();
+ } catch (e) {}
+ indirection5();
+
+ fs.readFile("/WHATEVER", (err, x) => {
+ req.query.foo; // OK
+ });
+ fs.readFile("/WHATEVER", (err, x) => {
+ req.query.foo.toString(); // OK
+ });
+
+ fs.readFile("/WHATEVER", (err, x) => {
+ req.query.foo.bar; // NOT OK [INCONSISTENCY]: need to add property reads as sinks
+ });
+ fs.readFile("/WHATEVER", (err, x) => {
+ res.setHeader("reflected", unknown); // OK
+ });
+});
From d591c519a8062053bbe588e8133b9297aa6cf4da Mon Sep 17 00:00:00 2001
From: Esben Sparre Andreasen
Date: Tue, 12 Jan 2021 15:11:52 +0100
Subject: [PATCH 2/9] JS: reformulate js/server-crash as a path problem
---
.../ql/src/Security/CWE-730/ServerCrash.ql | 194 ++++++++++++------
.../Security/CWE-730/ServerCrash.expected | 54 ++++-
.../Security/CWE-730/server-crash.js | 2 +-
3 files changed, 185 insertions(+), 65 deletions(-)
diff --git a/javascript/ql/src/Security/CWE-730/ServerCrash.ql b/javascript/ql/src/Security/CWE-730/ServerCrash.ql
index 8db7574d6c9..4df31174150 100644
--- a/javascript/ql/src/Security/CWE-730/ServerCrash.ql
+++ b/javascript/ql/src/Security/CWE-730/ServerCrash.ql
@@ -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"
diff --git a/javascript/ql/test/query-tests/Security/CWE-730/ServerCrash.expected b/javascript/ql/test/query-tests/Security/CWE-730/ServerCrash.expected
index b4e8de623a6..acee72436fc 100644
--- a/javascript/ql/test/query-tests/Security/CWE-730/ServerCrash.expected
+++ b/javascript/ql/test/query-tests/Security/CWE-730/ServerCrash.expected
@@ -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 |
diff --git a/javascript/ql/test/query-tests/Security/CWE-730/server-crash.js b/javascript/ql/test/query-tests/Security/CWE-730/server-crash.js
index 96b5fc71187..7b61262ee92 100644
--- a/javascript/ql/test/query-tests/Security/CWE-730/server-crash.js
+++ b/javascript/ql/test/query-tests/Security/CWE-730/server-crash.js
@@ -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 {
From 12b985be8748f5cd646639eb82aea560424f0bd0 Mon Sep 17 00:00:00 2001
From: Esben Sparre Andreasen
Date: Wed, 13 Jan 2021 14:49:29 +0100
Subject: [PATCH 3/9] Update javascript/ql/src/Security/CWE-730/ServerCrash.ql
Co-authored-by: Erik Krogh Kristensen
---
javascript/ql/src/Security/CWE-730/ServerCrash.ql | 1 +
1 file changed, 1 insertion(+)
diff --git a/javascript/ql/src/Security/CWE-730/ServerCrash.ql b/javascript/ql/src/Security/CWE-730/ServerCrash.ql
index 4df31174150..68607db011f 100644
--- a/javascript/ql/src/Security/CWE-730/ServerCrash.ql
+++ b/javascript/ql/src/Security/CWE-730/ServerCrash.ql
@@ -40,6 +40,7 @@ Function invokedByRouteHandler(HTTP::RouteHandler rh) {
// follow the immediate call graph
exists(DataFlow::InvokeNode invk |
result = invk.getACallee() and
+ // purposely not checking for `getEnclosingTryCatchStmt`. An async callback called from inside a try-catch can still crash the server.
invk.getEnclosingFunction() = invokedByRouteHandler(rh)
)
// if new edges are added here, the `edges` predicate should be updated accordingly
From 1bc7d68a50f402641a6fb5ba555ad4c55d37b763 Mon Sep 17 00:00:00 2001
From: Esben Sparre Andreasen
Date: Wed, 13 Jan 2021 14:49:42 +0100
Subject: [PATCH 4/9] Update
javascript/ql/test/query-tests/Security/CWE-730/server-crash.js
Co-authored-by: Erik Krogh Kristensen
---
.../ql/test/query-tests/Security/CWE-730/server-crash.js | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/javascript/ql/test/query-tests/Security/CWE-730/server-crash.js b/javascript/ql/test/query-tests/Security/CWE-730/server-crash.js
index 7b61262ee92..b1e796cbd7a 100644
--- a/javascript/ql/test/query-tests/Security/CWE-730/server-crash.js
+++ b/javascript/ql/test/query-tests/Security/CWE-730/server-crash.js
@@ -70,4 +70,13 @@ app.get("/async-throw", (req, res) => {
fs.readFile("/WHATEVER", (err, x) => {
res.setHeader("reflected", unknown); // OK
});
+
+ try {
+ indirection7();
+ } catch (e) {}
});
+function indirection7() {
+ fs.readFile("/foo", (err, x) => {
+ throw err; // NOT OK
+ });
+}
From 3015dcd3109563a0ffb415f57ca7fbcc183d5a6e Mon Sep 17 00:00:00 2001
From: Esben Sparre Andreasen
Date: Wed, 13 Jan 2021 15:46:34 +0100
Subject: [PATCH 5/9] JS: reformulate js/server-crash. Support promises and
shorter paths.
---
.../ql/src/Security/CWE-730/ServerCrash.ql | 278 +++++++++---------
.../Security/CWE-730/ServerCrash.expected | 107 ++++---
.../Security/CWE-730/server-crash.js | 108 ++++++-
3 files changed, 302 insertions(+), 191 deletions(-)
diff --git a/javascript/ql/src/Security/CWE-730/ServerCrash.ql b/javascript/ql/src/Security/CWE-730/ServerCrash.ql
index 68607db011f..b81126b33cd 100644
--- a/javascript/ql/src/Security/CWE-730/ServerCrash.ql
+++ b/javascript/ql/src/Security/CWE-730/ServerCrash.ql
@@ -3,7 +3,7 @@
* @description A server that can be forced to crash may be vulnerable to denial-of-service
* attacks.
* @kind path-problem
- * @problem.severity error
+ * @problem.severity warning
* @precision high
* @id js/server-crash
* @tags security
@@ -13,144 +13,114 @@
import javascript
/**
- * A call that appears to be asynchronous (heuristic).
+ * Gets a function that indirectly invokes an asynchronous callback through `async`, where the callback throws an uncaught exception at `thrower`.
*/
-class AsyncCall extends DataFlow::CallNode {
- DataFlow::FunctionNode callback;
+Function invokesCallbackThatThrowsUncaughtException(
+ AsyncSentinelCall async, LikelyExceptionThrower thrower
+) {
+ async.getAsyncCallee() = throwsUncaughtExceptionInAsyncContext(thrower) and
+ result = async.getEnclosingFunction()
+ or
+ exists(DataFlow::InvokeNode invk, Function fun |
+ fun = invokesCallbackThatThrowsUncaughtException(async, thrower) and
+ // purposely not checking for `getEnclosingTryCatchStmt`. An async callback called from inside a try-catch can still crash the server.
+ result = invk.getEnclosingFunction()
+ |
+ invk.getACallee() = fun
+ or
+ // traverse a slightly extended call graph to get additional TPs
+ invk.(AsyncSentinelCall).getAsyncCallee() = fun
+ )
+}
- AsyncCall() {
- callback.flowsTo(getLastArgument()) and
- callback.getParameter(0).getName() = ["e", "err", "error"] and
- callback.getNumParameter() = 2 and
- not exists(callback.getAReturn())
+/**
+ * Gets a callee of an invocation `invk` that is not guarded by a try statement.
+ */
+Function getUncaughtExceptionRethrowerCallee(DataFlow::InvokeNode invk) {
+ not exists(invk.asExpr().getEnclosingStmt().getEnclosingTryCatchStmt()) and
+ result = invk.getACallee()
+}
+
+/**
+ * Holds if `thrower` is not guarded by a try statement.
+ */
+predicate isUncaughtExceptionThrower(LikelyExceptionThrower thrower) {
+ not exists([thrower.(Expr).getEnclosingStmt(), thrower.(Stmt)].getEnclosingTryCatchStmt())
+}
+
+/**
+ * Gets a function that may throw an uncaught exception originating at `thrower`, which then may escape in an asynchronous calling context.
+ */
+Function throwsUncaughtExceptionInAsyncContext(LikelyExceptionThrower thrower) {
+ (
+ isUncaughtExceptionThrower(thrower) and
+ result = thrower.getContainer()
+ or
+ exists(DataFlow::InvokeNode invk |
+ getUncaughtExceptionRethrowerCallee(invk) = throwsUncaughtExceptionInAsyncContext(thrower) and
+ result = invk.getEnclosingFunction()
+ )
+ ) and
+ // Anti-case:
+ // An exception from an `async` function results in a rejected promise.
+ // Unhandled promises requires `node --unhandled-rejections=strict ...` to terminate the process
+ // without that flag, the DEP0018 deprecation warning is printed instead (node.js version 14 and below)
+ not result.isAsync() and
+ // pruning optimization since this predicate always is related to `invokesCallbackThatThrowsUncaughtException`
+ result = reachableFromAsyncCallback()
+}
+
+/**
+ * Holds if `result` is reachable from a callback that is invoked asynchronously.
+ */
+Function reachableFromAsyncCallback() {
+ result instanceof AsyncCallback
+ or
+ exists(DataFlow::InvokeNode invk |
+ invk.getEnclosingFunction() = reachableFromAsyncCallback() and
+ result = invk.getACallee()
+ )
+}
+
+/**
+ * The main predicate of this query: used for both result display and path computation.
+ */
+predicate main(
+ HTTP::RouteHandler rh, AsyncSentinelCall async, AsyncCallback cb, LikelyExceptionThrower thrower
+) {
+ async.getAsyncCallee() = cb and
+ rh.getAstNode() = invokesCallbackThatThrowsUncaughtException(async, thrower)
+}
+
+/**
+ * A call that may cause a function to be invoked in an asynchronous context outside of the visible source code.
+ */
+class AsyncSentinelCall extends DataFlow::CallNode {
+ Function asyncCallee;
+
+ AsyncSentinelCall() {
+ exists(DataFlow::FunctionNode node | node.getAstNode() = asyncCallee |
+ // manual models
+ exists(string memberName |
+ not "Sync" = memberName.suffix(memberName.length() - 4) and
+ this = NodeJSLib::FS::moduleMember(memberName).getACall() and
+ node = this.getCallback([1 .. 2])
+ )
+ // (add additional cases here to improve the query)
+ )
}
/**
- * Gets the callback that is invoked asynchronously.
+ * Gets the callee that is invoked in an asynchronous context.
*/
- DataFlow::FunctionNode getCallback() { result = callback }
+ Function getAsyncCallee() { result = asyncCallee }
}
/**
- * Gets a function that is invoked as a consequence of invoking a route handler `rh`.
+ * A callback provided to an asynchronous call (heuristic).
*/
-Function invokedByRouteHandler(HTTP::RouteHandler rh) {
- rh = result.flow()
- or
- // follow the immediate call graph
- exists(DataFlow::InvokeNode invk |
- result = invk.getACallee() and
- // purposely not checking for `getEnclosingTryCatchStmt`. An async callback called from inside a try-catch can still crash the server.
- 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, AsyncCall asyncCall, AsyncCallback asyncCallback
-) {
- // 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
- )
- )
-}
-
-/**
- * Nodes for building an explanatory graph that follows the mental model of how the the exception flows.
- */
-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)
-}
-
-predicate main(HTTP::RouteHandler rh, AsyncCallback asyncCallback, ExprOrStmt crasher) {
- crasher = getALikelyUncaughtExceptionThrower(getAPotentialServerCrasher(rh, _, asyncCallback))
+class AsyncCallback extends Function {
+ AsyncCallback() { any(AsyncSentinelCall c).getAsyncCallee() = this }
}
/**
@@ -172,8 +142,48 @@ 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"
+/**
+ * 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 escapes 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)
+ */
+query predicate edges(ASTNode pred, ASTNode succ) {
+ exists(LikelyExceptionThrower thrower | main(_, _, _, thrower) |
+ pred = thrower and
+ succ = thrower.getContainer()
+ or
+ exists(DataFlow::InvokeNode invk, Function fun |
+ fun = throwsUncaughtExceptionInAsyncContext(thrower)
+ |
+ succ = invk.getAstNode() and
+ pred = invk.getACallee() and
+ pred = fun
+ or
+ succ = fun and
+ succ = invk.getContainer() and
+ pred = invk.getAstNode()
+ )
+ )
+}
+
+/**
+ * A node in the `edge/2` relation above.
+ */
+query predicate nodes(ASTNode node) {
+ edges(node, _) or
+ edges(_, node)
+}
+
+from
+ HTTP::RouteHandler rh, AsyncSentinelCall async, DataFlow::Node callbackArg, AsyncCallback cb,
+ ExprOrStmt crasher
+where
+ main(rh, async, cb, crasher) and
+ callbackArg.getALocalSource().getAstNode() = cb and
+ async.getAnArgument() = callbackArg
+select crasher, crasher, cb,
+ "The server of $@ will terminate when an uncaught exception from here escapes this $@", rh,
+ "this route handler", callbackArg, "asynchronous callback"
diff --git a/javascript/ql/test/query-tests/Security/CWE-730/ServerCrash.expected b/javascript/ql/test/query-tests/Security/CWE-730/ServerCrash.expected
index acee72436fc..ef20a62aa9d 100644
--- a/javascript/ql/test/query-tests/Security/CWE-730/ServerCrash.expected
+++ b/javascript/ql/test/query-tests/Security/CWE-730/ServerCrash.expected
@@ -1,48 +1,65 @@
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} |
+| server-crash.js:15:5:15:14 | throw err; | server-crash.js:14:23:16:3 | (err, x ... OK\\n } |
+| server-crash.js:18:1:20:1 | functio ... OT OK\\n} | server-crash.js:59:5:59:18 | indirection2() |
+| server-crash.js:19:3:19:11 | throw 42; | server-crash.js:18:1:20:1 | functio ... OT OK\\n} |
+| server-crash.js:24:7:24:16 | throw err; | server-crash.js:23:25:25:5 | (err, x ... K\\n } |
+| server-crash.js:36:5:36:14 | throw err; | server-crash.js:35:23:37:3 | (err, x ... OK\\n } |
+| server-crash.js:41:5:41:14 | throw err; | server-crash.js:40:23:42:3 | (err, x ... OK\\n } |
+| server-crash.js:59:5:59:18 | indirection2() | server-crash.js:58:23:60:3 | (err, x ... ();\\n } |
+| server-crash.js:88:5:88:14 | throw err; | server-crash.js:87:23:89:3 | (err, x ... OK\\n } |
+| server-crash.js:94:5:94:14 | throw "e"; | server-crash.js:93:22:95:3 | () => { ... OK\\n } |
+| server-crash.js:102:7:102:16 | throw "e"; | server-crash.js:101:24:103:5 | () => { ... K\\n } |
+| server-crash.js:109:9:109:18 | throw "e"; | server-crash.js:108:26:110:7 | () => { ... } |
+| server-crash.js:117:9:117:18 | throw "e"; | server-crash.js:116:26:118:7 | () => { ... } |
+| server-crash.js:131:7:131:16 | throw err; | server-crash.js:130:25:132:5 | (err, x ... K\\n } |
+| server-crash.js:152:3:154:3 | functio ... OK\\n } | server-crash.js:157:5:157:16 | throwError() |
+| server-crash.js:152:3:154:3 | functio ... OK\\n } | server-crash.js:160:5:160:16 | throwError() |
+| server-crash.js:152:3:154:3 | functio ... OK\\n } | server-crash.js:164:3:164:14 | throwError() |
+| server-crash.js:153:5:153:22 | throw new Error(); | server-crash.js:152:3:154:3 | functio ... OK\\n } |
+| server-crash.js:153:11:153:21 | new Error() | server-crash.js:152:3:154:3 | functio ... OK\\n } |
+| server-crash.js:157:5:157:16 | throwError() | server-crash.js:156:3:158:3 | functio ... ath\\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() |
+| server-crash.js:14:23:16:3 | (err, x ... OK\\n } |
+| server-crash.js:15:5:15:14 | throw err; |
+| server-crash.js:18:1:20:1 | functio ... OT OK\\n} |
+| server-crash.js:19:3:19:11 | throw 42; |
+| server-crash.js:23:25:25:5 | (err, x ... K\\n } |
+| server-crash.js:24:7:24:16 | throw err; |
+| server-crash.js:35:23:37:3 | (err, x ... OK\\n } |
+| server-crash.js:36:5:36:14 | throw err; |
+| server-crash.js:40:23:42:3 | (err, x ... OK\\n } |
+| server-crash.js:41:5:41:14 | throw err; |
+| server-crash.js:58:23:60:3 | (err, x ... ();\\n } |
+| server-crash.js:59:5:59:18 | indirection2() |
+| server-crash.js:87:23:89:3 | (err, x ... OK\\n } |
+| server-crash.js:88:5:88:14 | throw err; |
+| server-crash.js:93:22:95:3 | () => { ... OK\\n } |
+| server-crash.js:94:5:94:14 | throw "e"; |
+| server-crash.js:101:24:103:5 | () => { ... K\\n } |
+| server-crash.js:102:7:102:16 | throw "e"; |
+| server-crash.js:108:26:110:7 | () => { ... } |
+| server-crash.js:109:9:109:18 | throw "e"; |
+| server-crash.js:116:26:118:7 | () => { ... } |
+| server-crash.js:117:9:117:18 | throw "e"; |
+| server-crash.js:130:25:132:5 | (err, x ... K\\n } |
+| server-crash.js:131:7:131:16 | throw err; |
+| server-crash.js:152:3:154:3 | functio ... OK\\n } |
+| server-crash.js:153:5:153:22 | throw new Error(); |
+| server-crash.js:153:11:153:21 | new Error() |
+| server-crash.js:156:3:158:3 | functio ... ath\\n } |
+| server-crash.js:157:5:157:16 | throwError() |
+| server-crash.js:160:5:160:16 | throwError() |
+| server-crash.js:164:3:164:14 | throwError() |
#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 |
+| server-crash.js:15:5:15:14 | throw err; | server-crash.js:15:5:15:14 | throw err; | server-crash.js:14:23:16:3 | (err, x ... OK\\n } | The server of $@ will terminate when an uncaught exception from here escapes this $@ | server-crash.js:39:25:85:1 | (req, r ... e) {}\\n} | this route handler | server-crash.js:14:23:16:3 | (err, x ... OK\\n } | asynchronous callback |
+| server-crash.js:19:3:19:11 | throw 42; | server-crash.js:19:3:19:11 | throw 42; | server-crash.js:58:23:60:3 | (err, x ... ();\\n } | The server of $@ will terminate when an uncaught exception from here escapes this $@ | server-crash.js:39:25:85:1 | (req, r ... e) {}\\n} | this route handler | server-crash.js:58:23:60:3 | (err, x ... ();\\n } | asynchronous callback |
+| server-crash.js:24:7:24:16 | throw err; | server-crash.js:24:7:24:16 | throw err; | server-crash.js:23:25:25:5 | (err, x ... K\\n } | The server of $@ will terminate when an uncaught exception from here escapes this $@ | server-crash.js:39:25:85:1 | (req, r ... e) {}\\n} | this route handler | server-crash.js:23:25:25:5 | (err, x ... K\\n } | asynchronous callback |
+| server-crash.js:36:5:36:14 | throw err; | server-crash.js:36:5:36:14 | throw err; | server-crash.js:35:23:37:3 | (err, x ... OK\\n } | The server of $@ will terminate when an uncaught exception from here escapes this $@ | server-crash.js:39:25:85:1 | (req, r ... e) {}\\n} | this route handler | server-crash.js:35:23:37:3 | (err, x ... OK\\n } | asynchronous callback |
+| server-crash.js:41:5:41:14 | throw err; | server-crash.js:41:5:41:14 | throw err; | server-crash.js:40:23:42:3 | (err, x ... OK\\n } | The server of $@ will terminate when an uncaught exception from here escapes this $@ | server-crash.js:39:25:85:1 | (req, r ... e) {}\\n} | this route handler | server-crash.js:40:23:42:3 | (err, x ... OK\\n } | asynchronous callback |
+| server-crash.js:88:5:88:14 | throw err; | server-crash.js:88:5:88:14 | throw err; | server-crash.js:87:23:89:3 | (err, x ... OK\\n } | The server of $@ will terminate when an uncaught exception from here escapes this $@ | server-crash.js:39:25:85:1 | (req, r ... e) {}\\n} | this route handler | server-crash.js:87:23:89:3 | (err, x ... OK\\n } | asynchronous callback |
+| server-crash.js:94:5:94:14 | throw "e"; | server-crash.js:94:5:94:14 | throw "e"; | server-crash.js:93:22:95:3 | () => { ... OK\\n } | The server of $@ will terminate when an uncaught exception from here escapes this $@ | server-crash.js:92:31:120:1 | (req, r ... });\\n} | this route handler | server-crash.js:93:22:95:3 | () => { ... OK\\n } | asynchronous callback |
+| server-crash.js:102:7:102:16 | throw "e"; | server-crash.js:102:7:102:16 | throw "e"; | server-crash.js:101:24:103:5 | () => { ... K\\n } | The server of $@ will terminate when an uncaught exception from here escapes this $@ | server-crash.js:92:31:120:1 | (req, r ... });\\n} | this route handler | server-crash.js:101:24:103:5 | () => { ... K\\n } | asynchronous callback |
+| server-crash.js:109:9:109:18 | throw "e"; | server-crash.js:109:9:109:18 | throw "e"; | server-crash.js:108:26:110:7 | () => { ... } | The server of $@ will terminate when an uncaught exception from here escapes this $@ | server-crash.js:92:31:120:1 | (req, r ... });\\n} | this route handler | server-crash.js:108:26:110:7 | () => { ... } | asynchronous callback |
+| server-crash.js:117:9:117:18 | throw "e"; | server-crash.js:117:9:117:18 | throw "e"; | server-crash.js:116:26:118:7 | () => { ... } | The server of $@ will terminate when an uncaught exception from here escapes this $@ | server-crash.js:92:31:120:1 | (req, r ... });\\n} | this route handler | server-crash.js:116:26:118:7 | () => { ... } | asynchronous callback |
+| server-crash.js:131:7:131:16 | throw err; | server-crash.js:131:7:131:16 | throw err; | server-crash.js:130:25:132:5 | (err, x ... K\\n } | The server of $@ will terminate when an uncaught exception from here escapes this $@ | server-crash.js:128:32:135:1 | async ( ... un();\\n} | this route handler | server-crash.js:130:25:132:5 | (err, x ... K\\n } | asynchronous callback |
+| server-crash.js:153:5:153:22 | throw new Error(); | server-crash.js:153:5:153:22 | throw new Error(); | server-crash.js:156:3:158:3 | functio ... ath\\n } | The server of $@ will terminate when an uncaught exception from here escapes this $@ | server-crash.js:151:40:166:1 | (req, r ... nc();\\n} | this route handler | server-crash.js:161:16:161:17 | cb | asynchronous callback |
diff --git a/javascript/ql/test/query-tests/Security/CWE-730/server-crash.js b/javascript/ql/test/query-tests/Security/CWE-730/server-crash.js
index b1e796cbd7a..cb7bc4d9b8e 100644
--- a/javascript/ql/test/query-tests/Security/CWE-730/server-crash.js
+++ b/javascript/ql/test/query-tests/Security/CWE-730/server-crash.js
@@ -1,9 +1,17 @@
const express = require("express");
const app = express();
const fs = require("fs");
+const EventEmitter = require("events");
+const http = require("http");
+
+const port = 12000;
+let server = app.listen(port, () =>
+ // for manual testing of the fickle node.js runtime
+ console.log(`Example app listening on port ${port}!`)
+);
function indirection1() {
- fs.readFile("/WHATEVER", (err, x) => {
+ fs.readFile("/foo", (err, x) => {
throw err; // NOT OK
});
}
@@ -12,7 +20,7 @@ function indirection2() {
}
function indirection3() {
try {
- fs.readFile("/WHATEVER", (err, x) => {
+ fs.readFile("/foo", (err, x) => {
throw err; // NOT OK
});
} catch (e) {}
@@ -24,30 +32,30 @@ function indirection5() {
indirection6();
}
function indirection6() {
- fs.readFile("/WHATEVER", (err, x) => {
+ fs.readFile("/foo", (err, x) => {
throw err; // NOT OK
});
}
app.get("/async-throw", (req, res) => {
- fs.readFile("/WHATEVER", (err, x) => {
+ fs.readFile("/foo", (err, x) => {
throw err; // NOT OK
});
- fs.readFile("/WHATEVER", (err, x) => {
+ fs.readFile("/foo", (err, x) => {
try {
throw err; // OK: guarded throw
} catch (e) {}
});
- fs.readFile("/WHATEVER", (err, x) => {
+ fs.readFile("/foo", (err, x) => {
res.setHeader("reflected", req.query.header); // NOT OK [INCONSISTENCY]
});
- fs.readFile("/WHATEVER", (err, x) => {
+ fs.readFile("/foo", (err, x) => {
try {
res.setHeader("reflected", req.query.header); // OK: guarded call
} catch (e) {}
});
indirection1();
- fs.readFile("/WHATEVER", (err, x) => {
+ fs.readFile("/foo", (err, x) => {
indirection2();
});
@@ -57,17 +65,17 @@ app.get("/async-throw", (req, res) => {
} catch (e) {}
indirection5();
- fs.readFile("/WHATEVER", (err, x) => {
+ fs.readFile("/foo", (err, x) => {
req.query.foo; // OK
});
- fs.readFile("/WHATEVER", (err, x) => {
+ fs.readFile("/foo", (err, x) => {
req.query.foo.toString(); // OK
});
- fs.readFile("/WHATEVER", (err, x) => {
+ fs.readFile("/foo", (err, x) => {
req.query.foo.bar; // NOT OK [INCONSISTENCY]: need to add property reads as sinks
});
- fs.readFile("/WHATEVER", (err, x) => {
+ fs.readFile("/foo", (err, x) => {
res.setHeader("reflected", unknown); // OK
});
@@ -80,3 +88,79 @@ function indirection7() {
throw err; // NOT OK
});
}
+
+app.get("/async-throw-again", (req, res) => {
+ fs.readFile("foo", () => {
+ throw "e"; // NOT OK
+ });
+ fs.readFileSync("foo", () => {
+ throw "e"; // OK (does not take callbacks at all)
+ });
+ // can nest async calls (and only warns about the inner one)
+ fs.readFile("foo", () => {
+ fs.readFile("bar", () => {
+ throw "e"; // NOT OK
+ });
+ });
+ fs.readFile("foo", () => {
+ // can not catch async exceptions
+ try {
+ fs.readFile("bar", () => {
+ throw "e"; // NOT OK
+ });
+ } catch (e) {}
+ });
+ // can mix sync/async calls
+ fs.readFile("foo", () => {
+ (() =>
+ fs.readFile("bar", () => {
+ throw "e"; // NOT OK
+ }))();
+ });
+});
+
+app.get("/throw-in-promise-1", async (req, res) => {
+ async function fun() {
+ throw new Error(); // OK, requires `node --unhandled-rejections=strict ...` to terminate the process
+ }
+ await fun();
+});
+app.get("/throw-in-promise-2", async (req, res) => {
+ async function fun() {
+ fs.readFile("/foo", (err, x) => {
+ throw err; // NOT OK
+ });
+ }
+ await fun();
+});
+app.get("/throw-in-promise-3", async (req, res) => {
+ fs.readFile("/foo", async (err, x) => {
+ throw err; // OK, requires `node --unhandled-rejections=strict ...` to terminate the process
+ });
+});
+
+app.get("/throw-in-event-emitter", async (req, res) => {
+ class MyEmitter extends EventEmitter {}
+ const myEmitter = new MyEmitter();
+ myEmitter.on("event", () => {
+ throw new Error(); // OK, requires `node --unhandled-rejections=strict ...` to terminate the process
+ });
+ myEmitter.emit("event");
+});
+
+app.get("/throw-with-ambiguous-paths", (req, res) => {
+ function throwError() {
+ throw new Error(); // NOT OK
+ }
+
+ function cb() {
+ throwError(); // on path
+ }
+ function withAsync() {
+ throwError(); // not on path
+ fs.stat(X, cb);
+ }
+
+ throwError(); // not on path
+ withAsync();
+});
From 9e3cc3b1b2a3d1721acdde76cd7f916b01862112 Mon Sep 17 00:00:00 2001
From: Esben Sparre Andreasen
Date: Tue, 19 Jan 2021 09:08:59 +0100
Subject: [PATCH 6/9] JS: add qhelp and changenotes for js/server-crash
---
.../change-notes/2021-01-18-server-crash.md | 2 +
.../ql/src/Security/CWE-730/ServerCrash.qhelp | 84 ++++++++++++++++---
.../CWE-730/examples/server-crash.BAD.js | 21 +++++
.../CWE-730/examples/server-crash.GOOD-A.js | 14 ++++
.../CWE-730/examples/server-crash.GOOD-B.js | 13 +++
5 files changed, 124 insertions(+), 10 deletions(-)
create mode 100644 javascript/change-notes/2021-01-18-server-crash.md
create mode 100644 javascript/ql/src/Security/CWE-730/examples/server-crash.BAD.js
create mode 100644 javascript/ql/src/Security/CWE-730/examples/server-crash.GOOD-A.js
create mode 100644 javascript/ql/src/Security/CWE-730/examples/server-crash.GOOD-B.js
diff --git a/javascript/change-notes/2021-01-18-server-crash.md b/javascript/change-notes/2021-01-18-server-crash.md
new file mode 100644
index 00000000000..bf5383c3bd8
--- /dev/null
+++ b/javascript/change-notes/2021-01-18-server-crash.md
@@ -0,0 +1,2 @@
+lgtm,codescanning
+* The `js/server-crash` query has been added. It highlights servers may be terminated by a malicious user.
diff --git a/javascript/ql/src/Security/CWE-730/ServerCrash.qhelp b/javascript/ql/src/Security/CWE-730/ServerCrash.qhelp
index c3258c4e5f1..e46616ade47 100644
--- a/javascript/ql/src/Security/CWE-730/ServerCrash.qhelp
+++ b/javascript/ql/src/Security/CWE-730/ServerCrash.qhelp
@@ -1,22 +1,86 @@
+"-//Semmle//qhelp//EN"
+"qhelp.dtd">
-
+
-
+
-
+ Servers handle requests from clients until terminated
+ deliberately by a server administrator. A client request that results
+ in an uncaught server-side exception causes the current server
+ response generation to fail, and should not have an effect on
+ subsequent client requests.
-
+
-
+
-
+ Under some circumstances, uncaught exceptions can however
+ cause the entire server to terminate abruptly. Such a behavior is
+ highly undesirable, especially if it gives malicious users the ability
+ to turn off the server at will, which is an efficient
+ denial-of-service attack.
-
+
-
+
+
+
+
+
+
+ Ensure that the processing of client requests can not cause
+ uncaught exceptions to terminate the entire server abruptly.
+
+
+
+
+
+
+
+
+
+ The following server implementation checks if a client-provided
+ file path is valid and throws an exception if the check fails. It can
+ be seen that the exception is uncaught, and it is therefore reasonable to
+ expect the server to respond with an error response to client requests
+ that cause the check to fail.
+
+ But since the exception is uncaught in the context of an
+ asynchronous callback invocation (fs.access(...)), the
+ entire server will terminate instead.
+
+
+
+
+
+
+ To remedy this, the server can catch the exception explicitly with
+ a try/catch block, and generate an appropriate error
+ response instead:
+
+
+
+
+
+
+
+ An alternative is to use an async and
+ await for the asynchronous behavior, since the server
+ will then print warning messages about uncaught exceptions instead of
+ terminating, unless the server was started with the commandline option
+ --unhandled-rejections=strict:
+
+
+
+
+
+
+
+
+
+
diff --git a/javascript/ql/src/Security/CWE-730/examples/server-crash.BAD.js b/javascript/ql/src/Security/CWE-730/examples/server-crash.BAD.js
new file mode 100644
index 00000000000..1b7001666ff
--- /dev/null
+++ b/javascript/ql/src/Security/CWE-730/examples/server-crash.BAD.js
@@ -0,0 +1,21 @@
+const express = require("express"),
+ fs = require("fs");
+
+function save(rootDir, path, content){
+ if (!isValidPath(rootDir, req.query.filePath)) {
+ throw new Error(`Invalid filePath: ${req.query.filePath}`); // BAD crashes the server
+ }
+ // write content to disk
+}
+express().post("/save", (req, res) => {
+ fs.access(rootDir, (err) => {
+ if (err) {
+ console.error(`Server setup is corrupted, ${rootDir} does not exist!`);
+ res.status(500);
+ res.end();
+ }
+ save(rootDir, req.query.path, req.body);
+ res.status(200);
+ res.end();
+ });
+});
diff --git a/javascript/ql/src/Security/CWE-730/examples/server-crash.GOOD-A.js b/javascript/ql/src/Security/CWE-730/examples/server-crash.GOOD-A.js
new file mode 100644
index 00000000000..60314259dc0
--- /dev/null
+++ b/javascript/ql/src/Security/CWE-730/examples/server-crash.GOOD-A.js
@@ -0,0 +1,14 @@
+// ...
+express().post("/save", (req, res) => {
+ fs.access(rootDir, (err) => {
+ // ...
+ try {
+ save(rootDir, req.query.path, req.body); // GOOD no uncaught exception
+ res.status(200);
+ res.end();
+ } catch (e) {
+ res.status(500);
+ res.end();
+ }
+ });
+});
diff --git a/javascript/ql/src/Security/CWE-730/examples/server-crash.GOOD-B.js b/javascript/ql/src/Security/CWE-730/examples/server-crash.GOOD-B.js
new file mode 100644
index 00000000000..1d02956a341
--- /dev/null
+++ b/javascript/ql/src/Security/CWE-730/examples/server-crash.GOOD-B.js
@@ -0,0 +1,13 @@
+// ...
+express().post("/save", async (req, res) => {
+ try {
+ await fs.access(rootDir);
+ } catch (e) {
+ console.error(`Server setup is corrupted, ${rootDir} does not exist!`);
+ res.status(500);
+ res.end();
+ }
+ save(rootDir, req.query.path, req.body); // MAYBE BAD, depends on the commandline options
+ res.status(200);
+ res.end();
+});
From 5a6e6928073603eda6cffbd0930f16c68197c198 Mon Sep 17 00:00:00 2001
From: Esben Sparre Andreasen
Date: Tue, 19 Jan 2021 09:58:08 +0100
Subject: [PATCH 7/9] add js/server-crash to the security suite
---
javascript/config/suites/javascript/security | 1 +
1 file changed, 1 insertion(+)
diff --git a/javascript/config/suites/javascript/security b/javascript/config/suites/javascript/security
index 3e87d292d7b..9f826d04314 100644
--- a/javascript/config/suites/javascript/security
+++ b/javascript/config/suites/javascript/security
@@ -49,6 +49,7 @@
+ semmlecode-javascript-queries/Security/CWE-640/HostHeaderPoisoningInEmailGeneration.ql: /Security/CWE/CWE-640
+ semmlecode-javascript-queries/Security/CWE-643/XpathInjection.ql: /Security/CWE/CWE-643
+ semmlecode-javascript-queries/Security/CWE-730/RegExpInjection.ql: /Security/CWE/CWE-730
++ semmlecode-javascript-queries/Security/CWE-730/ServerCrash.ql: /Security/CWE/CWE-730
+ semmlecode-javascript-queries/Security/CWE-754/UnvalidatedDynamicMethodCall.ql: /Security/CWE/CWE-754
+ semmlecode-javascript-queries/Security/CWE-770/MissingRateLimiting.ql: /Security/CWE/CWE-770
+ semmlecode-javascript-queries/Security/CWE-776/XmlBomb.ql: /Security/CWE/CWE-776
From 718f6eb3fde9837192c282ad35357094f5f1c98a Mon Sep 17 00:00:00 2001
From: Esben Sparre Andreasen
Date: Fri, 22 Jan 2021 13:17:38 +0100
Subject: [PATCH 8/9] JS: update and prettify examples
---
.../CWE-730/examples/server-crash.BAD.js | 33 ++++++++++---------
.../CWE-730/examples/server-crash.GOOD-A.js | 2 +-
.../CWE-730/examples/server-crash.GOOD-B.js | 5 ++-
3 files changed, 20 insertions(+), 20 deletions(-)
diff --git a/javascript/ql/src/Security/CWE-730/examples/server-crash.BAD.js b/javascript/ql/src/Security/CWE-730/examples/server-crash.BAD.js
index 1b7001666ff..9642ced4c1a 100644
--- a/javascript/ql/src/Security/CWE-730/examples/server-crash.BAD.js
+++ b/javascript/ql/src/Security/CWE-730/examples/server-crash.BAD.js
@@ -1,21 +1,22 @@
const express = require("express"),
- fs = require("fs");
+ fs = require("fs");
-function save(rootDir, path, content){
- if (!isValidPath(rootDir, req.query.filePath)) {
- throw new Error(`Invalid filePath: ${req.query.filePath}`); // BAD crashes the server
- }
- // write content to disk
+function save(rootDir, path, content) {
+ if (!isValidPath(rootDir, req.query.filePath)) {
+ throw new Error(`Invalid filePath: ${req.query.filePath}`); // BAD crashes the server
+ }
+ // write content to disk
}
express().post("/save", (req, res) => {
- fs.access(rootDir, (err) => {
- if (err) {
- console.error(`Server setup is corrupted, ${rootDir} does not exist!`);
- res.status(500);
- res.end();
- }
- save(rootDir, req.query.path, req.body);
- res.status(200);
- res.end();
- });
+ fs.exists(rootDir, (exists) => {
+ if (!exists) {
+ console.error(`Server setup is corrupted, ${rootDir} does not exist!`);
+ res.status(500);
+ res.end();
+ return;
+ }
+ save(rootDir, req.query.path, req.body);
+ res.status(200);
+ res.end();
+ });
});
diff --git a/javascript/ql/src/Security/CWE-730/examples/server-crash.GOOD-A.js b/javascript/ql/src/Security/CWE-730/examples/server-crash.GOOD-A.js
index 60314259dc0..b23071ea53c 100644
--- a/javascript/ql/src/Security/CWE-730/examples/server-crash.GOOD-A.js
+++ b/javascript/ql/src/Security/CWE-730/examples/server-crash.GOOD-A.js
@@ -1,6 +1,6 @@
// ...
express().post("/save", (req, res) => {
- fs.access(rootDir, (err) => {
+ fs.exists(rootDir, (exists) => {
// ...
try {
save(rootDir, req.query.path, req.body); // GOOD no uncaught exception
diff --git a/javascript/ql/src/Security/CWE-730/examples/server-crash.GOOD-B.js b/javascript/ql/src/Security/CWE-730/examples/server-crash.GOOD-B.js
index 1d02956a341..4e6e3c3bc72 100644
--- a/javascript/ql/src/Security/CWE-730/examples/server-crash.GOOD-B.js
+++ b/javascript/ql/src/Security/CWE-730/examples/server-crash.GOOD-B.js
@@ -1,11 +1,10 @@
// ...
express().post("/save", async (req, res) => {
- try {
- await fs.access(rootDir);
- } catch (e) {
+ if (await fs.promises.exists(rootDir)) {
console.error(`Server setup is corrupted, ${rootDir} does not exist!`);
res.status(500);
res.end();
+ return;
}
save(rootDir, req.query.path, req.body); // MAYBE BAD, depends on the commandline options
res.status(200);
From 3f3962f7a9d3d4a252985e7dc6ce61c5fb71e2a3 Mon Sep 17 00:00:00 2001
From: Esben Sparre Andreasen
Date: Fri, 22 Jan 2021 14:03:21 +0100
Subject: [PATCH 9/9] Update
javascript/ql/src/Security/CWE-730/examples/server-crash.GOOD-B.js
Co-authored-by: Erik Krogh Kristensen
---
.../ql/src/Security/CWE-730/examples/server-crash.GOOD-B.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/javascript/ql/src/Security/CWE-730/examples/server-crash.GOOD-B.js b/javascript/ql/src/Security/CWE-730/examples/server-crash.GOOD-B.js
index 4e6e3c3bc72..2a795ac001f 100644
--- a/javascript/ql/src/Security/CWE-730/examples/server-crash.GOOD-B.js
+++ b/javascript/ql/src/Security/CWE-730/examples/server-crash.GOOD-B.js
@@ -1,6 +1,6 @@
// ...
express().post("/save", async (req, res) => {
- if (await fs.promises.exists(rootDir)) {
+ if (!(await fs.promises.exists(rootDir))) {
console.error(`Server setup is corrupted, ${rootDir} does not exist!`);
res.status(500);
res.end();