Skip to content

Commit 5be3020

Browse files
authored
Merge pull request #9036 from luchua-bc/java/hardcoded-jwt-key
Java: CWE-321 Query to detect hardcoded JWT secret keys
2 parents 6ecc542 + f7e1f3e commit 5be3020

22 files changed

+1433
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// BAD: Get secret from hardcoded string then sign a JWT token
2+
Algorithm algorithm = Algorithm.HMAC256("hardcoded_secret");
3+
JWT.create()
4+
.withClaim("username", username)
5+
.sign(algorithm);
6+
}
7+
8+
// BAD: Get secret from hardcoded string then verify a JWT token
9+
JWTVerifier verifier = JWT.require(Algorithm.HMAC256("hardcoded_secret"))
10+
.withIssuer(ISSUER)
11+
.build();
12+
verifier.verify(token);
13+
14+
// GOOD: Get secret from system configuration then sign a token
15+
String tokenSecret = System.getenv("SECRET_KEY");
16+
Algorithm algorithm = Algorithm.HMAC256(tokenSecret);
17+
JWT.create()
18+
.withClaim("username", username)
19+
.sign(algorithm);
20+
}
21+
22+
// GOOD: Get secret from environment variable then verify a JWT token
23+
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(System.getenv("SECRET_KEY")))
24+
.withIssuer(ISSUER)
25+
.build();
26+
verifier.verify(token);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
2+
<qhelp>
3+
<overview>
4+
<p>
5+
JWT (JSON Web Token) is an open standard (RFC 7519) that defines a way to provide information
6+
within a JSON object between two parties. JWT is widely used for sharing security information
7+
between two parties in web applications. Each JWT contains encoded JSON objects, including a
8+
set of claims. JWTs are signed using a cryptographic algorithm to ensure that the claims cannot
9+
be altered after the token is issued.
10+
</p>
11+
<p>
12+
The most basic mistake is using hardcoded secrets for JWT generation/verification. This allows
13+
an attacker to forge the token if the source code (and JWT secret in it) is publicly exposed or
14+
leaked, which leads to authentication bypass or privilege escalation.
15+
</p>
16+
</overview>
17+
18+
<recommendation>
19+
<p>
20+
Generating a cryptographically secure secret key during application initialization and using this
21+
generated key for JWT signing/verification requests can prevent this vulnerability. Or safely store
22+
the secret key in a key vault that cannot be leaked in source code.
23+
</p>
24+
</recommendation>
25+
26+
<example>
27+
<p>
28+
The following examples show the bad case and the good case respectively. The <code>bad</code>
29+
methods show a hardcoded secret key is used to sign and verify JWT tokens. In the <code>good</code>
30+
method, the secret key is loaded from a system environment during application initialization.
31+
</p>
32+
<sample src="HardcodedJwtKey.java" />
33+
</example>
34+
35+
<references>
36+
<li>
37+
Semgrep Blog:
38+
<a href="https://r2c.dev/blog/2020/hardcoded-secrets-unverified-tokens-and-other-common-jwt-mistakes/">Hardcoded secrets, unverified tokens, and other common JWT mistakes</a>
39+
</li>
40+
<li>
41+
CVE-2022-24860:
42+
<a href="https://nvd.nist.gov/vuln/detail/CVE-2022-24860">Databasir 1.01 has Use of Hard-coded Cryptographic Key vulnerability.</a>
43+
</li>
44+
</references>
45+
46+
</qhelp>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @name Use of a hardcoded key for signing JWT
3+
* @description Using a hardcoded key for signing JWT can allow an attacker to compromise security.
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @id java/hardcoded-jwt-key
7+
* @tags security
8+
* external/cwe/cwe-321
9+
*/
10+
11+
import java
12+
import HardcodedJwtKey
13+
import semmle.code.java.dataflow.TaintTracking
14+
import DataFlow::PathGraph
15+
16+
from DataFlow::PathNode source, DataFlow::PathNode sink, HardcodedJwtKeyConfiguration cfg
17+
where cfg.hasFlowPath(source, sink)
18+
select sink.getNode(), source, sink, "$@ is used to sign a JWT token.", source.getNode(),
19+
"Hardcoded String"
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* Provides sources and sinks for detecting JWT token signing vulnerabilities.
3+
*/
4+
5+
import java
6+
private import semmle.code.java.dataflow.ExternalFlow
7+
private import semmle.code.java.dataflow.FlowSources
8+
9+
/** The class `com.auth0.jwt.JWT`. */
10+
class Jwt extends RefType {
11+
Jwt() { this.hasQualifiedName("com.auth0.jwt", "JWT") }
12+
}
13+
14+
/** The class `com.auth0.jwt.JWTCreator.Builder`. */
15+
class JwtBuilder extends RefType {
16+
JwtBuilder() { this.hasQualifiedName("com.auth0.jwt", "JWTCreator$Builder") }
17+
}
18+
19+
/** The class `com.auth0.jwt.algorithms.Algorithm`. */
20+
class JwtAlgorithm extends RefType {
21+
JwtAlgorithm() { this.hasQualifiedName("com.auth0.jwt.algorithms", "Algorithm") }
22+
}
23+
24+
/**
25+
* The interface `com.auth0.jwt.interfaces.JWTVerifier` or its implementation
26+
* `com.auth0.jwt.JWTVerifier`.
27+
*/
28+
class JwtVerifier extends RefType {
29+
JwtVerifier() {
30+
this.hasQualifiedName(["com.auth0.jwt", "com.auth0.jwt.interfaces"], "JWTVerifier")
31+
}
32+
}
33+
34+
/** A method that creates an instance of `com.auth0.jwt.algorithms.Algorithm`. */
35+
class GetAlgorithmMethod extends Method {
36+
GetAlgorithmMethod() {
37+
this.getDeclaringType() instanceof JwtAlgorithm and
38+
this.getName().matches(["HMAC%", "ECDSA%", "RSA%"])
39+
}
40+
}
41+
42+
/** The `require` method of `com.auth0.jwt.JWT`. */
43+
class RequireMethod extends Method {
44+
RequireMethod() {
45+
this.getDeclaringType() instanceof Jwt and
46+
this.hasName("require")
47+
}
48+
}
49+
50+
/** The `sign` method of `com.auth0.jwt.JWTCreator.Builder`. */
51+
class SignTokenMethod extends Method {
52+
SignTokenMethod() {
53+
this.getDeclaringType() instanceof JwtBuilder and
54+
this.hasName("sign")
55+
}
56+
}
57+
58+
/** The `verify` method of `com.auth0.jwt.interfaces.JWTVerifier`. */
59+
class VerifyTokenMethod extends Method {
60+
VerifyTokenMethod() {
61+
this.getDeclaringType() instanceof JwtVerifier and
62+
this.hasName("verify")
63+
}
64+
}
65+
66+
/**
67+
* A data flow source for JWT token signing vulnerabilities.
68+
*/
69+
abstract class JwtKeySource extends DataFlow::Node { }
70+
71+
/**
72+
* A data flow sink for JWT token signing vulnerabilities.
73+
*/
74+
abstract class JwtTokenSink extends DataFlow::Node { }
75+
76+
/**
77+
* A hardcoded string literal as a source for JWT token signing vulnerabilities.
78+
*/
79+
class HardcodedKeyStringSource extends JwtKeySource {
80+
HardcodedKeyStringSource() { exists(this.asExpr().(CompileTimeConstantExpr).getStringValue()) }
81+
}
82+
83+
/**
84+
* An expression used to sign JWT tokens as a sink of JWT token signing vulnerabilities.
85+
*/
86+
private class SignTokenSink extends JwtTokenSink {
87+
SignTokenSink() {
88+
exists(MethodAccess ma |
89+
ma.getMethod() instanceof SignTokenMethod and
90+
this.asExpr() = ma.getArgument(0)
91+
)
92+
}
93+
}
94+
95+
/**
96+
* An expression used to verify JWT tokens as a sink of JWT token signing vulnerabilities.
97+
*/
98+
private class VerifyTokenSink extends JwtTokenSink {
99+
VerifyTokenSink() {
100+
exists(MethodAccess ma |
101+
ma.getMethod() instanceof VerifyTokenMethod and
102+
this.asExpr() = ma.getQualifier()
103+
)
104+
}
105+
}
106+
107+
/**
108+
* A configuration depicting taint flow for checking JWT token signing vulnerabilities.
109+
*/
110+
class HardcodedJwtKeyConfiguration extends TaintTracking::Configuration {
111+
HardcodedJwtKeyConfiguration() { this = "Hard-coded JWT Signing Key" }
112+
113+
override predicate isSource(DataFlow::Node source) { source instanceof JwtKeySource }
114+
115+
override predicate isSink(DataFlow::Node sink) { sink instanceof JwtTokenSink }
116+
117+
override predicate isAdditionalTaintStep(DataFlow::Node prev, DataFlow::Node succ) {
118+
exists(MethodAccess ma |
119+
(
120+
ma.getMethod() instanceof GetAlgorithmMethod or
121+
ma.getMethod() instanceof RequireMethod
122+
) and
123+
prev.asExpr() = ma.getArgument(0) and
124+
succ.asExpr() = ma
125+
)
126+
}
127+
}
128+
129+
/** Taint model related to verifying JWT tokens. */
130+
private class VerificationFlowStep extends SummaryModelCsv {
131+
override predicate row(string row) {
132+
row =
133+
[
134+
"com.auth0.jwt.interfaces;Verification;true;build;;;Argument[-1];ReturnValue;taint",
135+
"com.auth0.jwt.interfaces;Verification;true;" +
136+
["acceptLeeway", "acceptExpiresAt", "acceptNotBefore", "acceptIssuedAt", "ignoreIssuedAt"]
137+
+ ";;;Argument[-1];ReturnValue;value",
138+
"com.auth0.jwt.interfaces;Verification;true;with" +
139+
[
140+
"Issuer", "Subject", "Audience", "AnyOfAudience", "ClaimPresence", "Claim",
141+
"ArrayClaim", "JWTId"
142+
] + ";;;Argument[-1];ReturnValue;value"
143+
]
144+
}
145+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
edges
2+
| HardcodedJwtKey.java:15:33:15:38 | SECRET : String | HardcodedJwtKey.java:19:49:19:54 | SECRET : String |
3+
| HardcodedJwtKey.java:15:33:15:38 | SECRET : String | HardcodedJwtKey.java:42:62:42:67 | SECRET : String |
4+
| HardcodedJwtKey.java:15:42:15:59 | "hardcoded_secret" : String | HardcodedJwtKey.java:15:33:15:38 | SECRET : String |
5+
| HardcodedJwtKey.java:19:49:19:54 | SECRET : String | HardcodedJwtKey.java:25:23:25:31 | algorithm |
6+
| HardcodedJwtKey.java:42:32:42:69 | require(...) : Verification | HardcodedJwtKey.java:42:32:43:35 | withIssuer(...) : Verification |
7+
| HardcodedJwtKey.java:42:32:43:35 | withIssuer(...) : Verification | HardcodedJwtKey.java:42:32:44:24 | build(...) : JWTVerifier |
8+
| HardcodedJwtKey.java:42:32:44:24 | build(...) : JWTVerifier | HardcodedJwtKey.java:46:13:46:20 | verifier |
9+
| HardcodedJwtKey.java:42:62:42:67 | SECRET : String | HardcodedJwtKey.java:42:32:42:69 | require(...) : Verification |
10+
nodes
11+
| HardcodedJwtKey.java:15:33:15:38 | SECRET : String | semmle.label | SECRET : String |
12+
| HardcodedJwtKey.java:15:42:15:59 | "hardcoded_secret" : String | semmle.label | "hardcoded_secret" : String |
13+
| HardcodedJwtKey.java:19:49:19:54 | SECRET : String | semmle.label | SECRET : String |
14+
| HardcodedJwtKey.java:25:23:25:31 | algorithm | semmle.label | algorithm |
15+
| HardcodedJwtKey.java:42:32:42:69 | require(...) : Verification | semmle.label | require(...) : Verification |
16+
| HardcodedJwtKey.java:42:32:43:35 | withIssuer(...) : Verification | semmle.label | withIssuer(...) : Verification |
17+
| HardcodedJwtKey.java:42:32:44:24 | build(...) : JWTVerifier | semmle.label | build(...) : JWTVerifier |
18+
| HardcodedJwtKey.java:42:62:42:67 | SECRET : String | semmle.label | SECRET : String |
19+
| HardcodedJwtKey.java:46:13:46:20 | verifier | semmle.label | verifier |
20+
subpaths
21+
#select
22+
| HardcodedJwtKey.java:25:23:25:31 | algorithm | HardcodedJwtKey.java:15:42:15:59 | "hardcoded_secret" : String | HardcodedJwtKey.java:25:23:25:31 | algorithm | $@ is used to sign a JWT token. | HardcodedJwtKey.java:15:42:15:59 | "hardcoded_secret" | Hardcoded String |
23+
| HardcodedJwtKey.java:25:23:25:31 | algorithm | HardcodedJwtKey.java:19:49:19:54 | SECRET : String | HardcodedJwtKey.java:25:23:25:31 | algorithm | $@ is used to sign a JWT token. | HardcodedJwtKey.java:19:49:19:54 | SECRET | Hardcoded String |
24+
| HardcodedJwtKey.java:46:13:46:20 | verifier | HardcodedJwtKey.java:15:42:15:59 | "hardcoded_secret" : String | HardcodedJwtKey.java:46:13:46:20 | verifier | $@ is used to sign a JWT token. | HardcodedJwtKey.java:15:42:15:59 | "hardcoded_secret" | Hardcoded String |
25+
| HardcodedJwtKey.java:46:13:46:20 | verifier | HardcodedJwtKey.java:42:62:42:67 | SECRET : String | HardcodedJwtKey.java:46:13:46:20 | verifier | $@ is used to sign a JWT token. | HardcodedJwtKey.java:42:62:42:67 | SECRET | Hardcoded String |
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import java.util.Date;
2+
import java.util.Properties;
3+
4+
import com.auth0.jwt.JWT;
5+
import com.auth0.jwt.algorithms.Algorithm;
6+
import com.auth0.jwt.exceptions.JWTVerificationException;
7+
import com.auth0.jwt.interfaces.JWTVerifier;
8+
9+
public class HardcodedJwtKey {
10+
// 15 minutes
11+
private static final long ACCESS_EXPIRE_TIME = 1000 * 60 * 15;
12+
13+
private static final String ISSUER = "example_com";
14+
15+
private static final String SECRET = "hardcoded_secret";
16+
17+
// BAD: Get secret from hardcoded string then sign a JWT token
18+
public String accessTokenBad(String username) {
19+
Algorithm algorithm = Algorithm.HMAC256(SECRET);
20+
21+
return JWT.create()
22+
.withExpiresAt(new Date(new Date().getTime() + ACCESS_EXPIRE_TIME))
23+
.withIssuer(ISSUER)
24+
.withClaim("username", username)
25+
.sign(algorithm);
26+
}
27+
28+
// GOOD: Get secret from system configuration then sign a token
29+
public String accessTokenGood(String username) {
30+
String tokenSecret = System.getenv("SECRET_KEY");
31+
Algorithm algorithm = Algorithm.HMAC256(tokenSecret);
32+
33+
return JWT.create()
34+
.withExpiresAt(new Date(new Date().getTime() + ACCESS_EXPIRE_TIME))
35+
.withIssuer(ISSUER)
36+
.withClaim("username", username)
37+
.sign(algorithm);
38+
}
39+
40+
// BAD: Get secret from hardcoded string then verify a JWT token
41+
public boolean verifyTokenBad(String token) {
42+
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET))
43+
.withIssuer(ISSUER)
44+
.build();
45+
try {
46+
verifier.verify(token);
47+
return true;
48+
} catch (JWTVerificationException e) {
49+
return false;
50+
}
51+
}
52+
53+
// GOOD: Get secret from environment variable then verify a JWT token
54+
public boolean verifyTokenGood(String token) {
55+
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(System.getenv("SECRET_KEY")))
56+
.withIssuer(ISSUER)
57+
.build();
58+
try {
59+
verifier.verify(token);
60+
return true;
61+
} catch (JWTVerificationException e) {
62+
return false;
63+
}
64+
}
65+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
experimental/Security/CWE/CWE-321/HardcodedJwtKey.ql
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/auth0-jwt-2.3

java/ql/test/stubs/auth0-jwt-2.3/com/auth0/jwt/JWT.java

Lines changed: 57 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)