Skip to content

Commit 15989ce

Browse files
authored
Merge pull request #14089 from am0o0/amammad-java-JWT
Java: JWT decoding without verification
2 parents a1a6fe4 + b001c24 commit 15989ce

File tree

28 files changed

+760
-0
lines changed

28 files changed

+760
-0
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
extensions:
2+
- addsTo:
3+
pack: codeql/java-all
4+
extensible: sourceModel
5+
data:
6+
- ["org.apache.shiro.authc","AuthenticationToken",true,"getCredentials","()","","ReturnValue","remote","manual"]
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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. It must be verified in order to ensure the JWT is genuine.
6+
</p>
7+
8+
</overview>
9+
<recommendation>
10+
11+
<p>
12+
Don't use information from a JWT without verifying that JWT.
13+
</p>
14+
15+
</recommendation>
16+
<example>
17+
18+
<p>
19+
The following example illustrates secure and insecure use of the Auth0 `java-jwt` library.
20+
</p>
21+
22+
<sample src="Example.java" />
23+
24+
</example>
25+
<references>
26+
<li>
27+
<a href="https://nvd.nist.gov/vuln/detail/CVE-2021-37580">The incorrect use of JWT in ShenyuAdminBootstrap allows an attacker to bypass authentication.</a>
28+
</li>
29+
</references>
30+
31+
</qhelp>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* @name Missing JWT signature check
3+
* @description Failing to check the Json Web Token (JWT) signature may allow an attacker to forge their own tokens.
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @security-severity 7.8
7+
* @precision high
8+
* @id java/missing-jwt-signature-check-auth0
9+
* @tags security
10+
* external/cwe/cwe-347
11+
*/
12+
13+
import java
14+
import semmle.code.java.dataflow.FlowSources
15+
import JwtAuth0 as JwtAuth0
16+
17+
module JwtDecodeConfig implements DataFlow::ConfigSig {
18+
predicate isSource(DataFlow::Node source) {
19+
source instanceof RemoteFlowSource and
20+
not FlowToJwtVerify::flow(source, _)
21+
}
22+
23+
predicate isSink(DataFlow::Node sink) { sink.asExpr() = any(JwtAuth0::GetPayload a) }
24+
25+
predicate isAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
26+
// Decode Should be one of the middle nodes
27+
exists(JwtAuth0::Decode a |
28+
nodeFrom.asExpr() = a.getArgument(0) and
29+
nodeTo.asExpr() = a
30+
)
31+
or
32+
exists(JwtAuth0::Verify a |
33+
nodeFrom.asExpr() = a.getArgument(0) and
34+
nodeTo.asExpr() = a
35+
)
36+
or
37+
exists(JwtAuth0::GetPayload a |
38+
nodeFrom.asExpr() = a.getQualifier() and
39+
nodeTo.asExpr() = a
40+
)
41+
}
42+
}
43+
44+
module FlowToJwtVerifyConfig implements DataFlow::ConfigSig {
45+
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
46+
47+
predicate isSink(DataFlow::Node sink) { sink.asExpr() = any(JwtAuth0::Verify a).getArgument(0) }
48+
}
49+
50+
module JwtDecode = TaintTracking::Global<JwtDecodeConfig>;
51+
52+
module FlowToJwtVerify = TaintTracking::Global<FlowToJwtVerifyConfig>;
53+
54+
import JwtDecode::PathGraph
55+
56+
from JwtDecode::PathNode source, JwtDecode::PathNode sink
57+
where JwtDecode::flowPath(source, sink)
58+
select sink.getNode(), source, sink, "This parses a $@, but the signature is not verified.",
59+
source.getNode(), "JWT"
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package com.example.JwtTest;
2+
3+
import java.io.*;
4+
import java.security.NoSuchAlgorithmException;
5+
import java.util.Objects;
6+
import java.util.Optional;
7+
import javax.crypto.KeyGenerator;
8+
import javax.servlet.http.*;
9+
import javax.servlet.annotation.*;
10+
import com.auth0.jwt.JWT;
11+
import com.auth0.jwt.JWTVerifier;
12+
import com.auth0.jwt.algorithms.Algorithm;
13+
import com.auth0.jwt.exceptions.JWTCreationException;
14+
import com.auth0.jwt.exceptions.JWTVerificationException;
15+
import com.auth0.jwt.interfaces.DecodedJWT;
16+
17+
@WebServlet(name = "JwtTest1", value = "/Auth")
18+
public class auth0 extends HttpServlet {
19+
20+
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
21+
response.setContentType("text/html");
22+
PrintWriter out = response.getWriter();
23+
24+
// OK: first decode without signature verification
25+
// and then verify with signature verification
26+
String JwtToken1 = request.getParameter("JWT1");
27+
String userName = decodeToken(JwtToken1);
28+
verifyToken(JwtToken1, "A Securely generated Key");
29+
if (Objects.equals(userName, "Admin")) {
30+
out.println("<html><body>");
31+
out.println("<h1>" + "heyyy Admin" + "</h1>");
32+
out.println("</body></html>");
33+
}
34+
35+
out.println("<html><body>");
36+
out.println("<h1>" + "heyyy Nobody" + "</h1>");
37+
out.println("</body></html>");
38+
}
39+
40+
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
41+
response.setContentType("text/html");
42+
PrintWriter out = response.getWriter();
43+
44+
// NOT OK: only decode, no verification
45+
String JwtToken2 = request.getParameter("JWT2");
46+
String userName = decodeToken(JwtToken2);
47+
if (Objects.equals(userName, "Admin")) {
48+
out.println("<html><body>");
49+
out.println("<h1>" + "heyyy Admin" + "</h1>");
50+
out.println("</body></html>");
51+
}
52+
53+
// OK: no clue of the use of unsafe decoded JWT return value
54+
JwtToken2 = request.getParameter("JWT2");
55+
JWT.decode(JwtToken2);
56+
57+
58+
out.println("<html><body>");
59+
out.println("<h1>" + "heyyy Nobody" + "</h1>");
60+
out.println("</body></html>");
61+
}
62+
63+
public static boolean verifyToken(final String token, final String key) {
64+
try {
65+
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(key)).build();
66+
verifier.verify(token);
67+
return true;
68+
} catch (JWTVerificationException e) {
69+
System.out.printf("jwt decode fail, token: %s", e);
70+
}
71+
return false;
72+
}
73+
74+
75+
public static String decodeToken(final String token) {
76+
DecodedJWT jwt = JWT.decode(token);
77+
return Optional.of(jwt).map(item -> item.getClaim("userName").asString()).orElse("");
78+
}
79+
80+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import java
2+
3+
class PayloadType extends RefType {
4+
PayloadType() { this.hasQualifiedName("com.auth0.jwt.interfaces", "Payload") }
5+
}
6+
7+
class JwtType extends RefType {
8+
JwtType() { this.hasQualifiedName("com.auth0.jwt", "JWT") }
9+
}
10+
11+
class JwtVerifierType extends RefType {
12+
JwtVerifierType() { this.hasQualifiedName("com.auth0.jwt", "JWTVerifier") }
13+
}
14+
15+
/**
16+
* A Method that returns a Decoded Claim of JWT
17+
*/
18+
class GetPayload extends MethodCall {
19+
GetPayload() {
20+
this.getCallee().getDeclaringType() instanceof PayloadType and
21+
this.getCallee().hasName(["getClaim", "getIssuedAt"])
22+
}
23+
}
24+
25+
/**
26+
* A Method that Decode JWT without signature verification
27+
*/
28+
class Decode extends MethodCall {
29+
Decode() {
30+
this.getCallee().getDeclaringType() instanceof JwtType and
31+
this.getCallee().hasName("decode")
32+
}
33+
}
34+
35+
/**
36+
* A Method that Decode JWT with signature verification
37+
*/
38+
class Verify extends MethodCall {
39+
Verify() {
40+
this.getCallee().getDeclaringType() instanceof JwtVerifierType and
41+
this.getCallee().hasName("verify")
42+
}
43+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#select
2+
| JwtNoVerifier.java:91:45:91:69 | getClaim(...) | JwtNoVerifier.java:44:28:44:55 | getParameter(...) : String | JwtNoVerifier.java:91:45:91:69 | getClaim(...) | This parses a $@, but the signature is not verified. | JwtNoVerifier.java:44:28:44:55 | getParameter(...) | JWT |
3+
| JwtNoVerifier.java:91:45:91:69 | getClaim(...) | JwtNoVerifier.java:58:37:58:62 | getCredentials(...) : Object | JwtNoVerifier.java:91:45:91:69 | getClaim(...) | This parses a $@, but the signature is not verified. | JwtNoVerifier.java:58:37:58:62 | getCredentials(...) | JWT |
4+
edges
5+
| JwtNoVerifier.java:44:28:44:55 | getParameter(...) : String | JwtNoVerifier.java:45:39:45:47 | JwtToken1 : String | provenance | Src:MaD:4 |
6+
| JwtNoVerifier.java:45:39:45:47 | JwtToken1 : String | JwtNoVerifier.java:89:38:89:55 | token : String | provenance | |
7+
| JwtNoVerifier.java:58:28:58:62 | (...)... : String | JwtNoVerifier.java:59:32:59:40 | JwtToken3 : String | provenance | |
8+
| JwtNoVerifier.java:58:37:58:62 | getCredentials(...) : Object | JwtNoVerifier.java:58:28:58:62 | (...)... : String | provenance | Src:MaD:1 |
9+
| JwtNoVerifier.java:59:32:59:40 | JwtToken3 : String | JwtNoVerifier.java:89:38:89:55 | token : String | provenance | |
10+
| JwtNoVerifier.java:89:38:89:55 | token : String | JwtNoVerifier.java:90:37:90:41 | token : String | provenance | |
11+
| JwtNoVerifier.java:90:26:90:42 | decode(...) : DecodedJWT | JwtNoVerifier.java:91:28:91:30 | jwt : DecodedJWT | provenance | |
12+
| JwtNoVerifier.java:90:37:90:41 | token : String | JwtNoVerifier.java:90:26:90:42 | decode(...) : DecodedJWT | provenance | Config |
13+
| JwtNoVerifier.java:91:16:91:31 | of(...) : Optional [<element>] : DecodedJWT | JwtNoVerifier.java:91:37:91:40 | item : DecodedJWT | provenance | MaD:2 |
14+
| JwtNoVerifier.java:91:28:91:30 | jwt : DecodedJWT | JwtNoVerifier.java:91:16:91:31 | of(...) : Optional [<element>] : DecodedJWT | provenance | MaD:3 |
15+
| JwtNoVerifier.java:91:37:91:40 | item : DecodedJWT | JwtNoVerifier.java:91:45:91:48 | item : DecodedJWT | provenance | |
16+
| JwtNoVerifier.java:91:45:91:48 | item : DecodedJWT | JwtNoVerifier.java:91:45:91:69 | getClaim(...) | provenance | Config |
17+
models
18+
| 1 | Source: org.apache.shiro.authc; AuthenticationToken; true; getCredentials; (); ; ReturnValue; remote; manual |
19+
| 2 | Summary: java.util; Optional; false; map; ; ; Argument[this].Element; Argument[0].Parameter[0]; value; manual |
20+
| 3 | Summary: java.util; Optional; false; of; ; ; Argument[0]; ReturnValue.Element; value; manual |
21+
| 4 | Source: javax.servlet; ServletRequest; false; getParameter; (String); ; ReturnValue; remote; manual |
22+
nodes
23+
| JwtNoVerifier.java:44:28:44:55 | getParameter(...) : String | semmle.label | getParameter(...) : String |
24+
| JwtNoVerifier.java:45:39:45:47 | JwtToken1 : String | semmle.label | JwtToken1 : String |
25+
| JwtNoVerifier.java:58:28:58:62 | (...)... : String | semmle.label | (...)... : String |
26+
| JwtNoVerifier.java:58:37:58:62 | getCredentials(...) : Object | semmle.label | getCredentials(...) : Object |
27+
| JwtNoVerifier.java:59:32:59:40 | JwtToken3 : String | semmle.label | JwtToken3 : String |
28+
| JwtNoVerifier.java:89:38:89:55 | token : String | semmle.label | token : String |
29+
| JwtNoVerifier.java:90:26:90:42 | decode(...) : DecodedJWT | semmle.label | decode(...) : DecodedJWT |
30+
| JwtNoVerifier.java:90:37:90:41 | token : String | semmle.label | token : String |
31+
| JwtNoVerifier.java:91:16:91:31 | of(...) : Optional [<element>] : DecodedJWT | semmle.label | of(...) : Optional [<element>] : DecodedJWT |
32+
| JwtNoVerifier.java:91:28:91:30 | jwt : DecodedJWT | semmle.label | jwt : DecodedJWT |
33+
| JwtNoVerifier.java:91:37:91:40 | item : DecodedJWT | semmle.label | item : DecodedJWT |
34+
| JwtNoVerifier.java:91:45:91:48 | item : DecodedJWT | semmle.label | item : DecodedJWT |
35+
| JwtNoVerifier.java:91:45:91:69 | getClaim(...) | semmle.label | getClaim(...) |
36+
subpaths
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
query: experimental/Security/CWE/CWE-347/Auth0NoVerifier.ql
2+
postprocess: TestUtilities/PrettyPrintModels.ql

0 commit comments

Comments
 (0)