Skip to content

Commit 0652afc

Browse files
committed
update tests, updated qldoc and examples, upgrade all libraries to path-problem, update jsonwebtoken source and sinks
1 parent a9c8bc0 commit 0652afc

22 files changed

+521
-195
lines changed

javascript/ql/src/Security/CWE-347-noVerification/Example.js

Lines changed: 0 additions & 45 deletions
This file was deleted.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
const express = require('express')
2+
const app = express()
3+
const jwtJsonwebtoken = require('jsonwebtoken');
4+
const jwt_decode = require('jwt-decode');
5+
const jwt_simple = require('jwt-simple');
6+
const jose = require('jose')
7+
const port = 3000
8+
9+
function getSecret() {
10+
return "A Safe generated random key"
11+
}
12+
app.get('/jose', (req, res) => {
13+
const UserToken = req.headers.authorization;
14+
// BAD: no signature verification
15+
jose.decodeJwt(UserToken)
16+
})
17+
18+
app.get('/jwtDecode', (req, res) => {
19+
const UserToken = req.headers.authorization;
20+
// BAD: no signature verification
21+
jwt_decode(UserToken)
22+
})
23+
24+
app.get('/jwtSimple', (req, res) => {
25+
const UserToken = req.headers.authorization;
26+
// jwt.decode(token, key, noVerify, algorithm)
27+
// BAD: no signature verification
28+
jwt_simple.decode(UserToken, getSecret(), true);
29+
})
30+
31+
app.get('/jwtJsonwebtoken', (req, res) => {
32+
const UserToken = req.headers.authorization;
33+
// BAD: no signature verification
34+
jwtJsonwebtoken.decode(UserToken)
35+
})
36+
37+
app.listen(port, () => {
38+
console.log(`Example app listening on port ${port}`)
39+
})
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
const express = require('express')
2+
const app = express()
3+
const jwtJsonwebtoken = require('jsonwebtoken');
4+
const jwt_decode = require('jwt-decode');
5+
const jwt_simple = require('jwt-simple');
6+
const jose = require('jose')
7+
const port = 3000
8+
9+
function getSecret() {
10+
return "A Safe generated random key"
11+
}
12+
13+
app.get('/jose1', async (req, res) => {
14+
const UserToken = req.headers.authorization;
15+
// GOOD: with signature verification
16+
await jose.jwtVerify(UserToken, new TextEncoder().encode(getSecret()))
17+
})
18+
19+
app.get('/jose2', async (req, res) => {
20+
const UserToken = req.headers.authorization;
21+
// GOOD: first without signature verification then with signature verification for same UserToken
22+
jose.decodeJwt(UserToken)
23+
await jose.jwtVerify(UserToken, new TextEncoder().encode(getSecret()))
24+
})
25+
26+
app.get('/jwtSimple1', (req, res) => {
27+
const UserToken = req.headers.authorization;
28+
// GOOD: first without signature verification then with signature verification for same UserToken
29+
jwt_simple.decode(UserToken, getSecret(), false);
30+
jwt_simple.decode(UserToken, getSecret());
31+
})
32+
33+
app.get('/jwtSimple2', (req, res) => {
34+
const UserToken = req.headers.authorization;
35+
// GOOD: with signature verification
36+
jwt_simple.decode(UserToken, getSecret(), true);
37+
jwt_simple.decode(UserToken, getSecret());
38+
})
39+
40+
app.get('/jwtJsonwebtoken1', (req, res) => {
41+
const UserToken = req.headers.authorization;
42+
// GOOD: with signature verification
43+
jwtJsonwebtoken.verify(UserToken, getSecret())
44+
})
45+
46+
app.get('/jwtJsonwebtoken2', (req, res) => {
47+
const UserToken = req.headers.authorization;
48+
// GOOD: first without signature verification then with signature verification for same UserToken
49+
jwtJsonwebtoken.decode(UserToken)
50+
jwtJsonwebtoken.verify(UserToken, getSecret())
51+
})
52+
53+
54+
app.listen(port, () => {
55+
console.log(`Example app listening on port ${port}`)
56+
})

