-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Java: JWT decoding without verification [smowton fork] #17020
Closed
Closed
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
ccfbedc
V1
am0o0 e9b3fab
update example to include more logical vulnerable pattern, add docume…
am0o0 c8e3ea4
stash
am0o0 6447b62
finilize tests
am0o0 eb94b9f
remove redundent stubs
am0o0 3096ea3
Make test compatible with Servlet 2.5; use old Servlet stubs
smowton a207503
Adjust test expectations
smowton File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
34 changes: 34 additions & 0 deletions
34
java/ql/src/experimental/Security/CWE/CWE-347/Auth0NoVerifier.qhelp
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd"> | ||
<qhelp> | ||
<overview> | ||
<p> | ||
A JSON Web Token (JWT) is used for authenticating and managing users in an application. | ||
</p> | ||
<p> | ||
Only Decoding JWTs without checking if they have a valid signature or not can lead to security vulnerabilities. | ||
</p> | ||
|
||
</overview> | ||
<recommendation> | ||
|
||
<p> | ||
Don't use methods that only decode JWT, Instead use methods that verify the signature of JWT. | ||
</p> | ||
|
||
</recommendation> | ||
<example> | ||
|
||
<p> | ||
The following code you can see an Example from a popular Library. | ||
</p> | ||
|
||
<sample src="Example.java" /> | ||
|
||
</example> | ||
<references> | ||
<li> | ||
<a href="CVE-2021-37580">The incorrect use of JWT in ShenyuAdminBootstrap allows an attacker to bypass authentication.</a> | ||
</li> | ||
</references> | ||
|
||
</qhelp> |
136 changes: 136 additions & 0 deletions
136
java/ql/src/experimental/Security/CWE/CWE-347/Auth0NoVerifier.ql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
/** | ||
* @name Missing JWT signature check | ||
* @description Failing to check the Json Web Token (JWT) signature may allow an attacker to forge their own tokens. | ||
* @kind path-problem | ||
* @problem.severity error | ||
* @security-severity 7.8 | ||
* @precision high | ||
* @id java/missing-jwt-signature-check | ||
* @tags security | ||
* external/cwe/cwe-347 | ||
*/ | ||
|
||
import java | ||
import semmle.code.java.dataflow.DataFlow | ||
import semmle.code.java.dataflow.FlowSources | ||
|
||
module JwtAuth0 { | ||
class PayloadType extends RefType { | ||
PayloadType() { this.hasQualifiedName("com.auth0.jwt.interfaces", "Payload") } | ||
} | ||
|
||
class JWTType extends RefType { | ||
Check warning Code scanning / CodeQL Acronyms should be PascalCase/camelCase. Warning
Acronyms in JWTType should be PascalCase/camelCase.
|
||
JWTType() { this.hasQualifiedName("com.auth0.jwt", "JWT") } | ||
} | ||
|
||
class JWTVerifierType extends RefType { | ||
Check warning Code scanning / CodeQL Acronyms should be PascalCase/camelCase. Warning
Acronyms in JWTVerifierType should be PascalCase/camelCase.
|
||
JWTVerifierType() { this.hasQualifiedName("com.auth0.jwt", "JWTVerifier") } | ||
} | ||
|
||
/** | ||
* A Method that returns a Decoded Claim of JWT | ||
*/ | ||
class GetPayload extends MethodAccess { | ||
GetPayload() { | ||
this.getCallee().getDeclaringType() instanceof PayloadType and | ||
this.getCallee().hasName(["getClaim", "getIssuedAt"]) | ||
} | ||
} | ||
|
||
/** | ||
* A Method that Decode JWT without signature verification | ||
*/ | ||
class Decode extends MethodAccess { | ||
Decode() { | ||
this.getCallee().getDeclaringType() instanceof JWTType and | ||
this.getCallee().hasName("decode") | ||
} | ||
} | ||
|
||
/** | ||
* A Method that Decode JWT with signature verification | ||
*/ | ||
class Verify extends MethodAccess { | ||
Verify() { | ||
this.getCallee().getDeclaringType() instanceof JWTVerifierType and | ||
this.getCallee().hasName("verify") | ||
} | ||
} | ||
} | ||
|
||
module JwtDecodeConfig implements DataFlow::StateConfigSig { | ||
class FlowState = DataFlow::FlowState; | ||
|
||
predicate isSource(DataFlow::Node source, FlowState state) { | ||
( | ||
exists(Variable v | | ||
source.asExpr() = v.getInitializer() and | ||
v.getType().hasName("String") | ||
) | ||
or | ||
source instanceof RemoteFlowSource | ||
) and | ||
not FlowToJwtVerify::flow(source, _) and | ||
state = "Auth0" and | ||
not state = "Auth0Verify" | ||
} | ||
|
||
predicate isSink(DataFlow::Node sink, FlowState state) { | ||
sink.asExpr() = any(JwtAuth0::GetPayload a) and | ||
state = "Auth0" and | ||
not state = "Auth0Verify" | ||
} | ||
|
||
predicate isAdditionalFlowStep( | ||
DataFlow::Node nodeFrom, FlowState stateFrom, DataFlow::Node nodeTo, FlowState stateTo | ||
) { | ||
// Decode Should be one of the middle nodes | ||
exists(JwtAuth0::Decode a | | ||
nodeFrom.asExpr() = a.getArgument(0) and | ||
nodeTo.asExpr() = a and | ||
stateTo = "Auth0" and | ||
stateFrom = "Auth0" | ||
) | ||
or | ||
exists(JwtAuth0::Verify a | | ||
nodeFrom.asExpr() = a.getArgument(0) and | ||
nodeTo.asExpr() = a and | ||
stateTo = "Auth0Verify" and | ||
stateFrom = "Auth0Verify" | ||
) | ||
or | ||
exists(JwtAuth0::GetPayload a | | ||
nodeFrom.asExpr() = a.getQualifier() and | ||
nodeTo.asExpr() = a and | ||
stateTo = "Auth0" and | ||
stateFrom = "Auth0" | ||
) | ||
} | ||
|
||
predicate isBarrier(DataFlow::Node sanitizer, FlowState state) { none() } | ||
} | ||
|
||
module FlowToJwtVerifyConfig implements DataFlow::ConfigSig { | ||
predicate isSource(DataFlow::Node source) { | ||
// source instanceof DataFlow::Node | ||
exists(Variable v | | ||
source.asExpr() = v.getInitializer() and | ||
v.getType().hasName("String") | ||
) | ||
} | ||
|
||
predicate isSink(DataFlow::Node sink) { sink.asExpr() = any(JwtAuth0::Verify a).getArgument(0) } | ||
|
||
predicate isAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { none() } | ||
} | ||
|
||
module JwtDecode = TaintTracking::GlobalWithState<JwtDecodeConfig>; | ||
|
||
module FlowToJwtVerify = TaintTracking::Global<FlowToJwtVerifyConfig>; | ||
|
||
import JwtDecode::PathGraph | ||
|
||
from JwtDecode::PathNode source, JwtDecode::PathNode sink | ||
where JwtDecode::flowPath(source, sink) | ||
select sink.getNode(), source, sink, "This parses a $@, but the signature is not verified.", | ||
source.getNode(), "JWT" |
80 changes: 80 additions & 0 deletions
80
java/ql/src/experimental/Security/CWE/CWE-347/Example.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
package com.example.JwtTest; | ||
|
||
import java.io.*; | ||
import java.security.NoSuchAlgorithmException; | ||
import java.util.Objects; | ||
import java.util.Optional; | ||
import javax.crypto.KeyGenerator; | ||
import javax.servlet.http.*; | ||
import javax.servlet.annotation.*; | ||
import com.auth0.jwt.JWT; | ||
import com.auth0.jwt.JWTVerifier; | ||
import com.auth0.jwt.algorithms.Algorithm; | ||
import com.auth0.jwt.exceptions.JWTCreationException; | ||
import com.auth0.jwt.exceptions.JWTVerificationException; | ||
import com.auth0.jwt.interfaces.DecodedJWT; | ||
|
||
@WebServlet(name = "JwtTest1", value = "/Auth") | ||
public class auth0 extends HttpServlet { | ||
|
||
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { | ||
response.setContentType("text/html"); | ||
PrintWriter out = response.getWriter(); | ||
|
||
// OK: first decode without signature verification | ||
// and then verify with signature verification | ||
String JwtToken1 = request.getParameter("JWT1"); | ||
String userName = decodeToken(JwtToken1); | ||
verifyToken(JwtToken1, "A Securely generated Key"); | ||
if (Objects.equals(userName, "Admin")) { | ||
out.println("<html><body>"); | ||
out.println("<h1>" + "heyyy Admin" + "</h1>"); | ||
out.println("</body></html>"); | ||
} | ||
|
||
out.println("<html><body>"); | ||
out.println("<h1>" + "heyyy Nobody" + "</h1>"); | ||
out.println("</body></html>"); | ||
} | ||
|
||
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { | ||
response.setContentType("text/html"); | ||
PrintWriter out = response.getWriter(); | ||
|
||
// NOT OK: only decode, no verification | ||
String JwtToken2 = request.getParameter("JWT2"); | ||
String userName = decodeToken(JwtToken2); | ||
if (Objects.equals(userName, "Admin")) { | ||
out.println("<html><body>"); | ||
out.println("<h1>" + "heyyy Admin" + "</h1>"); | ||
out.println("</body></html>"); | ||
} | ||
|
||
// OK: no clue of the use of unsafe decoded JWT return value | ||
JwtToken2 = request.getParameter("JWT2"); | ||
JWT.decode(JwtToken2); | ||
|
||
|
||
out.println("<html><body>"); | ||
out.println("<h1>" + "heyyy Nobody" + "</h1>"); | ||
out.println("</body></html>"); | ||
} | ||
|
||
public static boolean verifyToken(final String token, final String key) { | ||
try { | ||
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(key)).build(); | ||
verifier.verify(token); | ||
return true; | ||
} catch (JWTVerificationException e) { | ||
System.out.printf("jwt decode fail, token: %s", e); | ||
} | ||
return false; | ||
} | ||
|
||
|
||
public static String decodeToken(final String token) { | ||
DecodedJWT jwt = JWT.decode(token); | ||
return Optional.of(jwt).map(item -> item.getClaim("userName").asString()).orElse(""); | ||
} | ||
|
||
} |
28 changes: 28 additions & 0 deletions
28
java/ql/test/experimental/query-tests/security/CWE-347/Auth0NoVerifier.expected
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
WARNING: type 'FlowState' has been deprecated and may be removed in future (Auth0NoVerifier.ql:62,21-40) | ||
WARNING: type 'MethodAccess' has been deprecated and may be removed in future (Auth0NoVerifier.ql:33,28-40) | ||
WARNING: type 'MethodAccess' has been deprecated and may be removed in future (Auth0NoVerifier.ql:43,24-36) | ||
WARNING: type 'MethodAccess' has been deprecated and may be removed in future (Auth0NoVerifier.ql:53,24-36) | ||
edges | ||
| JwtNoVerifier.java:42:28:42:55 | getParameter(...) : String | JwtNoVerifier.java:43:39:43:47 | JwtToken2 : String | provenance | Src:MaD:44684 | | ||
| JwtNoVerifier.java:43:39:43:47 | JwtToken2 : String | JwtNoVerifier.java:72:38:72:55 | token : String | provenance | | | ||
| JwtNoVerifier.java:72:38:72:55 | token : String | JwtNoVerifier.java:73:37:73:41 | token : String | provenance | | | ||
| JwtNoVerifier.java:73:26:73:42 | decode(...) : DecodedJWT | JwtNoVerifier.java:74:28:74:30 | jwt : DecodedJWT | provenance | | | ||
| JwtNoVerifier.java:73:37:73:41 | token : String | JwtNoVerifier.java:73:26:73:42 | decode(...) : DecodedJWT | provenance | Config | | ||
| JwtNoVerifier.java:74:16:74:31 | of(...) : Optional [<element>] : DecodedJWT | JwtNoVerifier.java:74:37:74:40 | item : DecodedJWT | provenance | MaD:43977 | | ||
| JwtNoVerifier.java:74:28:74:30 | jwt : DecodedJWT | JwtNoVerifier.java:74:16:74:31 | of(...) : Optional [<element>] : DecodedJWT | provenance | MaD:43979 | | ||
| JwtNoVerifier.java:74:37:74:40 | item : DecodedJWT | JwtNoVerifier.java:74:45:74:48 | item : DecodedJWT | provenance | | | ||
| JwtNoVerifier.java:74:45:74:48 | item : DecodedJWT | JwtNoVerifier.java:74:45:74:69 | getClaim(...) | provenance | Config | | ||
nodes | ||
| JwtNoVerifier.java:42:28:42:55 | getParameter(...) : String | semmle.label | getParameter(...) : String | | ||
| JwtNoVerifier.java:43:39:43:47 | JwtToken2 : String | semmle.label | JwtToken2 : String | | ||
| JwtNoVerifier.java:72:38:72:55 | token : String | semmle.label | token : String | | ||
| JwtNoVerifier.java:73:26:73:42 | decode(...) : DecodedJWT | semmle.label | decode(...) : DecodedJWT | | ||
| JwtNoVerifier.java:73:37:73:41 | token : String | semmle.label | token : String | | ||
| JwtNoVerifier.java:74:16:74:31 | of(...) : Optional [<element>] : DecodedJWT | semmle.label | of(...) : Optional [<element>] : DecodedJWT | | ||
| JwtNoVerifier.java:74:28:74:30 | jwt : DecodedJWT | semmle.label | jwt : DecodedJWT | | ||
| JwtNoVerifier.java:74:37:74:40 | item : DecodedJWT | semmle.label | item : DecodedJWT | | ||
| JwtNoVerifier.java:74:45:74:48 | item : DecodedJWT | semmle.label | item : DecodedJWT | | ||
| JwtNoVerifier.java:74:45:74:69 | getClaim(...) | semmle.label | getClaim(...) | | ||
subpaths | ||
#select | ||
| JwtNoVerifier.java:74:45:74:69 | getClaim(...) | JwtNoVerifier.java:42:28:42:55 | getParameter(...) : String | JwtNoVerifier.java:74:45:74:69 | getClaim(...) | This parses a $@, but the signature is not verified. | JwtNoVerifier.java:42:28:42:55 | getParameter(...) | JWT | |
1 change: 1 addition & 0 deletions
1
java/ql/test/experimental/query-tests/security/CWE-347/Auth0NoVerifier.qlref
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
experimental/Security/CWE/CWE-347/Auth0NoVerifier.ql |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Check warning
Code scanning / CodeQL
Redundant import Warning