Skip to content

Commit e84028d

Browse files
authored
Merge pull request github#14088 from am0o0/amammad-js-JWT
JS: decoding JWT without signature verification
2 parents 0e04a59 + 1033bf9 commit e84028d

18 files changed

+850
-0
lines changed
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+
})
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import javascript
2+
3+
DataFlow::Node unverifiedDecode() {
4+
result = API::moduleImport("jsonwebtoken").getMember("decode").getParameter(0).asSink()
5+
or
6+
exists(API::Node verify | verify = API::moduleImport("jsonwebtoken").getMember("verify") |
7+
verify
8+
.getParameter(2)
9+
.getMember("algorithms")
10+
.getUnknownMember()
11+
.asSink()
12+
.mayHaveStringValue("none") and
13+
result = verify.getParameter(0).asSink()
14+
)
15+
or
16+
// jwt-simple
17+
exists(API::Node n | n = API::moduleImport("jwt-simple").getMember("decode") |
18+
n.getParameter(2).asSink().asExpr() = any(BoolLiteral b | b.getBoolValue() = true) and
19+
result = n.getParameter(0).asSink()
20+
)
21+
or
22+
// jwt-decode
23+
result = API::moduleImport("jwt-decode").getParameter(0).asSink()
24+
or
25+
//jose
26+
result = API::moduleImport("jose").getMember("decodeJwt").getParameter(0).asSink()
27+
}
28+
29+
DataFlow::Node verifiedDecode() {
30+
exists(API::Node verify | verify = API::moduleImport("jsonwebtoken").getMember("verify") |
31+
(
32+
not verify
33+
.getParameter(2)
34+
.getMember("algorithms")
35+
.getUnknownMember()
36+
.asSink()
37+
.mayHaveStringValue("none") or
38+
not exists(verify.getParameter(2).getMember("algorithms"))
39+
) and
40+
result = verify.getParameter(0).asSink()
41+
)
42+
or
43+
// jwt-simple
44+
exists(API::Node n | n = API::moduleImport("jwt-simple").getMember("decode") |
45+
(
46+
n.getParameter(2).asSink().asExpr() = any(BoolLiteral b | b.getBoolValue() = false) or
47+
not exists(n.getParameter(2))
48+
) and
49+
result = n.getParameter(0).asSink()
50+
or
51+
//jose
52+
result = API::moduleImport("jose").getMember("jwtVerify").getParameter(0).asSink()
53+
)
54+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
2+
<qhelp>
3+
<overview>
4+
<p>
5+
A JSON Web Token (JWT) is used for authenticating and managing users in an application.
6+
</p>
7+
<p>
8+
Only Decoding JWTs without checking if they have a valid signature or not can lead to security vulnerabilities.
9+
</p>
10+
11+
</overview>
12+
<recommendation>
13+
14+
<p>
15+
Don't use methods that only decode JWT, Instead use methods that verify the signature of JWT.
16+
</p>
17+
18+
</recommendation>
19+
<example>
20+
21+
<p>
22+
In the following code, you can see the proper usage of the most popular JWT libraries.
23+
</p>
24+
<sample src="Examples/Good.js" />
25+
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" />
30+
</example>
31+
<references>
32+
<li>
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>
34+
</li>
35+
</references>
36+
37+
</qhelp>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* @name JWT missing secret or public key verification
3+
* @description The application does not verify the JWT payload with a cryptographic secret or public key.
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @security-severity 8.0
7+
* @precision high
8+
* @id js/decode-jwt-without-verification
9+
* @tags security
10+
* external/cwe/cwe-347
11+
*/
12+
13+
import javascript
14+
import DataFlow::PathGraph
15+
import JWT
16+
17+
class ConfigurationUnverifiedDecode extends TaintTracking::Configuration {
18+
ConfigurationUnverifiedDecode() { this = "jsonwebtoken without any signature verification" }
19+
20+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
21+
22+
override predicate isSink(DataFlow::Node sink) { sink = unverifiedDecode() }
23+
}
24+
25+
class ConfigurationVerifiedDecode extends TaintTracking::Configuration {
26+
ConfigurationVerifiedDecode() { this = "jsonwebtoken with signature verification" }
27+
28+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
29+
30+
override predicate isSink(DataFlow::Node sink) { sink = verifiedDecode() }
31+
}
32+
33+
from ConfigurationUnverifiedDecode cfg, DataFlow::PathNode source, DataFlow::PathNode sink
34+
where
35+
cfg.hasFlowPath(source, sink) and
36+
not exists(ConfigurationVerifiedDecode cfg2 |
37+
cfg2.hasFlowPath(any(DataFlow::PathNode p | p.getNode() = source.getNode()), _)
38+
)
39+
select source.getNode(), source, sink, "Decoding JWT $@.", sink.getNode(),
40+
"without signature verification"
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* @name JWT missing secret or public key verification
3+
* @description The application does not verify the JWT payload with a cryptographic secret or public key.
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @security-severity 8.0
7+
* @precision high
8+
* @id js/decode-jwt-without-verification-local-source
9+
* @tags security
10+
* external/cwe/cwe-347
11+
*/
12+
13+
import javascript
14+
import DataFlow::PathGraph
15+
import JWT
16+
17+
class Configuration extends TaintTracking::Configuration {
18+
Configuration() { this = "jsonwebtoken without any signature verification" }
19+
20+
override predicate isSource(DataFlow::Node source) {
21+
source = [unverifiedDecode(), verifiedDecode()].getALocalSource()
22+
}
23+
24+
override predicate isSink(DataFlow::Node sink) {
25+
sink = unverifiedDecode()
26+
or
27+
sink = verifiedDecode()
28+
}
29+
}
30+
31+
/** Holds if `source` flows to the first parameter of jsonwebtoken.verify */
32+
predicate isSafe(Configuration cfg, DataFlow::Node source) {
33+
exists(DataFlow::Node sink |
34+
cfg.hasFlow(source, sink) and
35+
sink = verifiedDecode()
36+
)
37+
}
38+
39+
/**
40+
* Holds if:
41+
* - `source` does not flow to the first parameter of `jsonwebtoken.verify`, and
42+
* - `source` flows to the first parameter of `jsonwebtoken.decode`
43+
*/
44+
predicate isVulnerable(Configuration cfg, DataFlow::Node source, DataFlow::Node sink) {
45+
not isSafe(cfg, source) and // i.e., source does not flow to a verify call
46+
cfg.hasFlow(source, sink) and // but it does flow to something else
47+
sink = unverifiedDecode() // and that something else is a call to decode.
48+
}
49+
50+
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
51+
where
52+
cfg.hasFlowPath(source, sink) and
53+
isVulnerable(cfg, source.getNode(), sink.getNode())
54+
select source.getNode(), source, sink, "Decoding JWT $@.", sink.getNode(),
55+
"without signature verification"
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
const express = require('express')
2+
const jwtJsonwebtoken = require('jsonwebtoken');
3+
4+
function getSecret() {
5+
return "A Safe generated random key"
6+
}
7+
8+
function aJWT() {
9+
return "A JWT provided by user"
10+
}
11+
12+
(function () {
13+
const UserToken = aJwt()
14+
15+
// BAD: no signature verification
16+
jwtJsonwebtoken.decode(UserToken) // NOT OK
17+
})();
18+
19+
(function () {
20+
const UserToken = aJwt()
21+
22+
// BAD: no signature verification
23+
jwtJsonwebtoken.decode(UserToken) // NOT OK
24+
jwtJsonwebtoken.verify(UserToken, getSecret(), { algorithms: ["HS256", "none"] }) // NOT OK
25+
})();
26+
27+
(function () {
28+
const UserToken = aJwt()
29+
30+
// GOOD: with signature verification
31+
jwtJsonwebtoken.verify(UserToken, getSecret()) // OK
32+
})();
33+
34+
(function () {
35+
const UserToken = aJwt()
36+
37+
// GOOD: first without signature verification then with signature verification for same UserToken
38+
jwtJsonwebtoken.decode(UserToken) // OK
39+
jwtJsonwebtoken.verify(UserToken, getSecret()) // OK
40+
})();
41+
42+
(function () {
43+
const UserToken = aJwt()
44+
45+
// GOOD: first without signature verification then with signature verification for same UserToken
46+
jwtJsonwebtoken.decode(UserToken) // OK
47+
jwtJsonwebtoken.verify(UserToken, getSecret(), { algorithms: ["HS256"] }) // OK
48+
})();

0 commit comments

Comments
 (0)