javascript/ql/src/Security/CWE-347-noVerification/JsonWebToken.ql

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,35 @@ import DataFlow::PathGraph
1515

1616
DataFlow::Node unverifiedDecode() {
1717
result = API::moduleImport("jsonwebtoken").getMember("decode").getParameter(0).asSink()
18+
or
19+
exists(API::Node verify | verify = API::moduleImport("jsonwebtoken").getMember("verify") |
20+
verify
21+
.getParameter(2)
22+
.getMember("algorithms")
23+
.getUnknownMember()
24+
.asSink()
25+
.mayHaveStringValue("none") and
26+
result = verify.getParameter(0).asSink()
27+
)
1828
}
1929

2030
DataFlow::Node verifiedDecode() {
21-
result = API::moduleImport("jsonwebtoken").getMember("verify").getParameter(0).asSink()
31+
exists(API::Node verify | verify = API::moduleImport("jsonwebtoken").getMember("verify") |
32+
(
33+
not verify
34+
.getParameter(2)
35+
.getMember("algorithms")
36+
.getUnknownMember()
37+
.asSink()
38+
.mayHaveStringValue("none") or
39+
not exists(verify.getParameter(2).getMember("algorithms"))
40+
) and
41+
result = verify.getParameter(0).asSink()
42+
)
2243
}
2344

