mirror of
https://github.com/github/codeql.git
synced 2026-04-25 00:35:20 +02:00
Merge remote-tracking branch 'upstream/main' into 'rc/3.14'
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
* Added a new experimental query, `js/cors-misconfiguration`, which detects misconfigured CORS HTTP headers in the `cors` and `apollo` libraries.
|
||||
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
<p>
|
||||
Directly evaluating user input (for example, an HTTP request parameter) as code without properly
|
||||
sanitizing the input first allows an attacker arbitrary code execution. This can occur when user
|
||||
input is treated as JavaScript, or passed to a framework which interprets it as an expression to be
|
||||
evaluated. Examples include AngularJS expressions or JQuery selectors.
|
||||
</p>
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>
|
||||
Avoid including user input in any expression which may be dynamically evaluated. If user input must
|
||||
be included, use context-specific escaping before
|
||||
including it. It is important that the correct escaping is used for the type of evaluation that will
|
||||
occur.
|
||||
</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>
|
||||
The following example shows part of the page URL being evaluated as JavaScript code on the server. This allows an
|
||||
attacker to provide JavaScript within the URL and send it to server. client side attacks need victim users interaction
|
||||
like clicking on a attacker provided URL.
|
||||
</p>
|
||||
|
||||
<sample src="examples/CodeInjection.js" />
|
||||
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>
|
||||
OWASP:
|
||||
<a href="https://www.owasp.org/index.php/Code_Injection">Code Injection</a>.
|
||||
</li>
|
||||
<li>
|
||||
Wikipedia: <a href="https://en.wikipedia.org/wiki/Code_injection">Code Injection</a>.
|
||||
</li>
|
||||
<li>
|
||||
PortSwigger Research Blog:
|
||||
<a href="https://portswigger.net/research/server-side-template-injection">Server-Side Template Injection</a>.
|
||||
</li>
|
||||
</references>
|
||||
</qhelp>
|
||||
@@ -0,0 +1,6 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
<include src="CodeInjection.inc.qhelp" />
|
||||
</qhelp>
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* @name Code injection
|
||||
* @description Interpreting unsanitized user input as code allows a malicious user arbitrary
|
||||
* code execution.
|
||||
* @kind path-problem
|
||||
* @problem.severity error
|
||||
* @security-severity 9.3
|
||||
* @precision high
|
||||
* @id js/code-injection-dynamic-import
|
||||
* @tags security
|
||||
* external/cwe/cwe-094
|
||||
* external/cwe/cwe-095
|
||||
* external/cwe/cwe-079
|
||||
* external/cwe/cwe-116
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import DataFlow
|
||||
import DataFlow::PathGraph
|
||||
|
||||
abstract class Sanitizer extends DataFlow::Node { }
|
||||
|
||||
/** A non-first leaf in a string-concatenation. Seen as a sanitizer for dynamic import code injection. */
|
||||
class NonFirstStringConcatLeaf extends Sanitizer {
|
||||
NonFirstStringConcatLeaf() {
|
||||
exists(StringOps::ConcatenationRoot root |
|
||||
this = root.getALeaf() and
|
||||
not this = root.getFirstLeaf()
|
||||
)
|
||||
or
|
||||
exists(DataFlow::CallNode join |
|
||||
join = DataFlow::moduleMember("path", "join").getACall() and
|
||||
this = join.getArgument([1 .. join.getNumArgument() - 1])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The dynamic import expression input can be a `data:` URL which loads any module from that data
|
||||
*/
|
||||
class DynamicImport extends DataFlow::ExprNode {
|
||||
DynamicImport() { this = any(DynamicImportExpr e).getSource().flow() }
|
||||
}
|
||||
|
||||
/**
|
||||
* The dynamic import expression input can be a `data:` URL which loads any module from that data
|
||||
*/
|
||||
class WorkerThreads extends DataFlow::Node {
|
||||
WorkerThreads() {
|
||||
this = API::moduleImport("node:worker_threads").getMember("Worker").getParameter(0).asSink()
|
||||
}
|
||||
}
|
||||
|
||||
class UrlConstructorLabel extends FlowLabel {
|
||||
UrlConstructorLabel() { this = "UrlConstructorLabel" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for reasoning about code injection vulnerabilities.
|
||||
*/
|
||||
class Configuration extends TaintTracking::Configuration {
|
||||
Configuration() { this = "CodeInjection" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) { sink instanceof DynamicImport }
|
||||
|
||||
override predicate isSink(DataFlow::Node sink, FlowLabel label) {
|
||||
sink instanceof WorkerThreads and label instanceof UrlConstructorLabel
|
||||
}
|
||||
|
||||
override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer }
|
||||
|
||||
override predicate isAdditionalFlowStep(
|
||||
DataFlow::Node pred, DataFlow::Node succ, FlowLabel predlbl, FlowLabel succlbl
|
||||
) {
|
||||
exists(DataFlow::NewNode newUrl | succ = newUrl |
|
||||
newUrl = DataFlow::globalVarRef("URL").getAnInstantiation() and
|
||||
pred = newUrl.getArgument(0)
|
||||
) and
|
||||
predlbl instanceof StandardFlowLabel and
|
||||
succlbl instanceof UrlConstructorLabel
|
||||
}
|
||||
}
|
||||
|
||||
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
|
||||
where cfg.hasFlowPath(source, sink)
|
||||
select sink.getNode(), source, sink, "This command line depends on a $@.", source.getNode(),
|
||||
"user-provided value"
|
||||
@@ -0,0 +1,14 @@
|
||||
const { Worker } = require('node:worker_threads');
|
||||
var app = require('express')();
|
||||
|
||||
app.post('/path', async function (req, res) {
|
||||
const payload = req.query.queryParameter // like: payload = 'data:text/javascript,console.log("hello!");//'
|
||||
const payloadURL = new URL(payload)
|
||||
new Worker(payloadURL);
|
||||
});
|
||||
|
||||
app.post('/path2', async function (req, res) {
|
||||
const payload = req.query.queryParameter // like: payload = 'data:text/javascript,console.log("hello!");//'
|
||||
await import(payload)
|
||||
});
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
|
||||
<p>
|
||||
Controlling the value of arbitrary environment variables from user-controllable data is not safe.
|
||||
</p>
|
||||
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
|
||||
<p>
|
||||
Restrict this operation only to privileged users or only for some not important environment variables.
|
||||
</p>
|
||||
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
|
||||
<p>
|
||||
The following example allows unauthorized users to assign a value to any environment variable.
|
||||
</p>
|
||||
|
||||
<sample src="examples/Bad_Value_And_Key_Assignment.js" />
|
||||
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li> <a href="https://huntr.com/bounties/00ec6847-125b-43e9-9658-d3cace1751d6/">Admin account TakeOver in mintplex-labs/anything-llm</a></li>
|
||||
</references>
|
||||
|
||||
</qhelp>
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @name User controlled arbitrary environment variable injection
|
||||
* @description creating arbitrary environment variables from user controlled data is not secure
|
||||
* @kind path-problem
|
||||
* @id js/env-key-and-value-injection
|
||||
* @problem.severity error
|
||||
* @security-severity 7.5
|
||||
* @precision medium
|
||||
* @tags security
|
||||
* external/cwe/cwe-089
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import DataFlow::PathGraph
|
||||
|
||||
/** A taint tracking configuration for unsafe environment injection. */
|
||||
class Configuration extends TaintTracking::Configuration {
|
||||
Configuration() { this = "envInjection" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) {
|
||||
sink = keyOfEnv() or
|
||||
sink = valueOfEnv()
|
||||
}
|
||||
|
||||
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
|
||||
exists(DataFlow::InvokeNode ikn |
|
||||
ikn = DataFlow::globalVarRef("Object").getAMemberInvocation("keys")
|
||||
|
|
||||
pred = ikn.getArgument(0) and
|
||||
(
|
||||
succ = ikn.getAChainedMethodCall(["filter", "map"]) or
|
||||
succ = ikn or
|
||||
succ = ikn.getAChainedMethodCall("forEach").getABoundCallbackParameter(0, 0)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DataFlow::Node keyOfEnv() {
|
||||
result =
|
||||
NodeJSLib::process().getAPropertyRead("env").getAPropertyWrite().getPropertyNameExpr().flow()
|
||||
}
|
||||
|
||||
DataFlow::Node valueOfEnv() {
|
||||
result = API::moduleImport("process").getMember("env").getAMember().asSink()
|
||||
}
|
||||
|
||||
private predicate readToProcessEnv(DataFlow::Node envKey, DataFlow::Node envValue) {
|
||||
exists(DataFlow::PropWrite env |
|
||||
env = NodeJSLib::process().getAPropertyRead("env").getAPropertyWrite()
|
||||
|
|
||||
envKey = env.getPropertyNameExpr().flow() and
|
||||
envValue = env.getRhs()
|
||||
)
|
||||
}
|
||||
|
||||
from
|
||||
Configuration cfgForValue, Configuration cfgForKey, DataFlow::PathNode source,
|
||||
DataFlow::PathNode envKey, DataFlow::PathNode envValue
|
||||
where
|
||||
cfgForValue.hasFlowPath(source, envKey) and
|
||||
envKey.getNode() = keyOfEnv() and
|
||||
cfgForKey.hasFlowPath(source, envValue) and
|
||||
envValue.getNode() = valueOfEnv() and
|
||||
readToProcessEnv(envKey.getNode(), envValue.getNode())
|
||||
select envKey.getNode(), source, envKey, "arbitrary environment variable assignment from this $@.",
|
||||
source.getNode(), "user controllable source"
|
||||
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
|
||||
<p>
|
||||
Assigning Value to environment variables from user-controllable data is not safe.
|
||||
</p>
|
||||
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
|
||||
<p>
|
||||
Restrict this operation only to privileged users or only for some not important environment variables.
|
||||
</p>
|
||||
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
|
||||
<p>
|
||||
The following example allows unauthorized users to assign a value to a critical environment variable.
|
||||
</p>
|
||||
|
||||
<sample src="examples/Bad_Value_Assignment.js" />
|
||||
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li><a href="https://huntr.com/bounties/00ec6847-125b-43e9-9658-d3cace1751d6/">Admin account TakeOver in mintplex-labs/anything-llm</a></li>
|
||||
</references>
|
||||
|
||||
</qhelp>
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @name User controlled environment variable value injection
|
||||
* @description assigning important environment variables from user controlled data is not secure
|
||||
* @kind path-problem
|
||||
* @id js/env-value-injection
|
||||
* @problem.severity error
|
||||
* @security-severity 7.5
|
||||
* @precision medium
|
||||
* @tags security
|
||||
* external/cwe/cwe-089
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import DataFlow::PathGraph
|
||||
|
||||
/** A taint tracking configuration for unsafe environment injection. */
|
||||
class Configuration extends TaintTracking::Configuration {
|
||||
Configuration() { this = "envInjection" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) {
|
||||
sink = API::moduleImport("process").getMember("env").getAMember().asSink()
|
||||
}
|
||||
}
|
||||
|
||||
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
|
||||
where cfg.hasFlowPath(source, sink)
|
||||
select sink.getNode(), source, sink, "this environment variable assignment is $@.",
|
||||
source.getNode(), "user controllable"
|
||||
@@ -0,0 +1,8 @@
|
||||
const http = require('node:http');
|
||||
|
||||
http.createServer((req, res) => {
|
||||
const { EnvValue, EnvKey } = req.body;
|
||||
process.env[EnvKey] = EnvValue; // NOT OK
|
||||
|
||||
res.end('env has been injected!');
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
const http = require('node:http');
|
||||
|
||||
http.createServer((req, res) => {
|
||||
const { EnvValue } = req.body;
|
||||
process.env["A_Critical_Env"] = EnvValue; // NOT OK
|
||||
|
||||
res.end('env has been injected!');
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
const express = require('express')
|
||||
const app = express()
|
||||
const jwtJsonwebtoken = require('jsonwebtoken');
|
||||
const jwt_decode = require('jwt-decode');
|
||||
const jwt_simple = require('jwt-simple');
|
||||
const jose = require('jose')
|
||||
const port = 3000
|
||||
|
||||
function getSecret() {
|
||||
return "A Safe generated random key"
|
||||
}
|
||||
app.get('/jose', (req, res) => {
|
||||
const UserToken = req.headers.authorization;
|
||||
// BAD: no signature verification
|
||||
jose.decodeJwt(UserToken)
|
||||
})
|
||||
|
||||
app.get('/jwtDecode', (req, res) => {
|
||||
const UserToken = req.headers.authorization;
|
||||
// BAD: no signature verification
|
||||
jwt_decode(UserToken)
|
||||
})
|
||||
|
||||
app.get('/jwtSimple', (req, res) => {
|
||||
const UserToken = req.headers.authorization;
|
||||
// jwt.decode(token, key, noVerify, algorithm)
|
||||
// BAD: no signature verification
|
||||
jwt_simple.decode(UserToken, getSecret(), true);
|
||||
})
|
||||
|
||||
app.get('/jwtJsonwebtoken', (req, res) => {
|
||||
const UserToken = req.headers.authorization;
|
||||
// BAD: no signature verification
|
||||
jwtJsonwebtoken.decode(UserToken)
|
||||
})
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Example app listening on port ${port}`)
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
const express = require('express')
|
||||
const app = express()
|
||||
const jwtJsonwebtoken = require('jsonwebtoken');
|
||||
const jwt_decode = require('jwt-decode');
|
||||
const jwt_simple = require('jwt-simple');
|
||||
const jose = require('jose')
|
||||
const port = 3000
|
||||
|
||||
function getSecret() {
|
||||
return "A Safe generated random key"
|
||||
}
|
||||
|
||||
app.get('/jose1', async (req, res) => {
|
||||
const UserToken = req.headers.authorization;
|
||||
// GOOD: with signature verification
|
||||
await jose.jwtVerify(UserToken, new TextEncoder().encode(getSecret()))
|
||||
})
|
||||
|
||||
app.get('/jose2', async (req, res) => {
|
||||
const UserToken = req.headers.authorization;
|
||||
// GOOD: first without signature verification then with signature verification for same UserToken
|
||||
jose.decodeJwt(UserToken)
|
||||
await jose.jwtVerify(UserToken, new TextEncoder().encode(getSecret()))
|
||||
})
|
||||
|
||||
app.get('/jwtSimple1', (req, res) => {
|
||||
const UserToken = req.headers.authorization;
|
||||
// GOOD: first without signature verification then with signature verification for same UserToken
|
||||
jwt_simple.decode(UserToken, getSecret(), false);
|
||||
jwt_simple.decode(UserToken, getSecret());
|
||||
})
|
||||
|
||||
app.get('/jwtSimple2', (req, res) => {
|
||||
const UserToken = req.headers.authorization;
|
||||
// GOOD: with signature verification
|
||||
jwt_simple.decode(UserToken, getSecret(), true);
|
||||
jwt_simple.decode(UserToken, getSecret());
|
||||
})
|
||||
|
||||
app.get('/jwtJsonwebtoken1', (req, res) => {
|
||||
const UserToken = req.headers.authorization;
|
||||
// GOOD: with signature verification
|
||||
jwtJsonwebtoken.verify(UserToken, getSecret())
|
||||
})
|
||||
|
||||
app.get('/jwtJsonwebtoken2', (req, res) => {
|
||||
const UserToken = req.headers.authorization;
|
||||
// GOOD: first without signature verification then with signature verification for same UserToken
|
||||
jwtJsonwebtoken.decode(UserToken)
|
||||
jwtJsonwebtoken.verify(UserToken, getSecret())
|
||||
})
|
||||
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Example app listening on port ${port}`)
|
||||
})
|
||||
54
javascript/ql/src/experimental/Security/CWE-347/JWT.qll
Normal file
54
javascript/ql/src/experimental/Security/CWE-347/JWT.qll
Normal file
@@ -0,0 +1,54 @@
|
||||
import javascript
|
||||
|
||||
DataFlow::Node unverifiedDecode() {
|
||||
result = API::moduleImport("jsonwebtoken").getMember("decode").getParameter(0).asSink()
|
||||
or
|
||||
exists(API::Node verify | verify = API::moduleImport("jsonwebtoken").getMember("verify") |
|
||||
verify
|
||||
.getParameter(2)
|
||||
.getMember("algorithms")
|
||||
.getUnknownMember()
|
||||
.asSink()
|
||||
.mayHaveStringValue("none") and
|
||||
result = verify.getParameter(0).asSink()
|
||||
)
|
||||
or
|
||||
// jwt-simple
|
||||
exists(API::Node n | n = API::moduleImport("jwt-simple").getMember("decode") |
|
||||
n.getParameter(2).asSink().asExpr() = any(BoolLiteral b | b.getBoolValue() = true) and
|
||||
result = n.getParameter(0).asSink()
|
||||
)
|
||||
or
|
||||
// jwt-decode
|
||||
result = API::moduleImport("jwt-decode").getParameter(0).asSink()
|
||||
or
|
||||
//jose
|
||||
result = API::moduleImport("jose").getMember("decodeJwt").getParameter(0).asSink()
|
||||
}
|
||||
|
||||
DataFlow::Node verifiedDecode() {
|
||||
exists(API::Node verify | verify = API::moduleImport("jsonwebtoken").getMember("verify") |
|
||||
(
|
||||
not verify
|
||||
.getParameter(2)
|
||||
.getMember("algorithms")
|
||||
.getUnknownMember()
|
||||
.asSink()
|
||||
.mayHaveStringValue("none") or
|
||||
not exists(verify.getParameter(2).getMember("algorithms"))
|
||||
) and
|
||||
result = verify.getParameter(0).asSink()
|
||||
)
|
||||
or
|
||||
// jwt-simple
|
||||
exists(API::Node n | n = API::moduleImport("jwt-simple").getMember("decode") |
|
||||
(
|
||||
n.getParameter(2).asSink().asExpr() = any(BoolLiteral b | b.getBoolValue() = false) or
|
||||
not exists(n.getParameter(2))
|
||||
) and
|
||||
result = n.getParameter(0).asSink()
|
||||
or
|
||||
//jose
|
||||
result = API::moduleImport("jose").getMember("jwtVerify").getParameter(0).asSink()
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
|
||||
<qhelp>
|
||||
<overview>
|
||||
<p>
|
||||
A JSON Web Token (JWT) is used for authenticating and managing users in an application.
|
||||
</p>
|
||||
<p>
|
||||
Only Decoding JWTs without checking if they have a valid signature or not can lead to security vulnerabilities.
|
||||
</p>
|
||||
|
||||
</overview>
|
||||
<recommendation>
|
||||
|
||||
<p>
|
||||
Don't use methods that only decode JWT, Instead use methods that verify the signature of JWT.
|
||||
</p>
|
||||
|
||||
</recommendation>
|
||||
<example>
|
||||
|
||||
<p>
|
||||
In the following code, you can see the proper usage of the most popular JWT libraries.
|
||||
</p>
|
||||
<sample src="Examples/Good.js" />
|
||||
|
||||
<p>
|
||||
In the following code, you can see the improper usage of the most popular JWT libraries.
|
||||
</p>
|
||||
<sample src="Examples/Bad.js" />
|
||||
</example>
|
||||
<references>
|
||||
<li>
|
||||
<a href="https://www.ghostccamm.com/blog/multi_strapi_vulns/#cve-2023-22893-authentication-bypass-for-aws-cognito-login-provider-in-strapi-versions-456">JWT claim has not been verified</a>
|
||||
</li>
|
||||
</references>
|
||||
|
||||
</qhelp>
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* @name JWT missing secret or public key verification
|
||||
* @description The application does not verify the JWT payload with a cryptographic secret or public key.
|
||||
* @kind path-problem
|
||||
* @problem.severity error
|
||||
* @security-severity 8.0
|
||||
* @precision high
|
||||
* @id js/decode-jwt-without-verification
|
||||
* @tags security
|
||||
* external/cwe/cwe-347
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import DataFlow::PathGraph
|
||||
import JWT
|
||||
|
||||
class ConfigurationUnverifiedDecode extends TaintTracking::Configuration {
|
||||
ConfigurationUnverifiedDecode() { this = "jsonwebtoken without any signature verification" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) { sink = unverifiedDecode() }
|
||||
}
|
||||
|
||||
class ConfigurationVerifiedDecode extends TaintTracking::Configuration {
|
||||
ConfigurationVerifiedDecode() { this = "jsonwebtoken with signature verification" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) { sink = verifiedDecode() }
|
||||
}
|
||||
|
||||
from ConfigurationUnverifiedDecode cfg, DataFlow::PathNode source, DataFlow::PathNode sink
|
||||
where
|
||||
cfg.hasFlowPath(source, sink) and
|
||||
not exists(ConfigurationVerifiedDecode cfg2 |
|
||||
cfg2.hasFlowPath(any(DataFlow::PathNode p | p.getNode() = source.getNode()), _)
|
||||
)
|
||||
select source.getNode(), source, sink, "Decoding JWT $@.", sink.getNode(),
|
||||
"without signature verification"
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* @name JWT missing secret or public key verification
|
||||
* @description The application does not verify the JWT payload with a cryptographic secret or public key.
|
||||
* @kind path-problem
|
||||
* @problem.severity error
|
||||
* @security-severity 8.0
|
||||
* @precision high
|
||||
* @id js/decode-jwt-without-verification-local-source
|
||||
* @tags security
|
||||
* external/cwe/cwe-347
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import DataFlow::PathGraph
|
||||
import JWT
|
||||
|
||||
class Configuration extends TaintTracking::Configuration {
|
||||
Configuration() { this = "jsonwebtoken without any signature verification" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) {
|
||||
source = [unverifiedDecode(), verifiedDecode()].getALocalSource()
|
||||
}
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) {
|
||||
sink = unverifiedDecode()
|
||||
or
|
||||
sink = verifiedDecode()
|
||||
}
|
||||
}
|
||||
|
||||
/** Holds if `source` flows to the first parameter of jsonwebtoken.verify */
|
||||
predicate isSafe(Configuration cfg, DataFlow::Node source) {
|
||||
exists(DataFlow::Node sink |
|
||||
cfg.hasFlow(source, sink) and
|
||||
sink = verifiedDecode()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if:
|
||||
* - `source` does not flow to the first parameter of `jsonwebtoken.verify`, and
|
||||
* - `source` flows to the first parameter of `jsonwebtoken.decode`
|
||||
*/
|
||||
predicate isVulnerable(Configuration cfg, DataFlow::Node source, DataFlow::Node sink) {
|
||||
not isSafe(cfg, source) and // i.e., source does not flow to a verify call
|
||||
cfg.hasFlow(source, sink) and // but it does flow to something else
|
||||
sink = unverifiedDecode() // and that something else is a call to decode.
|
||||
}
|
||||
|
||||
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
|
||||
where
|
||||
cfg.hasFlowPath(source, sink) and
|
||||
isVulnerable(cfg, source.getNode(), sink.getNode())
|
||||
select source.getNode(), source, sink, "Decoding JWT $@.", sink.getNode(),
|
||||
"without signature verification"
|
||||
36
javascript/ql/src/experimental/Security/CWE-942/Apollo.qll
Normal file
36
javascript/ql/src/experimental/Security/CWE-942/Apollo.qll
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Provides classes for working with Apollo GraphQL connectors.
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
/** Provides classes modeling the apollo packages [@apollo/server](https://npmjs.com/package/@apollo/server`) */
|
||||
module Apollo {
|
||||
/** Get a reference to the `ApolloServer` class. */
|
||||
private API::Node apollo() {
|
||||
result =
|
||||
API::moduleImport([
|
||||
"@apollo/server", "@apollo/apollo-server-express", "@apollo/apollo-server-core",
|
||||
"apollo-server", "apollo-server-express"
|
||||
]).getMember("ApolloServer")
|
||||
}
|
||||
|
||||
/** Gets a reference to the `gql` function that parses GraphQL strings. */
|
||||
private API::Node gql() {
|
||||
result =
|
||||
API::moduleImport([
|
||||
"@apollo/server", "@apollo/apollo-server-express", "@apollo/apollo-server-core",
|
||||
"apollo-server", "apollo-server-express"
|
||||
]).getMember("gql")
|
||||
}
|
||||
|
||||
/** An instantiation of an `ApolloServer`. */
|
||||
class ApolloServer extends API::NewNode {
|
||||
ApolloServer() { this = apollo().getAnInstantiation() }
|
||||
}
|
||||
|
||||
/** A string that is interpreted as a GraphQL query by a `apollo` package. */
|
||||
private class ApolloGraphQLString extends GraphQL::GraphQLString {
|
||||
ApolloGraphQLString() { this = gql().getACall().getArgument(0) }
|
||||
}
|
||||
}
|
||||
24
javascript/ql/src/experimental/Security/CWE-942/Cors.qll
Normal file
24
javascript/ql/src/experimental/Security/CWE-942/Cors.qll
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Provides classes for working with Cors connectors.
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
/** Provides classes modeling the [cors](https://npmjs.com/package/cors) library. */
|
||||
module Cors {
|
||||
/**
|
||||
* An expression that creates a new CORS configuration.
|
||||
*/
|
||||
class Cors extends DataFlow::CallNode {
|
||||
Cors() { this = DataFlow::moduleImport("cors").getAnInvocation() }
|
||||
|
||||
/** Get the options used to configure Cors */
|
||||
DataFlow::Node getOptionsArgument() { result = this.getArgument(0) }
|
||||
|
||||
/** Holds if cors is using default configuration */
|
||||
predicate isDefault() { this.getNumArgument() = 0 }
|
||||
|
||||
/** Gets the value of the `origin` option used to configure this Cors instance. */
|
||||
DataFlow::Node getOrigin() { result = this.getOptionArgument(0, "origin") }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
<p>
|
||||
|
||||
A server can use <code>CORS</code> (Cross-Origin Resource Sharing) to relax the
|
||||
restrictions imposed by the <code>SOP</code> (Same-Origin Policy), allowing controlled, secure
|
||||
cross-origin requests when necessary.
|
||||
|
||||
A server with an overly permissive <code>CORS</code> configuration may inadvertently
|
||||
expose sensitive data or lead to <code>CSRF</code> which is an attack that allows attackers to trick
|
||||
users into performing unwanted operations in websites they're authenticated to.
|
||||
|
||||
</p>
|
||||
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>
|
||||
|
||||
When the <code>origin</code> is set to <code>true</code>, it signifies that the server
|
||||
is accepting requests from <code>any</code> origin, potentially exposing the system to
|
||||
CSRF attacks. This can be fixed using <code>false</code> as origin value or using a whitelist.
|
||||
|
||||
</p>
|
||||
<p>
|
||||
|
||||
On the other hand, if the <code>origin</code> is
|
||||
set to <code>null</code>, it can be exploited by an attacker to deceive a user into making
|
||||
requests from a <code>null</code> origin form, often hosted within a sandboxed iframe.
|
||||
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
||||
If the <code>origin</code> value is user controlled, make sure that the data
|
||||
is properly sanitized.
|
||||
|
||||
</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>
|
||||
|
||||
In the example below, the <code>server_1</code> accepts requests from any origin
|
||||
since the value of <code>origin</code> is set to <code>true</code>.
|
||||
And <code>server_2</code>'s origin is user-controlled.
|
||||
|
||||
</p>
|
||||
|
||||
<sample src="examples/CorsPermissiveConfigurationBad.js"/>
|
||||
|
||||
<p>
|
||||
|
||||
In the example below, the <code>server_1</code> CORS is restrictive so it's not
|
||||
vulnerable to CSRF attacks. And <code>server_2</code>'s is using properly sanitized
|
||||
user-controlled data.
|
||||
|
||||
</p>
|
||||
|
||||
<sample src="examples/CorsPermissiveConfigurationGood.js"/>
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>Mozilla Developer Network: <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin">CORS, Access-Control-Allow-Origin</a>.</li>
|
||||
<li>W3C: <a href="https://w3c.github.io/webappsec-cors-for-developers/#resources">CORS for developers, Advice for Resource Owners</a></li>
|
||||
</references>
|
||||
</qhelp>
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @name overly CORS configuration
|
||||
* @description Misconfiguration of CORS HTTP headers allows CSRF attacks.
|
||||
* @kind path-problem
|
||||
* @problem.severity error
|
||||
* @security-severity 7.5
|
||||
* @precision high
|
||||
* @id js/cors-misconfiguration
|
||||
* @tags security
|
||||
* external/cwe/cwe-942
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import CorsPermissiveConfigurationQuery
|
||||
import DataFlow::PathGraph
|
||||
|
||||
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
|
||||
where cfg.hasFlowPath(source, sink)
|
||||
select sink.getNode(), source, sink, "CORS Origin misconfiguration due to a $@.", source.getNode(),
|
||||
"too permissive or user controlled value"
|
||||
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for reasoning about
|
||||
* overly permissive CORS configurations, as well as
|
||||
* extension points for adding your own.
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import Cors::Cors
|
||||
import Apollo::Apollo
|
||||
|
||||
/** Module containing sources, sinks, and sanitizers for overly permissive CORS configurations. */
|
||||
module CorsPermissiveConfiguration {
|
||||
/**
|
||||
* A data flow source for permissive CORS configuration.
|
||||
*/
|
||||
abstract class Source extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A data flow sink for permissive CORS configuration.
|
||||
*/
|
||||
abstract class Sink extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A sanitizer for permissive CORS configuration.
|
||||
*/
|
||||
abstract class Sanitizer extends DataFlow::Node { }
|
||||
|
||||
/** A source of remote user input, considered as a flow source for CORS misconfiguration. */
|
||||
class RemoteFlowSourceAsSource extends Source instanceof RemoteFlowSource {
|
||||
RemoteFlowSourceAsSource() { not this instanceof ClientSideRemoteFlowSource }
|
||||
}
|
||||
|
||||
/** A flow label representing `true` and `null` values. */
|
||||
abstract class TrueAndNull extends DataFlow::FlowLabel {
|
||||
TrueAndNull() { this = "TrueAndNull" }
|
||||
}
|
||||
|
||||
TrueAndNull truenullLabel() { any() }
|
||||
|
||||
/** A flow label representing `*` value. */
|
||||
abstract class Wildcard extends DataFlow::FlowLabel {
|
||||
Wildcard() { this = "Wildcard" }
|
||||
}
|
||||
|
||||
Wildcard wildcardLabel() { any() }
|
||||
|
||||
/** An overly permissive value for `origin` (Apollo) */
|
||||
class TrueNullValue extends Source {
|
||||
TrueNullValue() { this.mayHaveBooleanValue(true) or this.asExpr() instanceof NullLiteral }
|
||||
}
|
||||
|
||||
/** An overly permissive value for `origin` (Express) */
|
||||
class WildcardValue extends Source {
|
||||
WildcardValue() { this.mayHaveStringValue("*") }
|
||||
}
|
||||
|
||||
/**
|
||||
* The value of cors origin when initializing the application.
|
||||
*/
|
||||
class CorsApolloServer extends Sink, DataFlow::ValueNode {
|
||||
CorsApolloServer() {
|
||||
exists(ApolloServer agql |
|
||||
this =
|
||||
agql.getOptionArgument(0, "cors").getALocalSource().getAPropertyWrite("origin").getRhs()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The value of cors origin when initializing the application.
|
||||
*/
|
||||
class ExpressCors extends Sink, DataFlow::ValueNode {
|
||||
ExpressCors() {
|
||||
exists(CorsConfiguration config | this = config.getCorsConfiguration().getOrigin())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An express route setup configured with the `cors` package.
|
||||
*/
|
||||
class CorsConfiguration extends DataFlow::MethodCallNode {
|
||||
Cors corsConfig;
|
||||
|
||||
CorsConfiguration() {
|
||||
exists(Express::RouteSetup setup | this = setup |
|
||||
if setup.isUseCall()
|
||||
then corsConfig = setup.getArgument(0)
|
||||
else corsConfig = setup.getArgument(any(int i | i > 0))
|
||||
)
|
||||
}
|
||||
|
||||
/** Gets the expression that configures `cors` on this route setup. */
|
||||
Cors getCorsConfiguration() { result = corsConfig }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Provides a dataflow taint tracking configuration for reasoning
|
||||
* about overly permissive CORS configurations.
|
||||
*
|
||||
* Note, for performance reasons: only import this file if
|
||||
* `CorsPermissiveConfiguration::Configuration` is needed,
|
||||
* otherwise `CorsPermissiveConfigurationCustomizations` should
|
||||
* be imported instead.
|
||||
*/
|
||||
|
||||
import javascript
|
||||
import CorsPermissiveConfigurationCustomizations::CorsPermissiveConfiguration
|
||||
|
||||
/**
|
||||
* A data flow configuration for overly permissive CORS configuration.
|
||||
*/
|
||||
class Configuration extends TaintTracking::Configuration {
|
||||
Configuration() { this = "CorsPermissiveConfiguration" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source, DataFlow::FlowLabel label) {
|
||||
source instanceof TrueNullValue and label = truenullLabel()
|
||||
or
|
||||
source instanceof WildcardValue and label = wildcardLabel()
|
||||
or
|
||||
source instanceof RemoteFlowSource and label = DataFlow::FlowLabel::taint()
|
||||
}
|
||||
|
||||
override predicate isSink(DataFlow::Node sink, DataFlow::FlowLabel label) {
|
||||
sink instanceof CorsApolloServer and label = [DataFlow::FlowLabel::taint(), truenullLabel()]
|
||||
or
|
||||
sink instanceof ExpressCors and label = [DataFlow::FlowLabel::taint(), wildcardLabel()]
|
||||
}
|
||||
|
||||
override predicate isSanitizer(DataFlow::Node node) {
|
||||
super.isSanitizer(node) or
|
||||
node instanceof Sanitizer
|
||||
}
|
||||
}
|
||||
|
||||
private class WildcardActivated extends DataFlow::FlowLabel, Wildcard {
|
||||
WildcardActivated() { this = this }
|
||||
}
|
||||
|
||||
private class TrueAndNullActivated extends DataFlow::FlowLabel, TrueAndNull {
|
||||
TrueAndNullActivated() { this = this }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ApolloServer } from 'apollo-server';
|
||||
var https = require('https'),
|
||||
url = require('url');
|
||||
|
||||
var server = https.createServer(function () { });
|
||||
|
||||
server.on('request', function (req, res) {
|
||||
// BAD: origin is too permissive
|
||||
const server_1 = new ApolloServer({
|
||||
cors: { origin: true }
|
||||
});
|
||||
|
||||
let user_origin = url.parse(req.url, true).query.origin;
|
||||
// BAD: CORS is controlled by user
|
||||
const server_2 = new ApolloServer({
|
||||
cors: { origin: user_origin }
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ApolloServer } from 'apollo-server';
|
||||
var https = require('https'),
|
||||
url = require('url');
|
||||
|
||||
var server = https.createServer(function () { });
|
||||
|
||||
server.on('request', function (req, res) {
|
||||
// GOOD: origin is restrictive
|
||||
const server_1 = new ApolloServer({
|
||||
cors: { origin: false }
|
||||
});
|
||||
|
||||
let user_origin = url.parse(req.url, true).query.origin;
|
||||
// GOOD: user data is properly sanitized
|
||||
const server_2 = new ApolloServer({
|
||||
cors: { origin: (user_origin === "https://allowed1.com" || user_origin === "https://allowed2.com") ? user_origin : false }
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user