diff --git a/javascript/ql/src/experimental/Security/CWE-918/SSRF.js b/javascript/ql/src/experimental/Security/CWE-918/SSRF.js new file mode 100644 index 000000000000..e39e97604417 --- /dev/null +++ b/javascript/ql/src/experimental/Security/CWE-918/SSRF.js @@ -0,0 +1,15 @@ +const axios = require('axios'); + +export const handler = async (req, res, next) => { + const { targetĀ } = req.body; + + try { + // BAD: `target` is controlled by the attacker + const response = await axios.get('https://example.com/current_api/' + target); + + // process request response + use(response); + } catch (err) { + // process error + } +}; diff --git a/javascript/ql/src/experimental/Security/CWE-918/SSRF.qhelp b/javascript/ql/src/experimental/Security/CWE-918/SSRF.qhelp new file mode 100644 index 000000000000..05ac30b2e954 --- /dev/null +++ b/javascript/ql/src/experimental/Security/CWE-918/SSRF.qhelp @@ -0,0 +1,49 @@ + + + + +

+ Directly incorporating user input into an HTTP request without validating the input can facilitate + server side request forgery attacks, where the attacker essentially controls the request. +

+
+ + +

+ To guard against server side request forgery, it is advisable to avoid putting user input directly into a + network request. If using user input is necessary, then is mandatory to validate them. Only allow numeric and alphanumeric values. + URL encoding is not a solution in certain scenarios, such as, an architecture build over NGINX proxies. +

+
+ + +

+ The following example shows an HTTP request parameter being used directly in a URL request without + validating the input, which facilitates an SSRF attack. The request axios.get("https://example.com/current_api/"+target) is + vulnerable since attackers can choose the value of target to be anything they want. For + instance, the attacker can choose "../super_secret_api" as the target, causing the + URL to become "https://example.com/super_secret_api". +

+ +

+ A request to https://example.com/super_secret_api may be problematic if that api is not + meant to be directly accessible from the attacker's machine. +

+ + + +

+ One way to remedy the problem is to validate the user input to only allow alphanumeric values: +

+ + +
+ + + +
  • OWASP: SSRF
  • + +
    +
    \ No newline at end of file diff --git a/javascript/ql/src/experimental/Security/CWE-918/SSRF.ql b/javascript/ql/src/experimental/Security/CWE-918/SSRF.ql new file mode 100644 index 000000000000..f13f5fae14e2 --- /dev/null +++ b/javascript/ql/src/experimental/Security/CWE-918/SSRF.ql @@ -0,0 +1,19 @@ +/** + * @id javascript/ssrf + * @kind path-problem + * @name Uncontrolled data used in network request + * @description Sending network requests with user-controlled data as part of the URL allows for request forgery attacks. + * @problem.severity error + * @precision medium + * @tags security + * external/cwe/cwe-918 + */ + +import javascript +import SSRF +import DataFlow::PathGraph + +from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink, DataFlow::Node request +where + cfg.hasFlowPath(source, sink) and request = sink.getNode().(RequestForgery::Sink).getARequest() +select sink, source, sink, "The URL of this request depends on a user-provided value" diff --git a/javascript/ql/src/experimental/Security/CWE-918/SSRF.qll b/javascript/ql/src/experimental/Security/CWE-918/SSRF.qll new file mode 100644 index 000000000000..cba9b1f51543 --- /dev/null +++ b/javascript/ql/src/experimental/Security/CWE-918/SSRF.qll @@ -0,0 +1,154 @@ +import javascript +import semmle.javascript.security.dataflow.RequestForgeryCustomizations +import semmle.javascript.security.dataflow.UrlConcatenation + +class Configuration extends TaintTracking::Configuration { + Configuration() { this = "SSRF" } + + override predicate isSource(DataFlow::Node source) { source instanceof RequestForgery::Source } + + override predicate isSink(DataFlow::Node sink) { sink instanceof RequestForgery::Sink } + + override predicate isSanitizer(DataFlow::Node node) { + super.isSanitizer(node) or + node instanceof RequestForgery::Sanitizer + } + + private predicate hasSanitizingSubstring(DataFlow::Node nd) { + nd.getStringValue().regexpMatch(".*[?#].*") + or + hasSanitizingSubstring(StringConcatenation::getAnOperand(nd)) + or + hasSanitizingSubstring(nd.getAPredecessor()) + } + + private predicate strictSanitizingPrefixEdge(DataFlow::Node source, DataFlow::Node sink) { + exists(DataFlow::Node operator, int n | + StringConcatenation::taintStep(source, sink, operator, n) and + hasSanitizingSubstring(StringConcatenation::getOperand(operator, [0 .. n - 1])) + ) + } + + override predicate isSanitizerEdge(DataFlow::Node source, DataFlow::Node sink) { + strictSanitizingPrefixEdge(source, sink) + } + + override predicate isSanitizerGuard(TaintTracking::SanitizerGuardNode nd) { + nd instanceof IntegerCheck or + nd instanceof ValidatorCheck or + nd instanceof TernaryOperatorSanitizerGuard + } +} + +/** + * This sanitizers models the next example: + * let valid = req.params.id ? Number.isInteger(req.params.id) : false + * if (valid) { sink(req.params.id) } + * + * This sanitizer models this way of using ternary operators, + * when the sanitizer guard is used as any of the branches + * instead of being used as the condition. + * + * This sanitizer sanitize the corresponding if statement branch. + */ +class TernaryOperatorSanitizer extends RequestForgery::Sanitizer { + TernaryOperatorSanitizer() { + exists( + TaintTracking::SanitizerGuardNode guard, IfStmt ifStmt, DataFlow::Node taintedInput, + boolean outcome, Stmt r, DataFlow::Node falseNode + | + ifStmt.getCondition().flow().getAPredecessor+() = guard and + ifStmt.getCondition().flow().getAPredecessor+() = falseNode and + falseNode.asExpr().(BooleanLiteral).mayHaveBooleanValue(false) and + not ifStmt.getCondition() instanceof LogicalBinaryExpr and + guard.sanitizes(outcome, taintedInput.asExpr()) and + ( + outcome = true and r = ifStmt.getThen() and not ifStmt.getCondition() instanceof LogNotExpr + or + outcome = false and r = ifStmt.getElse() and not ifStmt.getCondition() instanceof LogNotExpr + or + outcome = false and r = ifStmt.getThen() and ifStmt.getCondition() instanceof LogNotExpr + or + outcome = true and r = ifStmt.getElse() and ifStmt.getCondition() instanceof LogNotExpr + ) and + r.getFirstControlFlowNode() + .getBasicBlock() + .(ReachableBasicBlock) + .dominates(this.getBasicBlock()) + ) + } +} + +/** + * This sanitizer guard is another way of modeling the example from above + * In this case: + * let valid = req.params.id ? Number.isInteger(req.params.id) : false + * if (!valid) { return } + * sink(req.params.id) + * + * The previous sanitizer is not enough, + * because we are sanitizing the entire if statement branch + * but we need to sanitize the use of this variable from now on. + * + * Thats why we model this sanitizer guard which says that + * the result of the ternary operator execution is a sanitizer guard. + */ +class TernaryOperatorSanitizerGuard extends TaintTracking::SanitizerGuardNode { + TaintTracking::SanitizerGuardNode originalGuard; + + TernaryOperatorSanitizerGuard() { + this.getAPredecessor+().asExpr().(BooleanLiteral).mayHaveBooleanValue(false) and + this.getAPredecessor+() = originalGuard and + not this.asExpr() instanceof LogicalBinaryExpr + } + + override predicate sanitizes(boolean outcome, Expr e) { + not this.asExpr() instanceof LogNotExpr and + originalGuard.sanitizes(outcome, e) + or + exists(boolean originalOutcome | + this.asExpr() instanceof LogNotExpr and + originalGuard.sanitizes(originalOutcome, e) and + ( + originalOutcome = true and outcome = false + or + originalOutcome = false and outcome = true + ) + ) + } +} + +/** + * Number.isInteger is a sanitizer guard because a number can't be used to exploit a SSRF. + */ +class IntegerCheck extends TaintTracking::SanitizerGuardNode, DataFlow::CallNode { + IntegerCheck() { this = DataFlow::globalVarRef("Number").getAMemberCall("isInteger") } + + override predicate sanitizes(boolean outcome, Expr e) { + outcome = true and + e = getArgument(0).asExpr() + } +} + +/** + * ValidatorCheck identifies if exists a call to validator's library methods. + * validator is a library which has a variety of input-validation functions. We are interesed in + * checking that source is a number (any type of number) or an alphanumeric value. + */ +class ValidatorCheck extends TaintTracking::SanitizerGuardNode, DataFlow::CallNode { + ValidatorCheck() { + exists(DataFlow::SourceNode mod, string method | + mod = DataFlow::moduleImport("validator") and + this = mod.getAChainedMethodCall(method) and + method in [ + "isAlphanumeric", "isAlpha", "isDecimal", "isFloat", "isHexadecimal", "isHexColor", + "isInt", "isNumeric", "isOctal", "isUUID" + ] + ) + } + + override predicate sanitizes(boolean outcome, Expr e) { + outcome = true and + e = getArgument(0).asExpr() + } +} diff --git a/javascript/ql/src/experimental/Security/CWE-918/SSRFGood.js b/javascript/ql/src/experimental/Security/CWE-918/SSRFGood.js new file mode 100644 index 000000000000..10ab26e607b7 --- /dev/null +++ b/javascript/ql/src/experimental/Security/CWE-918/SSRFGood.js @@ -0,0 +1,20 @@ +const axios = require('axios'); +const validator = require('validator'); + +export const handler = async (req, res, next) => { + const { targetĀ } = req.body; + + if (!validator.isAlphanumeric(target)) { + return next(new Error('Bad request')); + } + + try { + // `target` is validated + const response = await axios.get('https://example.com/current_api/' + target); + + // process request response + use(response); + } catch (err) { + // process error + } +}; diff --git a/javascript/ql/test/experimental/Security/CWE-918/SSRF.expected b/javascript/ql/test/experimental/Security/CWE-918/SSRF.expected new file mode 100644 index 000000000000..a7bfb5d1f6c0 --- /dev/null +++ b/javascript/ql/test/experimental/Security/CWE-918/SSRF.expected @@ -0,0 +1,185 @@ +nodes +| check-domain.js:16:9:16:27 | url | +| check-domain.js:16:15:16:27 | req.query.url | +| check-domain.js:16:15:16:27 | req.query.url | +| check-domain.js:17:13:17:15 | url | +| check-domain.js:17:13:17:15 | url | +| check-domain.js:26:15:26:27 | req.query.url | +| check-domain.js:26:15:26:27 | req.query.url | +| check-domain.js:26:15:26:27 | req.query.url | +| check-middleware.js:9:13:9:43 | "test.c ... tainted | +| check-middleware.js:9:13:9:43 | "test.c ... tainted | +| check-middleware.js:9:27:9:43 | req.query.tainted | +| check-middleware.js:9:27:9:43 | req.query.tainted | +| check-path.js:19:13:19:43 | 'test.c ... tainted | +| check-path.js:19:13:19:43 | 'test.c ... tainted | +| check-path.js:19:27:19:43 | req.query.tainted | +| check-path.js:19:27:19:43 | req.query.tainted | +| check-path.js:22:13:22:63 | 'test.c ... ainted) | +| check-path.js:22:13:22:63 | 'test.c ... ainted) | +| check-path.js:22:27:22:63 | encodeU ... ainted) | +| check-path.js:22:46:22:62 | req.query.tainted | +| check-path.js:22:46:22:62 | req.query.tainted | +| check-path.js:23:13:23:45 | `/addre ... inted}` | +| check-path.js:23:13:23:45 | `/addre ... inted}` | +| check-path.js:23:27:23:43 | req.query.tainted | +| check-path.js:23:27:23:43 | req.query.tainted | +| check-path.js:24:13:24:65 | `/addre ... nted)}` | +| check-path.js:24:13:24:65 | `/addre ... nted)}` | +| check-path.js:24:27:24:63 | encodeU ... ainted) | +| check-path.js:24:46:24:62 | req.query.tainted | +| check-path.js:24:46:24:62 | req.query.tainted | +| check-path.js:33:15:33:45 | 'test.c ... tainted | +| check-path.js:33:15:33:45 | 'test.c ... tainted | +| check-path.js:33:29:33:45 | req.query.tainted | +| check-path.js:33:29:33:45 | req.query.tainted | +| check-path.js:37:15:37:45 | 'test.c ... tainted | +| check-path.js:37:15:37:45 | 'test.c ... tainted | +| check-path.js:37:29:37:45 | req.query.tainted | +| check-path.js:37:29:37:45 | req.query.tainted | +| check-path.js:45:13:45:44 | `${base ... inted}` | +| check-path.js:45:13:45:44 | `${base ... inted}` | +| check-path.js:45:26:45:42 | req.query.tainted | +| check-path.js:45:26:45:42 | req.query.tainted | +| check-regex.js:24:15:24:42 | baseURL ... tainted | +| check-regex.js:24:15:24:42 | baseURL ... tainted | +| check-regex.js:24:25:24:42 | req.params.tainted | +| check-regex.js:24:25:24:42 | req.params.tainted | +| check-regex.js:31:15:31:45 | "test.c ... tainted | +| check-regex.js:31:15:31:45 | "test.c ... tainted | +| check-regex.js:31:29:31:45 | req.query.tainted | +| check-regex.js:31:29:31:45 | req.query.tainted | +| check-regex.js:34:15:34:42 | baseURL ... tainted | +| check-regex.js:34:15:34:42 | baseURL ... tainted | +| check-regex.js:34:25:34:42 | req.params.tainted | +| check-regex.js:34:25:34:42 | req.params.tainted | +| check-regex.js:41:13:41:43 | "test.c ... tainted | +| check-regex.js:41:13:41:43 | "test.c ... tainted | +| check-regex.js:41:27:41:43 | req.query.tainted | +| check-regex.js:41:27:41:43 | req.query.tainted | +| check-validator.js:15:15:15:45 | "test.c ... tainted | +| check-validator.js:15:15:15:45 | "test.c ... tainted | +| check-validator.js:15:29:15:45 | req.query.tainted | +| check-validator.js:15:29:15:45 | req.query.tainted | +| check-validator.js:27:15:27:45 | "test.c ... tainted | +| check-validator.js:27:15:27:45 | "test.c ... tainted | +| check-validator.js:27:29:27:45 | req.query.tainted | +| check-validator.js:27:29:27:45 | req.query.tainted | +| check-validator.js:50:15:50:45 | "test.c ... tainted | +| check-validator.js:50:15:50:45 | "test.c ... tainted | +| check-validator.js:50:29:50:45 | req.query.tainted | +| check-validator.js:50:29:50:45 | req.query.tainted | +| check-validator.js:54:9:54:37 | numberURL | +| check-validator.js:54:21:54:37 | req.query.tainted | +| check-validator.js:54:21:54:37 | req.query.tainted | +| check-validator.js:59:15:59:45 | "test.c ... tainted | +| check-validator.js:59:15:59:45 | "test.c ... tainted | +| check-validator.js:59:29:59:45 | req.query.tainted | +| check-validator.js:59:29:59:45 | req.query.tainted | +| check-validator.js:62:15:62:37 | "test.c ... mberURL | +| check-validator.js:62:15:62:37 | "test.c ... mberURL | +| check-validator.js:62:29:62:37 | numberURL | +| check-validator.js:68:15:68:45 | "test.c ... tainted | +| check-validator.js:68:15:68:45 | "test.c ... tainted | +| check-validator.js:68:29:68:45 | req.query.tainted | +| check-validator.js:68:29:68:45 | req.query.tainted | +edges +| check-domain.js:16:9:16:27 | url | check-domain.js:17:13:17:15 | url | +| check-domain.js:16:9:16:27 | url | check-domain.js:17:13:17:15 | url | +| check-domain.js:16:15:16:27 | req.query.url | check-domain.js:16:9:16:27 | url | +| check-domain.js:16:15:16:27 | req.query.url | check-domain.js:16:9:16:27 | url | +| check-domain.js:26:15:26:27 | req.query.url | check-domain.js:26:15:26:27 | req.query.url | +| check-middleware.js:9:27:9:43 | req.query.tainted | check-middleware.js:9:13:9:43 | "test.c ... tainted | +| check-middleware.js:9:27:9:43 | req.query.tainted | check-middleware.js:9:13:9:43 | "test.c ... tainted | +| check-middleware.js:9:27:9:43 | req.query.tainted | check-middleware.js:9:13:9:43 | "test.c ... tainted | +| check-middleware.js:9:27:9:43 | req.query.tainted | check-middleware.js:9:13:9:43 | "test.c ... tainted | +| check-path.js:19:27:19:43 | req.query.tainted | check-path.js:19:13:19:43 | 'test.c ... tainted | +| check-path.js:19:27:19:43 | req.query.tainted | check-path.js:19:13:19:43 | 'test.c ... tainted | +| check-path.js:19:27:19:43 | req.query.tainted | check-path.js:19:13:19:43 | 'test.c ... tainted | +| check-path.js:19:27:19:43 | req.query.tainted | check-path.js:19:13:19:43 | 'test.c ... tainted | +| check-path.js:22:27:22:63 | encodeU ... ainted) | check-path.js:22:13:22:63 | 'test.c ... ainted) | +| check-path.js:22:27:22:63 | encodeU ... ainted) | check-path.js:22:13:22:63 | 'test.c ... ainted) | +| check-path.js:22:46:22:62 | req.query.tainted | check-path.js:22:27:22:63 | encodeU ... ainted) | +| check-path.js:22:46:22:62 | req.query.tainted | check-path.js:22:27:22:63 | encodeU ... ainted) | +| check-path.js:23:27:23:43 | req.query.tainted | check-path.js:23:13:23:45 | `/addre ... inted}` | +| check-path.js:23:27:23:43 | req.query.tainted | check-path.js:23:13:23:45 | `/addre ... inted}` | +| check-path.js:23:27:23:43 | req.query.tainted | check-path.js:23:13:23:45 | `/addre ... inted}` | +| check-path.js:23:27:23:43 | req.query.tainted | check-path.js:23:13:23:45 | `/addre ... inted}` | +| check-path.js:24:27:24:63 | encodeU ... ainted) | check-path.js:24:13:24:65 | `/addre ... nted)}` | +| check-path.js:24:27:24:63 | encodeU ... ainted) | check-path.js:24:13:24:65 | `/addre ... nted)}` | +| check-path.js:24:46:24:62 | req.query.tainted | check-path.js:24:27:24:63 | encodeU ... ainted) | +| check-path.js:24:46:24:62 | req.query.tainted | check-path.js:24:27:24:63 | encodeU ... ainted) | +| check-path.js:33:29:33:45 | req.query.tainted | check-path.js:33:15:33:45 | 'test.c ... tainted | +| check-path.js:33:29:33:45 | req.query.tainted | check-path.js:33:15:33:45 | 'test.c ... tainted | +| check-path.js:33:29:33:45 | req.query.tainted | check-path.js:33:15:33:45 | 'test.c ... tainted | +| check-path.js:33:29:33:45 | req.query.tainted | check-path.js:33:15:33:45 | 'test.c ... tainted | +| check-path.js:37:29:37:45 | req.query.tainted | check-path.js:37:15:37:45 | 'test.c ... tainted | +| check-path.js:37:29:37:45 | req.query.tainted | check-path.js:37:15:37:45 | 'test.c ... tainted | +| check-path.js:37:29:37:45 | req.query.tainted | check-path.js:37:15:37:45 | 'test.c ... tainted | +| check-path.js:37:29:37:45 | req.query.tainted | check-path.js:37:15:37:45 | 'test.c ... tainted | +| check-path.js:45:26:45:42 | req.query.tainted | check-path.js:45:13:45:44 | `${base ... inted}` | +| check-path.js:45:26:45:42 | req.query.tainted | check-path.js:45:13:45:44 | `${base ... inted}` | +| check-path.js:45:26:45:42 | req.query.tainted | check-path.js:45:13:45:44 | `${base ... inted}` | +| check-path.js:45:26:45:42 | req.query.tainted | check-path.js:45:13:45:44 | `${base ... inted}` | +| check-regex.js:24:25:24:42 | req.params.tainted | check-regex.js:24:15:24:42 | baseURL ... tainted | +| check-regex.js:24:25:24:42 | req.params.tainted | check-regex.js:24:15:24:42 | baseURL ... tainted | +| check-regex.js:24:25:24:42 | req.params.tainted | check-regex.js:24:15:24:42 | baseURL ... tainted | +| check-regex.js:24:25:24:42 | req.params.tainted | check-regex.js:24:15:24:42 | baseURL ... tainted | +| check-regex.js:31:29:31:45 | req.query.tainted | check-regex.js:31:15:31:45 | "test.c ... tainted | +| check-regex.js:31:29:31:45 | req.query.tainted | check-regex.js:31:15:31:45 | "test.c ... tainted | +| check-regex.js:31:29:31:45 | req.query.tainted | check-regex.js:31:15:31:45 | "test.c ... tainted | +| check-regex.js:31:29:31:45 | req.query.tainted | check-regex.js:31:15:31:45 | "test.c ... tainted | +| check-regex.js:34:25:34:42 | req.params.tainted | check-regex.js:34:15:34:42 | baseURL ... tainted | +| check-regex.js:34:25:34:42 | req.params.tainted | check-regex.js:34:15:34:42 | baseURL ... tainted | +| check-regex.js:34:25:34:42 | req.params.tainted | check-regex.js:34:15:34:42 | baseURL ... tainted | +| check-regex.js:34:25:34:42 | req.params.tainted | check-regex.js:34:15:34:42 | baseURL ... tainted | +| check-regex.js:41:27:41:43 | req.query.tainted | check-regex.js:41:13:41:43 | "test.c ... tainted | +| check-regex.js:41:27:41:43 | req.query.tainted | check-regex.js:41:13:41:43 | "test.c ... tainted | +| check-regex.js:41:27:41:43 | req.query.tainted | check-regex.js:41:13:41:43 | "test.c ... tainted | +| check-regex.js:41:27:41:43 | req.query.tainted | check-regex.js:41:13:41:43 | "test.c ... tainted | +| check-validator.js:15:29:15:45 | req.query.tainted | check-validator.js:15:15:15:45 | "test.c ... tainted | +| check-validator.js:15:29:15:45 | req.query.tainted | check-validator.js:15:15:15:45 | "test.c ... tainted | +| check-validator.js:15:29:15:45 | req.query.tainted | check-validator.js:15:15:15:45 | "test.c ... tainted | +| check-validator.js:15:29:15:45 | req.query.tainted | check-validator.js:15:15:15:45 | "test.c ... tainted | +| check-validator.js:27:29:27:45 | req.query.tainted | check-validator.js:27:15:27:45 | "test.c ... tainted | +| check-validator.js:27:29:27:45 | req.query.tainted | check-validator.js:27:15:27:45 | "test.c ... tainted | +| check-validator.js:27:29:27:45 | req.query.tainted | check-validator.js:27:15:27:45 | "test.c ... tainted | +| check-validator.js:27:29:27:45 | req.query.tainted | check-validator.js:27:15:27:45 | "test.c ... tainted | +| check-validator.js:50:29:50:45 | req.query.tainted | check-validator.js:50:15:50:45 | "test.c ... tainted | +| check-validator.js:50:29:50:45 | req.query.tainted | check-validator.js:50:15:50:45 | "test.c ... tainted | +| check-validator.js:50:29:50:45 | req.query.tainted | check-validator.js:50:15:50:45 | "test.c ... tainted | +| check-validator.js:50:29:50:45 | req.query.tainted | check-validator.js:50:15:50:45 | "test.c ... tainted | +| check-validator.js:54:9:54:37 | numberURL | check-validator.js:62:29:62:37 | numberURL | +| check-validator.js:54:21:54:37 | req.query.tainted | check-validator.js:54:9:54:37 | numberURL | +| check-validator.js:54:21:54:37 | req.query.tainted | check-validator.js:54:9:54:37 | numberURL | +| check-validator.js:59:29:59:45 | req.query.tainted | check-validator.js:59:15:59:45 | "test.c ... tainted | +| check-validator.js:59:29:59:45 | req.query.tainted | check-validator.js:59:15:59:45 | "test.c ... tainted | +| check-validator.js:59:29:59:45 | req.query.tainted | check-validator.js:59:15:59:45 | "test.c ... tainted | +| check-validator.js:59:29:59:45 | req.query.tainted | check-validator.js:59:15:59:45 | "test.c ... tainted | +| check-validator.js:62:29:62:37 | numberURL | check-validator.js:62:15:62:37 | "test.c ... mberURL | +| check-validator.js:62:29:62:37 | numberURL | check-validator.js:62:15:62:37 | "test.c ... mberURL | +| check-validator.js:68:29:68:45 | req.query.tainted | check-validator.js:68:15:68:45 | "test.c ... tainted | +| check-validator.js:68:29:68:45 | req.query.tainted | check-validator.js:68:15:68:45 | "test.c ... tainted | +| check-validator.js:68:29:68:45 | req.query.tainted | check-validator.js:68:15:68:45 | "test.c ... tainted | +| check-validator.js:68:29:68:45 | req.query.tainted | check-validator.js:68:15:68:45 | "test.c ... tainted | +#select +| check-domain.js:17:13:17:15 | url | check-domain.js:16:15:16:27 | req.query.url | check-domain.js:17:13:17:15 | url | The URL of this request depends on a user-provided value | +| check-domain.js:26:15:26:27 | req.query.url | check-domain.js:26:15:26:27 | req.query.url | check-domain.js:26:15:26:27 | req.query.url | The URL of this request depends on a user-provided value | +| check-middleware.js:9:13:9:43 | "test.c ... tainted | check-middleware.js:9:27:9:43 | req.query.tainted | check-middleware.js:9:13:9:43 | "test.c ... tainted | The URL of this request depends on a user-provided value | +| check-path.js:19:13:19:43 | 'test.c ... tainted | check-path.js:19:27:19:43 | req.query.tainted | check-path.js:19:13:19:43 | 'test.c ... tainted | The URL of this request depends on a user-provided value | +| check-path.js:22:13:22:63 | 'test.c ... ainted) | check-path.js:22:46:22:62 | req.query.tainted | check-path.js:22:13:22:63 | 'test.c ... ainted) | The URL of this request depends on a user-provided value | +| check-path.js:23:13:23:45 | `/addre ... inted}` | check-path.js:23:27:23:43 | req.query.tainted | check-path.js:23:13:23:45 | `/addre ... inted}` | The URL of this request depends on a user-provided value | +| check-path.js:24:13:24:65 | `/addre ... nted)}` | check-path.js:24:46:24:62 | req.query.tainted | check-path.js:24:13:24:65 | `/addre ... nted)}` | The URL of this request depends on a user-provided value | +| check-path.js:33:15:33:45 | 'test.c ... tainted | check-path.js:33:29:33:45 | req.query.tainted | check-path.js:33:15:33:45 | 'test.c ... tainted | The URL of this request depends on a user-provided value | +| check-path.js:37:15:37:45 | 'test.c ... tainted | check-path.js:37:29:37:45 | req.query.tainted | check-path.js:37:15:37:45 | 'test.c ... tainted | The URL of this request depends on a user-provided value | +| check-path.js:45:13:45:44 | `${base ... inted}` | check-path.js:45:26:45:42 | req.query.tainted | check-path.js:45:13:45:44 | `${base ... inted}` | The URL of this request depends on a user-provided value | +| check-regex.js:24:15:24:42 | baseURL ... tainted | check-regex.js:24:25:24:42 | req.params.tainted | check-regex.js:24:15:24:42 | baseURL ... tainted | The URL of this request depends on a user-provided value | +| check-regex.js:31:15:31:45 | "test.c ... tainted | check-regex.js:31:29:31:45 | req.query.tainted | check-regex.js:31:15:31:45 | "test.c ... tainted | The URL of this request depends on a user-provided value | +| check-regex.js:34:15:34:42 | baseURL ... tainted | check-regex.js:34:25:34:42 | req.params.tainted | check-regex.js:34:15:34:42 | baseURL ... tainted | The URL of this request depends on a user-provided value | +| check-regex.js:41:13:41:43 | "test.c ... tainted | check-regex.js:41:27:41:43 | req.query.tainted | check-regex.js:41:13:41:43 | "test.c ... tainted | The URL of this request depends on a user-provided value | +| check-validator.js:15:15:15:45 | "test.c ... tainted | check-validator.js:15:29:15:45 | req.query.tainted | check-validator.js:15:15:15:45 | "test.c ... tainted | The URL of this request depends on a user-provided value | +| check-validator.js:27:15:27:45 | "test.c ... tainted | check-validator.js:27:29:27:45 | req.query.tainted | check-validator.js:27:15:27:45 | "test.c ... tainted | The URL of this request depends on a user-provided value | +| check-validator.js:50:15:50:45 | "test.c ... tainted | check-validator.js:50:29:50:45 | req.query.tainted | check-validator.js:50:15:50:45 | "test.c ... tainted | The URL of this request depends on a user-provided value | +| check-validator.js:59:15:59:45 | "test.c ... tainted | check-validator.js:59:29:59:45 | req.query.tainted | check-validator.js:59:15:59:45 | "test.c ... tainted | The URL of this request depends on a user-provided value | +| check-validator.js:62:15:62:37 | "test.c ... mberURL | check-validator.js:54:21:54:37 | req.query.tainted | check-validator.js:62:15:62:37 | "test.c ... mberURL | The URL of this request depends on a user-provided value | +| check-validator.js:68:15:68:45 | "test.c ... tainted | check-validator.js:68:29:68:45 | req.query.tainted | check-validator.js:68:15:68:45 | "test.c ... tainted | The URL of this request depends on a user-provided value | diff --git a/javascript/ql/test/experimental/Security/CWE-918/SSRF.qlref b/javascript/ql/test/experimental/Security/CWE-918/SSRF.qlref new file mode 100644 index 000000000000..05a9c8145e60 --- /dev/null +++ b/javascript/ql/test/experimental/Security/CWE-918/SSRF.qlref @@ -0,0 +1 @@ +./experimental/Security/CWE-918/SSRF.ql \ No newline at end of file diff --git a/javascript/ql/test/experimental/Security/CWE-918/check-domain.js b/javascript/ql/test/experimental/Security/CWE-918/check-domain.js new file mode 100644 index 000000000000..0821140ab5fe --- /dev/null +++ b/javascript/ql/test/experimental/Security/CWE-918/check-domain.js @@ -0,0 +1,34 @@ +// native modules +const url = require('url'); + +// dependencies +const axios = require('axios'); +const express = require('express'); + +// constants +const VALID_DOMAINS = ['example.com', 'example-2.com']; + +// start +const app = express(); + +app.get('/check-with-axios', req => { + // without validation + const url = req.query.url; + axios.get(url); //SSRF + + // validating domain only + const decodedURI = decodeURIComponent(req.query.url); + const { hostname } = url.parse(decodedURI); + + const { hostname } = url.parse(decodedURI); + + if (isValidDomain(hostname, validDomains)) { + axios.get(req.query.url); //SSRF + } +}); + +const isValidDomain = (hostname, validDomains) => ( + validDomains.some(domain => ( + hostname === domain || hostname.endsWith(`.${domain}`)) + ) +); \ No newline at end of file diff --git a/javascript/ql/test/experimental/Security/CWE-918/check-middleware.js b/javascript/ql/test/experimental/Security/CWE-918/check-middleware.js new file mode 100644 index 000000000000..2a1e6d541665 --- /dev/null +++ b/javascript/ql/test/experimental/Security/CWE-918/check-middleware.js @@ -0,0 +1,19 @@ +// dependencies +const axios = require('axios'); +const express = require('express'); + +// start +const app = express(); + +app.get('/check-with-axios', validationMiddleware, req => { + axios.get("test.com/" + req.query.tainted); // OK is sanitized by the middleware - False Positive +}); + + +const validationMiddleware = (req, res, next) => { + if (!Number.isInteger(req.query.tainted)) { + return res.sendStatus(400); + } + + next(); +} diff --git a/javascript/ql/test/experimental/Security/CWE-918/check-path.js b/javascript/ql/test/experimental/Security/CWE-918/check-path.js new file mode 100644 index 000000000000..6ed1ab1fb7b7 --- /dev/null +++ b/javascript/ql/test/experimental/Security/CWE-918/check-path.js @@ -0,0 +1,57 @@ +// native modules +const path = require('path'); +const url = require('url'); + +// dependencies +const axios = require('axios'); +const express = require('express'); + +// constants +const VALID_PATHS = ['/api/users/me', '/help', '/system/health']; + +// start +const app = express(); + +app.get('/check-with-axios', req => { + const hardcoded = 'hardcodeado'; + + axios.get('test.com/' + hardcoded); // OK + axios.get('test.com/' + req.query.tainted); // SSRF + axios.get('test.com/' + Number(req.query.tainted)); // OK + axios.get('test.com/' + req.user.id); // OK + axios.get('test.com/' + encodeURIComponent(req.query.tainted)); // SSRF + axios.get(`/addresses/${req.query.tainted}`); // SSRF + axios.get(`/addresses/${encodeURIComponent(req.query.tainted)}`); // SSRF + + if (Number.isInteger(req.query.tainted)) { + axios.get('test.com/' + req.query.tainted); // OK + } + + if (isValidInput(req.query.tainted)){ + axios.get('test.com/' + req.query.tainted); // OK + } else { + axios.get('test.com/' + req.query.tainted); // SSRF + } + + if (doesntCheckAnything(req.query.tainted)) { + axios.get('test.com/' + req.query.tainted); // SSRF + } + + if (isValidPath(req.query.tainted, VALID_PATHS)) { + axios.get('test.com/' + req.query.tainted) // OK + } + + let baseURL = require('config').base + axios.get(`${baseURL}${req.query.tainted}`); // SSRF + + if(!isValidInput(req.query.tainted)) { + return; + } + axios.get("test.com/" + req.query.tainted); // OK +}); + +const isValidPath = (path, validPaths) => validPaths.includes(path); + +const isValidInput = (path) => Number.isInteger(path); + +const doesntCheckAnything = (path) => true; diff --git a/javascript/ql/test/experimental/Security/CWE-918/check-regex.js b/javascript/ql/test/experimental/Security/CWE-918/check-regex.js new file mode 100644 index 000000000000..a05c1ae7ddfa --- /dev/null +++ b/javascript/ql/test/experimental/Security/CWE-918/check-regex.js @@ -0,0 +1,46 @@ +// dependencies +const axios = require('axios'); +const express = require('express'); + +// start +const app = express(); + +app.get('/check-with-axios', req => { + if (req.query.tainted.match(/^[0-9a-z]+$/)) { // letters and numbers + axios.get("test.com/" + req.query.tainted); // OK + } + if (req.query.tainted.match(/^[0-9a-z\-_]+$/)) { // letters, numbers, - and _ + axios.get("test.com/" + req.query.tainted); // OK + } + if (req.query.tainted.match(/^.*$/)) { // anything + axios.get("test.com/" + req.query.tainted); // SSRF - False Negative + } + + const baseURL = "test.com/" + if (isValidPath(req.params.tainted) ) { + axios.get(baseURL + req.params.tainted); // OK + } + if (!isValidPath(req.params.tainted) ) { + axios.get(baseURL + req.params.tainted); // SSRF + } else { + axios.get(baseURL + req.params.tainted); // OK + } + + // Blacklists are not safe + if (!req.query.tainted.match(/^[/\.%]+$/)) { + axios.get("test.com/" + req.query.tainted); // SSRF + } + if (!isInBlacklist(req.params.tainted) ) { + axios.get(baseURL + req.params.tainted); // SSRF + } + + if (!isValidPath(req.params.tainted)) { + return; + } + + axios.get("test.com/" + req.query.tainted); // OK - False Positive +}); + +const isValidPath = path => path.match(/^[0-9a-z]+$/); + +const isInBlackList = path => path.match(/^[/\.%]+$/); diff --git a/javascript/ql/test/experimental/Security/CWE-918/check-validator.js b/javascript/ql/test/experimental/Security/CWE-918/check-validator.js new file mode 100644 index 000000000000..dfe3314b07b3 --- /dev/null +++ b/javascript/ql/test/experimental/Security/CWE-918/check-validator.js @@ -0,0 +1,98 @@ +// dependencies +const axios = require('axios'); +const express = require('express'); +const validator = require('validator'); + +// start +const app = express(); + +app.get("/check-with-axios", req => { + // alphanumeric + if (validator.isAlphanumeric(req.query.tainted)) { + axios.get("test.com/" + req.query.tainted); // OK + } + if (isAlphanumeric(req.query.tainted)) { + axios.get("test.com/" + req.query.tainted); // SSRF + } + if (validAlphanumeric(req.query.tainted)) { + axios.get("test.com/" + req.query.tainted); // OK + } + if (validAlpha(req.query.tainted)) { + axios.get("test.com/" + req.query.tainted); // OK + } + if (validNumber(req.query.tainted)) { + axios.get("test.com/" + req.query.tainted); // OK + } + if (wrongValidation(req.query.tainted)) { + axios.get("test.com/" + req.query.tainted); // SSRF + } + + // numbers + if (validHexadecimal(req.query.tainted)) { + axios.get("test.com/" + req.query.tainted); // OK + } + if (validHexaColor(req.query.tainted)) { + axios.get("test.com/" + req.query.tainted); // OK + } + if (validDecimal(req.query.tainted)) { + axios.get("test.com/" + req.query.tainted); // OK + } + if (validFloat(req.query.tainted)) { + axios.get("test.com/" + req.query.tainted); // OK + } + if (validInt(req.query.tainted)) { + axios.get("test.com/" + req.query.tainted); // OK + } + if (validOctal(req.query.tainted)) { + axios.get("test.com/" + req.query.tainted); // OK + } + if (validHexa(req.query.tainted)) { + axios.get("test.com/" + req.query.tainted); // OK. False Positive + } + + // with simple assignation + const numberURL = req.query.tainted; + if (validNumber(numberURL)) { + axios.get("test.com/" + numberURL); // OK + } + if (validNumber(numberURL)) { + axios.get("test.com/" + req.query.tainted); // OK. False Positive + } + if (validNumber(req.query.tainted)) { + axios.get("test.com/" + numberURL); // OK. False Positive + } + + if (validHexadecimal(req.query.tainted) || validHexaColor(req.query.tainted) || + validDecimal(req.query.tainted) || validFloat(req.query.tainted) || validInt(req.query.tainted) || + validNumber(req.query.tainted) || validOctal(req.query.tainted)) { + axios.get("test.com/" + req.query.tainted); // OK. False Positive + } +}); + +// safe validators +const validAlphanumeric = url => validator.isAlphanumeric(url); + +const validAlpha = url => validator.isAlpha(url); + +const validDecimal = url => validator.isDecimal(url); + +const validFloat = url => validator.isFloat(url); + +const validInt = url => validator.isInt(url); + +const validNumber = url => validator.isNumeric(url); + +const validOctal = url => validator.isOctal(url); + +const validHexa = url => validator.isHexadecimal(url) || validator.isHexColor(url); + +const validHexadecimal = url => validator.isHexadecimal(url); + +const validHexaColor = url => validator.isHexColor(url); + +const validUUID = url => validator.isUUID(url); + +// unsafe validators +const wrongValidation = url => validator.isByteLength(url, {min:4,max:8}); + +const isAlphanumeric = url => true; diff --git a/javascript/ql/test/experimental/Security/CWE-918/ternary-operator.js b/javascript/ql/test/experimental/Security/CWE-918/ternary-operator.js new file mode 100644 index 000000000000..26e2607a80f2 --- /dev/null +++ b/javascript/ql/test/experimental/Security/CWE-918/ternary-operator.js @@ -0,0 +1,182 @@ +const express = require('express'); +const app = express(); + +app.use(express.json()); + +app.get('/direct-ternary-operator', function (req, res) { + let taintedURL = req.params.url + + let v = req.params.url ? req.params.url == "someURL" : false + if (v) { + req_frontend_restclient.get(req.params.url) // OK + } + + let v1 = taintedURL ? taintedURL == "someURL" : false + if (v1) { + req_frontend_restclient.get(taintedURL) // OK + } + + let v2 = taintedURL ? valid(taintedURL) : false + if (v2) { + req_frontend_restclient.get(taintedURL) // OK + } + + let v3 = req.params.url ? valid(req.params.url) : false + if (v3) { + req_frontend_restclient.get(req.params.url) // OK + } + + let v4 = req.params.url == undefined ? false : valid(req.params.url) + if (v4) { + req_frontend_restclient.get(req.params.url) // OK + } + + let v5 = req.params.url == undefined ? true : valid(req.params.url) + if (v5) { + req_frontend_restclient.get(req.params.url) // SSRF + } + + let v6 = req.params.url ? valid(req.params.url) : true + if (v6) { + req_frontend_restclient.get(req.params.url) // SSRF + } + + let f = false + let v7 = req.params.url ? valid(req.params.url) : true + if (v7) { + req_frontend_restclient.get(req.params.url) // SSRF + } + + let v8 = req.params.url == undefined ? false : valid(req.params.url) + if (!v8) { + return + } + req_frontend_restclient.get(req.params.url) // OK +}) + +app.get('/functions', function (req, res) { + let taintedURL = req.params.url + + if (valid2(taintedURL)) { + req_frontend_restclient.get(taintedURL) // OK + } + + if (!invalid(taintedURL)) { + req_frontend_restclient.get(taintedURL) // False positive + } + + if (valid2(req.params.url)){ + req_frontend_restclient.get(req.params.url) // OK + } + + if (!assertAlphanumeric(req.params.url)) { + return + } + req_frontend_restclient.get(req.params.url); // OK +}) + +app.get('/normal-use-of-ternary-operator', function (req, res) { + let taintedURL = req.params.url + + let url = valid(req.params.url) ? req.params.url : undefined + req_frontend_restclient.get(url) // OK + + let url = valid(taintedURL) ? taintedURL : undefined + req_frontend_restclient.get(url) // OK + + let url4 = req.params.url.match(/^[\w.-]+$/) ? req.params.url : undefined + req_frontend_restclient.get(url4) // OK +}) + +app.get('/throw-errors', function (req, res) { + req_frontend_restclient.get(valid3(req.params.url)) // False positive + + req_frontend_restclient.get(assertOther(req.params.url)); // False positive + + req_frontend_restclient.get(assertOther2(req.params.url)); // False positive +}); + +app.get('/bad-endpoint', function (req, res) { + req_frontend_restclient.get(req.params.url); // SSRF + + const valid = req.params.url ? req.params.url == "someURL" : false + if (!valid) { + throw new Error(`Invalid parameter: "${req.params.url}", must be alphanumeric`); + } + req_frontend_restclient.get(req.params.url); // OK +}) + + +app.get('/bad-endpoint-variable', function (req, res) { + let taintedURL = req.params.url + req_frontend_restclient.get(taintedURL); // SSRF + + const valid = taintedURL ? taintedURL == "someURL" : false + if (!valid) { + return + } + req_frontend_restclient.get(taintedURL); // False positive +}) + +app.get('/not-invalid', function (req, res) { + const invalidParam = req.params.url ? !Number.isInteger(req.params.url) : false + if (invalidParam) { + return + } + req_frontend_restclient.get(req.params.url); // False positive +}) + + +app.get('/bad-endpoint-2', function (req, res) { + other(req.params.url) +}) + +function other(taintedURL) { + req_frontend_restclient.get(taintedURL); // SSRF + + const valid = taintedURL ? taintedURL == "someURL" : false + if (!valid) { + return + } + req_frontend_restclient.get(taintedURL); // False positive +} + +function assertAlphanumeric(value) { + return value ? value.match(/^[\w.-]+$/) : false; +} + +function assertOther(value) { + const valid = value ? !!value.match(/^[\w.-]+$/) : false; + if (!valid) { + throw new Error(`Invalid parameter: "${value}", must be alphanumeric`); + } + return value; +} + +function assertOther2(value) { + const valid = value ? value.match(/^[\w.-]+$/) : false; + if (!valid) { + throw new Error(`Invalid parameter: "${value}", must be alphanumeric`); + } + return value; +} + +function invalid(value) { + return value ? !Number.isInteger(value) : true +} + +function valid(value) { + return value.match(/^[\w.-]+$/) +} + +function valid2(value) { + return value ? value == "someURL" : false +} + +function valid3(value) { + const valid = value ? value == "someURL" : false + if (!valid) { + throw new Error(`Invalid parameter: "${value}", must be alphanumeric`); + } + return value; +} \ No newline at end of file