Skip to content

Commit 937ab41

Browse files
committed
Query to detect hardcoded JWT secret keys
1 parent 8e33653 commit 937ab41

22 files changed

+1444
-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/#:~:text=The%20most%20basic%20mistake%20is,considered%20a%20software%20anti%2Dpattern.">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: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/**
2+
* Provides sources and sinks for detecting JWT token signing vulnerabilities.
3+
*/
4+
5+
import java
6+
private import semmle.code.java.dataflow.FlowSources
7+
8+
/** The Java class `com.auth0.jwt.JWT`. */
9+
class Jwt extends RefType {
10+
Jwt() { this.hasQualifiedName("com.auth0.jwt", "JWT") }
11+
}
12+
13+
/** The Java class `com.auth0.jwt.JWTCreator.Builder`. */
14+
class JwtBuilder extends RefType {
15+
JwtBuilder() { this.hasQualifiedName("com.auth0.jwt", "JWTCreator$Builder") }
16+
}
17+
18+
/** The Java class `com.auth0.jwt.algorithms.Algorithm`. */
19+
class Algorithm extends RefType {
20+
Algorithm() { this.hasQualifiedName("com.auth0.jwt.algorithms", "Algorithm") }
21+
}
22+
23+
/**
24+
* The Java interface `com.auth0.jwt.interfaces.JWTVerifier` or it implementation class
25+
* `com.auth0.jwt.JWTVerifier`.
26+
*/
27+
class JwtVerifier extends RefType {
28+
JwtVerifier() {
29+
this.hasQualifiedName(["com.auth0.jwt", "com.auth0.jwt.interfaces"], "JWTVerifier")
30+
}
31+
}
32+
33+
/** The secret generation method declared in `com.auth0.jwt.algorithms.Algorithm`. */
34+
class GetSecretMethod extends Method {
35+
GetSecretMethod() {
36+
this.getDeclaringType() instanceof Algorithm and
37+
(
38+
this.getName().substring(0, 4) = "HMAC" or
39+
this.getName().substring(0, 5) = "ECDSA" or
40+
this.getName().substring(0, 3) = "RSA"
41+
)
42+
}
43+
}
44+
45+
/** The `require` method of `com.auth0.jwt.JWT`. */
46+
class RequireMethod extends Method {
47+
RequireMethod() {
48+
this.getDeclaringType() instanceof Jwt and
49+
this.hasName("require")
50+
}
51+
}
52+
53+
/** The `sign` method of `com.auth0.jwt.JWTCreator.Builder`. */
54+
class SignTokenMethod extends Method {
55+
SignTokenMethod() {
56+
this.getDeclaringType() instanceof JwtBuilder and
57+
this.hasName("sign")
58+
}
59+
}
60+
61+
/** The `verify` method of `com.auth0.jwt.interfaces.JWTVerifier`. */
62+
class VerifyTokenMethod extends Method {
63+
VerifyTokenMethod() {
64+
this.getDeclaringType() instanceof JwtVerifier and
65+
this.hasName("verify")
66+
}
67+
}
68+
69+
/**
70+
* A data flow source for JWT token signing vulnerabilities.
71+
*/
72+
abstract class JwtKeySource extends DataFlow::Node { }
73+
74+
/**
75+
* A data flow sink for JWT token signing vulnerabilities.
76+
*/
77+
abstract class JwtTokenSink extends DataFlow::Node { }
78+
79+
private predicate isTestCode(Expr e) {
80+
e.getFile().getAbsolutePath().toLowerCase().matches("%test%") and
81+
not e.getFile().getAbsolutePath().toLowerCase().matches("%ql/test%")
82+
}
83+
84+
/**
85+
* A hardcoded string literal as a source for JWT token signing vulnerabilities.
86+
*/
87+
class HardcodedKeyStringSource extends JwtKeySource {
88+
HardcodedKeyStringSource() {
89+
this.asExpr() instanceof CompileTimeConstantExpr and
90+
not isTestCode(this.asExpr())
91+
}
92+
}
93+
94+
/**
95+
* An expression used to sign JWT tokens as a sink of JWT token signing vulnerabilities.
96+
*/
97+
private class SignTokenSink extends JwtTokenSink {
98+
SignTokenSink() {
99+
exists(MethodAccess ma |
100+
ma.getMethod() instanceof SignTokenMethod and
101+
this.asExpr() = ma.getArgument(0)
102+
)
103+
}
104+
}
105+
106+
/**
107+
* An expression used to verify JWT tokens as a sink of JWT token signing vulnerabilities.
108+
*/
109+
private class VerifyTokenSink extends JwtTokenSink {
110+
VerifyTokenSink() {
111+
exists(MethodAccess ma |
112+
ma.getMethod() instanceof VerifyTokenMethod and
113+
this.asExpr() = ma.getQualifier()
114+
)
115+
}
116+
}
117+
118+
/**
119+
* A configuration depicting taint flow for checking JWT token signing vulnerabilities.
120+
*/
121+
class HardcodedJwtKeyConfiguration extends TaintTracking::Configuration {
122+
HardcodedJwtKeyConfiguration() { this = "Hard-coded JWT Signing Key" }
123+
124+
override predicate isSource(DataFlow::Node source) { source instanceof JwtKeySource }
125+
126+
override predicate isSink(DataFlow::Node sink) { sink instanceof JwtTokenSink }
127+
128+
override predicate isAdditionalTaintStep(DataFlow::Node prev, DataFlow::Node succ) {
129+
exists(MethodAccess ma |
130+
(
131+
ma.getMethod() instanceof GetSecretMethod or
132+
ma.getMethod() instanceof RequireMethod
133+
) and
134+
prev.asExpr() = ma.getArgument(0) and
135+
succ.asExpr() = ma
136+
)
137+
}
138+
}
139+
140+
/** Taint model related to verifying JWT tokens. */
141+
private class VerificationFlowStep extends SummaryModelCsv {
142+
override predicate row(string row) {
143+
row =
144+
[
145+
"com.auth0.jwt.interfaces;Verification;true;build;;;Argument[-1];ReturnValue;taint",
146+
"com.auth0.jwt.interfaces;Verification;true;" +
147+
["acceptLeeway", "acceptExpiresAt", "acceptNotBefore", "acceptIssuedAt", "ignoreIssuedAt"]
148+
+ ";;;Argument[-1];ReturnValue;taint",
149+
"com.auth0.jwt.interfaces;Verification;true;with" +
150+
[
151+
"Issuer", "Subject", "Audience", "AnyOfAudience", "ClaimPresence", "Claim",
152+
"ArrayClaim", "JWTId"
153+
] + ";;;Argument[-1];ReturnValue;taint"
154+
]
155+
}
156+
}
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

0 commit comments

Comments
 (0)