2445
class Configuration extends TaintTracking::Configuration {
25-
Configuration() { this = "jsonwebtoken constant secret key" }
46+
Configuration() { this = "jsonwebtoken without any signature verification" }
2647

2748
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
2849

@@ -37,11 +58,8 @@ from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
3758
where
3859
cfg.hasFlowPath(source, sink) and
3960
sink.getNode() = unverifiedDecode() and
40-
not isSafe(source.getNode())
61+
not exists(Configuration cfg2 |
62+
cfg2.hasFlowPath(source, any(DataFlow::SinkPathNode n | n.getNode() = verifiedDecode()))
63+
)
4164
select source.getNode(), source, sink, "Decoding JWT $@.", sink.getNode(),
4265
"without signature verification"
43-
44-
// Holds if the source has a flow to a JWT decoding function with signature verification
45-
predicate isSafe(DataFlow::Node source) {
46-
exists(Configuration cfg | cfg.hasFlow(source, verifiedDecode()))
47-
}
Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* @name JWT missing secret or public key verification
33
* @description The application does not verify the JWT payload with a cryptographic secret or public key.
4-
* @kind problem
4+
* @kind path-problem
55
* @problem.severity error
66
* @security-severity 8.0
77
* @precision high
@@ -11,7 +11,34 @@
1111
*/
1212

1313
import javascript
14+
import DataFlow::PathGraph
1415

15-
from DataFlow::Node sink
16-
where sink = API::moduleImport("jose").getMember("decodeJwt").getParameter(0).asSink()
17-
select sink, "This Token is Decoded in without signature validation"
16+
DataFlow::Node unverifiedDecode() {
17+
result = API::moduleImport("jose").getMember("decodeJwt").getParameter(0).asSink()
18+
}
19+
20+
DataFlow::Node verifiedDecode() {
21+
result = API::moduleImport("jose").getMember("jwtVerify").getParameter(0).asSink()
22+
}
23+
24+
class Configuration extends TaintTracking::Configuration {
25+
Configuration() { this = "jsonwebtoken without any signature verification" }
26+
27+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
28+
29+
override predicate isSink(DataFlow::Node sink) {
30+
sink = unverifiedDecode()
31+
or
32+
sink = verifiedDecode()
33+
}
34+
}
35+
36+
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
37+
where
38+
cfg.hasFlowPath(source, sink) and
39+
sink.getNode() = unverifiedDecode() and
40+
not exists(Configuration cfg2 |
41+
cfg2.hasFlowPath(source, any(DataFlow::SinkPathNode n | n.getNode() = verifiedDecode()))
42+
)
43+
select source.getNode(), source, sink, "Decoding JWT $@.", sink.getNode(),
44+
"without signature verification"
Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* @name JWT missing secret or public key verification
33
* @description The application does not verify the JWT payload with a cryptographic secret or public key.
4-
* @kind problem
4+
* @kind path-problem
55
* @problem.severity error
66
* @security-severity 8.0
77
* @precision high
@@ -11,9 +11,21 @@
1111
*/
1212

1313
import javascript
14+
import DataFlow::PathGraph
1415

15-
from DataFlow::Node sink
16-
where
17-
// I must somehow check that following is implemented in server not browser
18-
sink = API::moduleImport("jwt-decode").getParameter(0).asSink()
19-
select sink, "This Token is Decoded in without signature validation"
16+
DataFlow::Node unverifiedDecode() {
17+
result = API::moduleImport("jwt-decode").getParameter(0).asSink()
18+
}
19+
20+
class Configuration extends TaintTracking::Configuration {
21+
Configuration() { this = "jsonwebtoken without any signature verification" }
22+
23+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
24+
25+
override predicate isSink(DataFlow::Node sink) { sink = unverifiedDecode() }
26+
}
27+
28+
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
29+
where cfg.hasFlowPath(source, sink)
30+
select source.getNode(), source, sink, "Decoding JWT $@.", sink.getNode(),
31+
"without signature verification"

javascript/ql/src/Security/CWE-347-noVerification/jwtNoVerification.qhelp

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,18 @@
1919
<example>
2020

2121
<p>
22-
The following code you can see an Example from a popular Library.
22+
In the following code, you can see the proper usage of the most popular JWT libraries.
2323
</p>
24+
<sample src="Examples/Good.js" />
2425

25-
<sample src="Example.js" />
26-
26+
<p>
27+
In the following code, you can see the improper usage of the most popular JWT libraries.
28+
</p>
29+
<sample src="Examples/Bad.js" />
2730
</example>
2831
<references>
2932
<li>
30-
<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 had not been verified</a>
33+
<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>
3134
</li>
3235
</references>
3336

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,53 @@
11
/**
22
* @name JWT missing secret or public key verification
33
* @description The application does not verify the JWT payload with a cryptographic secret or public key.
4-
* @kind problem
4+
* @kind path-problem
55
* @problem.severity error
66
* @security-severity 8.0
77
* @precision high
8-
* @id js/jwt-missing-verification
8+
* @id js/jwt-missing-verification-jwt-simple
99
* @tags security
1010
* external/cwe/cwe-347
1111
*/
1212

1313
import javascript
14+
import DataFlow::PathGraph
1415

15-
from DataFlow::Node sink
16-
where
16+
DataFlow::Node unverifiedDecode() {
1717
exists(API::Node n | n = API::moduleImport("jwt-simple").getMember("decode") |
1818
n.getParameter(2).asSink().asExpr() = any(BoolLiteral b | b.getBoolValue() = true) and
19-
sink = n.getParameter(0).asSink()
19+
result = n.getParameter(0).asSink()
20+
)
21+
}
22+
23+
DataFlow::Node verifiedDecode() {
24+
exists(API::Node n | n = API::moduleImport("jwt-simple").getMember("decode") |
25+
(
26+
n.getParameter(2).asSink().asExpr() = any(BoolLiteral b | b.getBoolValue() = false) or
27+
not exists(n.getParameter(2))
28+
) and
29+
result = n.getParameter(0).asSink()
30+
)
31+
}
32+
33+
class Configuration extends TaintTracking::Configuration {
34+
Configuration() { this = "jsonwebtoken without any signature verification" }
35+
36+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
37+
38+
override predicate isSink(DataFlow::Node sink) {
39+
sink = unverifiedDecode()
40+
or
41+
sink = verifiedDecode()
42+
}
43+
}
44+
45+
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
46+
where
47+
cfg.hasFlowPath(source, sink) and
48+
sink.getNode() = unverifiedDecode() and
49+
not exists(Configuration cfg2 |
50+
cfg2.hasFlowPath(source, any(DataFlow::SinkPathNode n | n.getNode() = verifiedDecode()))
2051
)
21-
select sink, "This Token is Decoded in without signature validation"
52+
select source.getNode(), source, sink, "Decoding JWT $@.", sink.getNode(),
53+
"without signature verification"

0 commit comments

Comments
 (0)