Skip to content

Commit ad9ea40

Browse files
authored
Merge pull request github#5597 from intrigus-lgtm/java/jwt-insecure-parse
[Java] JWT without signature check.
2 parents c406936 + a8865e2 commit ad9ea40

27 files changed

+1901
-0
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
public void badJwt(String token) {
2+
Jwts.parserBuilder()
3+
.setSigningKey("someBase64EncodedKey").build()
4+
.parse(token); // BAD: Does not verify the signature
5+
}
6+
7+
public void badJwtHandler(String token) {
8+
Jwts.parserBuilder()
9+
.setSigningKey("someBase64EncodedKey").build()
10+
.parse(plaintextJwt, new JwtHandlerAdapter<Jwt<Header, String>>() {
11+
@Override
12+
public Jwt<Header, String> onPlaintextJwt(Jwt<Header, String> jwt) {
13+
return jwt;
14+
}
15+
}); // BAD: The handler is called on an unverified JWT
16+
}
17+
18+
public void goodJwt(String token) {
19+
Jwts.parserBuilder()
20+
.setSigningKey("someBase64EncodedKey").build()
21+
.parseClaimsJws(token) // GOOD: Verify the signature
22+
.getBody();
23+
}
24+
25+
public void goodJwtHandler(String token) {
26+
Jwts.parserBuilder()
27+
.setSigningKey("someBase64EncodedKey").build()
28+
.parse(plaintextJwt, new JwtHandlerAdapter<Jws<String>>() {
29+
@Override
30+
public Jws<String> onPlaintextJws(Jws<String> jws) {
31+
return jws;
32+
}
33+
}); // GOOD: The handler is called on a verified JWS
34+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
2+
<qhelp>
3+
4+
<overview>
5+
<p> A JSON Web Token (JWT) consists of three parts: header, payload, and signature.
6+
The <code>io.jsonwebtoken.jjwt</code> library is one of many libraries used for working with JWTs.
7+
It offers different methods for parsing tokens like <code>parse</code>, <code>parseClaimsJws</code>, and <code>parsePlaintextJws</code>.
8+
The last two correctly verify that the JWT is properly signed.
9+
This is done by computing the signature of the combination of header and payload and
10+
comparing the locally computed signature with the signature part of the JWT.
11+
</p>
12+
<p>
13+
Therefore it is necessary to provide the <code>JwtParser</code> with a key that is used for signature validation.
14+
Unfortunately the <code>parse</code> method <b>accepts</b> a JWT whose signature is empty although a signing key has been set for the parser.
15+
This means that an attacker can create arbitrary JWTs that will be accepted if this method is used.
16+
</p>
17+
</overview>
18+
<recommendation>
19+
20+
<p>Always verify the signature by using either the <code>parseClaimsJws</code> and <code>parsePlaintextJws</code> methods or
21+
by overriding the <code>onPlaintextJws</code> or <code>onClaimsJws</code> of <code>JwtHandlerAdapter</code>.
22+
</p>
23+
24+
</recommendation>
25+
<example>
26+
27+
<p>The following example shows four cases where a signing key is set for a parser.
28+
In the first bad case the <code>parse</code> method is used which will not validate the signature.
29+
The second bad case uses a <code>JwtHandlerAdapter</code> where the <code>onPlaintextJwt</code> method is overriden so it will not validate the signature.
30+
The third and fourth good cases use <code>parseClaimsJws</code> method or override the <code>onPlaintextJws</code> method.
31+
</p>
32+
33+
<sample src="MissingJWTSignatureCheck.java" />
34+
35+
</example>
36+
<references>
37+
<li>zofrex: <a href="https://www.zofrex.com/blog/2020/10/20/alg-none-jwt-nhs-contact-tracing-app/">How I Found An alg=none JWT Vulnerability in the NHS Contact Tracing App</a>.</li>
38+
</references>
39+
</qhelp>
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/**
2+
* @name Missing JWT signature check
3+
* @description Not checking the JWT signature allows an attacker to forge their own tokens.
4+
* @kind problem
5+
* @problem.severity error
6+
* @precision high
7+
* @id java/missing-jwt-signature-check
8+
* @tags security
9+
* external/cwe/cwe-347
10+
*/
11+
12+
import java
13+
import semmle.code.java.dataflow.DataFlow
14+
15+
/** The interface `io.jsonwebtoken.JwtParser`. */
16+
class TypeJwtParser extends Interface {
17+
TypeJwtParser() { this.hasQualifiedName("io.jsonwebtoken", "JwtParser") }
18+
}
19+
20+
/** The interface `io.jsonwebtoken.JwtParser` or a type derived from it. */
21+
class TypeDerivedJwtParser extends RefType {
22+
TypeDerivedJwtParser() { this.getASourceSupertype*() instanceof TypeJwtParser }
23+
}
24+
25+
/** The interface `io.jsonwebtoken.JwtParserBuilder`. */
26+
class TypeJwtParserBuilder extends Interface {
27+
TypeJwtParserBuilder() { this.hasQualifiedName("io.jsonwebtoken", "JwtParserBuilder") }
28+
}
29+
30+
/** The interface `io.jsonwebtoken.JwtHandler`. */
31+
class TypeJwtHandler extends Interface {
32+
TypeJwtHandler() { this.hasQualifiedName("io.jsonwebtoken", "JwtHandler") }
33+
}
34+
35+
/** The class `io.jsonwebtoken.JwtHandlerAdapter`. */
36+
class TypeJwtHandlerAdapter extends Class {
37+
TypeJwtHandlerAdapter() { this.hasQualifiedName("io.jsonwebtoken", "JwtHandlerAdapter") }
38+
}
39+
40+
/** The `parse(token, handler)` method defined in `JwtParser`. */
41+
private class JwtParserParseHandlerMethod extends Method {
42+
JwtParserParseHandlerMethod() {
43+
this.hasName("parse") and
44+
this.getDeclaringType() instanceof TypeJwtParser and
45+
this.getNumberOfParameters() = 2
46+
}
47+
}
48+
49+
/** The `parse(token)`, `parseClaimsJwt(token)` and `parsePlaintextJwt(token)` methods defined in `JwtParser`. */
50+
private class JwtParserInsecureParseMethod extends Method {
51+
JwtParserInsecureParseMethod() {
52+
this.hasName(["parse", "parseClaimsJwt", "parsePlaintextJwt"]) and
53+
this.getNumberOfParameters() = 1 and
54+
this.getDeclaringType() instanceof TypeJwtParser
55+
}
56+
}
57+
58+
/** The `on(Claims|Plaintext)Jwt` methods defined in `JwtHandler`. */
59+
private class JwtHandlerOnJwtMethod extends Method {
60+
JwtHandlerOnJwtMethod() {
61+
this.hasName(["onClaimsJwt", "onPlaintextJwt"]) and
62+
this.getNumberOfParameters() = 1 and
63+
this.getDeclaringType() instanceof TypeJwtHandler
64+
}
65+
}
66+
67+
/** The `on(Claims|Plaintext)Jwt` methods defined in `JwtHandlerAdapter`. */
68+
private class JwtHandlerAdapterOnJwtMethod extends Method {
69+
JwtHandlerAdapterOnJwtMethod() {
70+
this.hasName(["onClaimsJwt", "onPlaintextJwt"]) and
71+
this.getNumberOfParameters() = 1 and
72+
this.getDeclaringType() instanceof TypeJwtHandlerAdapter
73+
}
74+
}
75+
76+
/**
77+
* Holds if `parseHandlerExpr` is an insecure `JwtHandler`.
78+
* That is, it overrides a method from `JwtHandlerOnJwtMethod` and the override is not defined on `JwtHandlerAdapter`.
79+
* `JwtHandlerAdapter`'s overrides are safe since they always throw an exception.
80+
*/
81+
private predicate isInsecureJwtHandler(Expr parseHandlerExpr) {
82+
exists(RefType t |
83+
parseHandlerExpr.getType() = t and
84+
t.getASourceSupertype*() instanceof TypeJwtHandler and
85+
exists(Method m |
86+
m = t.getAMethod() and
87+
m.getASourceOverriddenMethod+() instanceof JwtHandlerOnJwtMethod and
88+
not m.getSourceDeclaration() instanceof JwtHandlerAdapterOnJwtMethod
89+
)
90+
)
91+
}
92+
93+
/**
94+
* An access to an insecure parsing method.
95+
* That is, either a call to a `parse(token)`, `parseClaimsJwt(token)` or `parsePlaintextJwt(token)` method or
96+
* a call to a `parse(token, handler)` method where the `handler` is considered insecure.
97+
*/
98+
private class JwtParserInsecureParseMethodAccess extends MethodAccess {
99+
JwtParserInsecureParseMethodAccess() {
100+
this.getMethod().getASourceOverriddenMethod*() instanceof JwtParserInsecureParseMethod
101+
or
102+
this.getMethod().getASourceOverriddenMethod*() instanceof JwtParserParseHandlerMethod and
103+
isInsecureJwtHandler(this.getArgument(1))
104+
}
105+
}
106+
107+
/**
108+
* Holds if `signingMa` directly or indirectly sets a signing key for `expr`, which is a `JwtParser`.
109+
* The `setSigningKey` and `setSigningKeyResolver` methods set a signing key for a `JwtParser`.
110+
* Directly means code like this:
111+
* ```java
112+
* Jwts.parser().setSigningKey(key).parse(token);
113+
* ```
114+
* Here the signing key is set directly on a `JwtParser`.
115+
* Indirectly means code like this:
116+
* ```java
117+
* Jwts.parserBuilder().setSigningKey(key).build().parse(token);
118+
* ```
119+
* In this case, the signing key is set on a `JwtParserBuilder` indirectly setting the key of `JwtParser` that is created by the call to `build`.
120+
*/
121+
private predicate isSigningKeySetter(Expr expr, MethodAccess signingMa) {
122+
any(SigningToInsecureMethodAccessDataFlow s)
123+
.hasFlow(DataFlow::exprNode(signingMa), DataFlow::exprNode(expr))
124+
}
125+
126+
/**
127+
* An expr that is a (sub-type of) `JwtParser` for which a signing key has been set and which is used as
128+
* the qualifier to a `JwtParserInsecureParseMethodAccess`.
129+
*/
130+
private class JwtParserWithSigningKeyExpr extends Expr {
131+
MethodAccess signingMa;
132+
133+
JwtParserWithSigningKeyExpr() {
134+
this.getType() instanceof TypeDerivedJwtParser and
135+
isSigningKeySetter(this, signingMa)
136+
}
137+
138+
/** Gets the method access that sets the signing key for this parser. */
139+
MethodAccess getSigningMethodAccess() { result = signingMa }
140+
}
141+
142+
/**
143+
* Models flow from `SigningKeyMethodAccess`es to qualifiers of `JwtParserInsecureParseMethodAccess`es.
144+
* This is used to determine whether a `JwtParser` has a signing key set.
145+
*/
146+
private class SigningToInsecureMethodAccessDataFlow extends DataFlow::Configuration {
147+
SigningToInsecureMethodAccessDataFlow() { this = "SigningToExprDataFlow" }
148+
149+
override predicate isSource(DataFlow::Node source) {
150+
source.asExpr() instanceof SigningKeyMethodAccess
151+
}
152+
153+
override predicate isSink(DataFlow::Node sink) {
154+
any(JwtParserInsecureParseMethodAccess ma).getQualifier() = sink.asExpr()
155+
}
156+
157+
/** Models the builder style of `JwtParser` and `JwtParserBuilder`. */
158+
override predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
159+
(
160+
pred.asExpr().getType() instanceof TypeDerivedJwtParser or
161+
pred.asExpr().getType().(RefType).getASourceSupertype*() instanceof TypeJwtParserBuilder
162+
) and
163+
succ.asExpr().(MethodAccess).getQualifier() = pred.asExpr()
164+
}
165+
}
166+
167+
/** An access to the `setSigningKey` or `setSigningKeyResolver` method (or an overridden method) defined in `JwtParser` and `JwtParserBuilder`. */
168+
private class SigningKeyMethodAccess extends MethodAccess {
169+
SigningKeyMethodAccess() {
170+
exists(Method m |
171+
m.hasName(["setSigningKey", "setSigningKeyResolver"]) and
172+
m.getNumberOfParameters() = 1 and
173+
(
174+
m.getDeclaringType() instanceof TypeJwtParser or
175+
m.getDeclaringType() instanceof TypeJwtParserBuilder
176+
)
177+
|
178+
m = this.getMethod().getASourceOverriddenMethod*()
179+
)
180+
}
181+
}
182+
183+
/**
184+
* Holds if the `MethodAccess` `ma` occurs in a test file. A test file is any file that
185+
* is a direct or indirect child of a directory named `test`, ignoring case.
186+
*/
187+
private predicate isInTestFile(MethodAccess ma) {
188+
exists(string lowerCasedAbsolutePath |
189+
lowerCasedAbsolutePath = ma.getLocation().getFile().getAbsolutePath().toLowerCase()
190+
|
191+
lowerCasedAbsolutePath.matches("%/test/%") and
192+
not lowerCasedAbsolutePath
193+
.matches("%/ql/test/experimental/query-tests/security/CWE-347/%".toLowerCase())
194+
)
195+
}
196+
197+
from JwtParserInsecureParseMethodAccess ma, JwtParserWithSigningKeyExpr parserExpr
198+
where ma.getQualifier() = parserExpr and not isInTestFile(ma)
199+
select ma, "A signing key is set $@, but the signature is not verified.",
200+
parserExpr.getSigningMethodAccess(), "here"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
| MissingJWTSignatureCheck.java:96:9:96:27 | parse(...) | A signing key is set $@, but the signature is not verified. | MissingJWTSignatureCheck.java:18:16:18:66 | setSigningKey(...) | here |
2+
| MissingJWTSignatureCheck.java:96:9:96:27 | parse(...) | A signing key is set $@, but the signature is not verified. | MissingJWTSignatureCheck.java:22:16:22:73 | setSigningKey(...) | here |
3+
| MissingJWTSignatureCheck.java:96:9:96:27 | parse(...) | A signing key is set $@, but the signature is not verified. | MissingJWTSignatureCheck.java:26:16:26:75 | setSigningKey(...) | here |
4+
| MissingJWTSignatureCheck.java:100:9:105:22 | parse(...) | A signing key is set $@, but the signature is not verified. | MissingJWTSignatureCheck.java:18:16:18:66 | setSigningKey(...) | here |
5+
| MissingJWTSignatureCheck.java:100:9:105:22 | parse(...) | A signing key is set $@, but the signature is not verified. | MissingJWTSignatureCheck.java:22:16:22:73 | setSigningKey(...) | here |
6+
| MissingJWTSignatureCheck.java:100:9:105:22 | parse(...) | A signing key is set $@, but the signature is not verified. | MissingJWTSignatureCheck.java:26:16:26:75 | setSigningKey(...) | here |
7+
| MissingJWTSignatureCheck.java:127:9:129:33 | parse(...) | A signing key is set $@, but the signature is not verified. | MissingJWTSignatureCheck.java:127:9:128:58 | setSigningKey(...) | here |
8+
| MissingJWTSignatureCheck.java:133:9:140:22 | parse(...) | A signing key is set $@, but the signature is not verified. | MissingJWTSignatureCheck.java:133:9:134:58 | setSigningKey(...) | here |

0 commit comments

Comments
 (0)