-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Java: JWT decoding without verification #14089
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
Conversation
|
||
// only decode, no verification | ||
String JwtToken2 = request.getParameter("JWT2"); | ||
decodeToken(JwtToken2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since nobody is using the result of decodeToken
why does it matter whether we verify it or not?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:) @intrigus-lgtm really fast response!
it is just an example if it is mandatory that I wrote a complete example then please let me know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the sink is the jwt claims values so I think the query is OK but the example can be wrong I can extend the code or write more comment too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a good point made by @intrigus-lgtm. As you see in the similar query for JS it can be decisive factor if the decoded payload is used or not. I think you should update your tests and query to raise the alert if it is used and do not raise if it is discarded.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW, I think your query already discards the alert if the result of decode is not used, but it would be good to confirm it with tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the only idea that I have is to add another global taint module and use that to reach from the current sink JWT.decode(sink)
to item.getClaim("sth")
if you agree with this and you think having a third global taint module is not a problem I'll implement this too.
please note the third global taint module can be a small path so maybe there are no performance issues against it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you need changes to the existing codeql query. Have you tried just adding two tests: one that doesn't do anything with the result of decodeToken (should not alert) and another one that calls getClaim
for example?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can use flowState in one of the two taint configurations of my query and set the sink to getClaim
instead of JWT.decode
I'll test it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
About tests i'll check more tests first.
We have already https://github.com/github/codeql/blob/main/java/ql/src/Security/CWE/CWE-347/MissingJWTSignatureCheck.ql (Which doesn't detect the CVE), but I'll leave it to the CodeQL team to decide if it is better to merge in this PR or later. |
…ntations for ql classes
@JarLob except for the tests that I don't know how to create the tests in java, I think everything is good now. |
@am0o0 Could you please mark this pull request as ready for review? Have you looked already how tests for java are made in https://github.com/github/codeql/tree/main/java/ql/test/query-tests/security/CWE-347? |
@JarLob Yes I tried to learn to create test cases by comparing them to a similar part of current query tests but I failed, I'll try again. |
@am0o0 We are sunsetting our codeql bug bounty program. To be eligible for the reward please work on the pull request ASAP. |
@JarLob I'm learning how to create stubs for Java, otherwise the tests are the same as the example file, so when I finish the work of creating the test for the other java PR I'll quickly create a test case here. |
Do you need anything particular to servlet-api-4.0.1, or can you just use |
I tried it just now, and it failed:
|
@am0o0 Can you be more specific about what you tried? |
I simply replaced my servlet api stub with "java/ql/test/stubs/servlet-api-2.4" in the options file, I double-checked that the new path of the stub was correct too. |
it is a very old version i dont expect to work honestly. Also we can think in this way that we have the stubs for new version now.😅 |
You can see what the extractions are in |
So, looking at this I suggest this could be simplified: All you're trying to do is notice when the result of To do this you only need one dataflow config, and you don't need flow states. Suggest simply: Source = result of the decode method Then the query result is simply those (source, sink) pairs where the sink is a get-payload method and there does not exist a (source2, sink2) such that sink2 is a verify method and source.getNode = source2.getNode. If it should turn out there are false positives due to decoding JWTs that don't come from a remote source, then flow-state can be used to track RemoteFlowSource -> decode -> verify/get-payload -- in that case decode is an additional flow step, but you still don't need flow-state because the type system guarantees that if we're verifying something from a remote flow source, it must have been decoded at some point. Finally I would remove the flow sources that seem to regard any string-typed variable initializer as a source. In that case you might as well call any string at all a source (what's special about initializing a variable?), and in that case you might as well just start tracking flow from the decode method instead, since clearly it got a string input from somewhere. |
Other simple things to fix: MethodAccess -> MethodCall DataFlow::FlowState -> string (but as mentioned above this should be removed anyway) Change the query id and description to reflect this being an auth0-specific query (and not clash with the non-experimental query that also checks for JWT mistakes) |
QHelp previews: java/ql/src/experimental/Security/CWE/CWE-347/Auth0NoVerifier.qhelpMissing JWT signature checkA 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. RecommendationDon't use information from a JWT without verifying that JWT. ExampleThe following example illustrates secure and insecure use of the Auth0 `java-jwt` library. 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("");
}
} References
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The query doesn't detect the original CVE anymore.
@JarLob Could you please upload the DB, I lost it and I can't create it again because of some errors :) |
Hi, I tried to fix my mistake with minimum changes, you can check the changes with commits, I tried to have a good commit history for this to make the review easier. |
@JarLob the CVE can be detected with the local source version of the final query now. Also I managed to create the DB. |
Hi, it still doesn't work. Here is the database https://we.tl/t-WOpOJykqkl
…On Wed, Jul 31, 2024 at 11:07 AM Am ***@***.***> wrote:
[image: am0o0]*am0o0* left a comment (github/codeql#14089)
<#14089 (comment)>
@JarLob <https://github.com/JarLob> the CVE can be detected with the
local source version of the final query now. Also I managed to create the
DB.
—
Reply to this email directly, view it on GitHub
<#14089 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AGLK53EU4HOXZICURTKYW3DZPCSMZAVCNFSM6AAAAAA4C4SPJCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDENRQGAZDIOBQGQ>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
@JarLob I think I'm wrong maybe, could you confirm that this was the sink :) ? |
Maybe my codeql branch is outdated. I'll check out the pr branch and try
again.
…On Wed, Jul 31, 2024 at 2:11 PM Am ***@***.***> wrote:
[image: am0o0]*am0o0* left a comment (github/codeql#14089)
<#14089 (comment)>
@JarLob <https://github.com/JarLob> I think I'm wrong maybe, could you
confirm that this was the sink :) ?
image.png (view on web)
<https://github.com/user-attachments/assets/aae8b2c1-4a33-4192-bf9d-d77a35f9a659>
—
Reply to this email directly, view it on GitHub
<#14089 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AGLK53GYOEPL5HSKBJRV2VDZPDH5PAVCNFSM6AAAAAA4C4SPJCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDENRQGM3TIMRXGE>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
I have opened your branch in a codespace and got 0 results:

On Wed, Jul 31, 2024 at 2:20 PM Jaroslav Lobacevski ***@***.***>
wrote:
… Maybe my codeql branch is outdated. I'll check out the pr branch and try
again.
On Wed, Jul 31, 2024 at 2:11 PM Am ***@***.***> wrote:
> [image: am0o0]*am0o0* left a comment (github/codeql#14089)
> <#14089 (comment)>
>
> @JarLob <https://github.com/JarLob> I think I'm wrong maybe, could you
> confirm that this was the sink :) ?
> image.png (view on web)
> <https://github.com/user-attachments/assets/aae8b2c1-4a33-4192-bf9d-d77a35f9a659>
>
> —
> Reply to this email directly, view it on GitHub
> <#14089 (comment)>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AGLK53GYOEPL5HSKBJRV2VDZPDH5PAVCNFSM6AAAAAA4C4SPJCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDENRQGM3TIMRXGE>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>
|
Ah, I missed that local.ql. I can only blame GitHub UI for cutting the file
list :)
It gives the result.
… Message ID: ***@***.***>
>>
>
|
exists(Variable v | | ||
source.asExpr() = v.getInitializer() and | ||
v.getType().hasName("String") | ||
) and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is still a bad source. If your source is just "any string variable" then there is essentially no point in having a source at all. In this case just track flow starting at the decode operation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(on the other hand is there an actual source that we don't call a RemoteFlowSource but ought to be considered dangerous in order to get the CVE hit you're looking for? If so can you say what it is?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is the source:
https://github.com/apache/shenyu/blob/dc841a2720ea0b5a6ca71f1b9c1aa7958d241f16/shenyu-admin/src/main/java/org/apache/shenyu/admin/shiro/config/ShiroRealm.java#L75
I think we can set this authenticationToken.getCredentials()
as a remote flow source since the Shiro is a package and it can be imported into other applications I guess. so it can be utilized in other projects too and this remote flow source is not specific to only this source code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should just consider any JWT token potentially interesting, since they're inherently things used in a security-sensitive context. I would recommend making the result of the decode method your source, then just measuring flow from there to either verification or to information access, rather than worry about the ultimate flow source being remote or otherwise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is not possible to use the decoding result as the source because from the result we don't have a flow to a JWT verify method, we should have the original JWT( here is the token
variable) as the source and check if this token has a flow to a decoding method and doesn't have a flow to verify method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes I see. I'm not familiar enough with Shiro to know if doGetAuthenticationInfo
is a reasonable method to assume is generally called with an untrusted parameter. If so, I would be fine with adding as a source, any method implementing AuthenticatingRealm.doGetAuthenticationInfo
(note Authenticating not Authorizing; Authenticating defines the interface and Authorizing inherits it). Use Method.overrides+
to find implementations, like
exists(Method base, Method override | base.hasQualifiedName("package", "Class", "methodName") and override.overrides+(base))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh or better yet, add it as an experimental models-as-data source, like you did in #16708 but defining a sourceModel
not a sinkModel
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this method can be the source: https://github.com/apache/shenyu/blob/dc841a2720ea0b5a6ca71f1b9c1aa7958d241f16/shenyu-admin/src/main/java/org/apache/shenyu/admin/shiro/config/ShiroRealm.java#L75
authenticationToken.getCredentials();
about the AuthenticationToken interface, the document defines this interface as follows (link):
An AuthenticationToken is a consolidation of an account's principals and supporting credentials submitted by a user during an authentication attempt.
I think it is fine to add some methods of this interface as remote sources.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good to me-- if you go ahead and add them as sources, we'll be able to get this PR merged
@JarLob are you able to confirm this latest draft is satisfactory from your point of view? |
Yes, it is. |
@@ -0,0 +1 @@ | |||
experimental/Security/CWE/CWE-347/Auth0NoVerifier.ql |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
experimental/Security/CWE/CWE-347/Auth0NoVerifier.ql | |
query: experimental/Security/CWE/CWE-347/Auth0NoVerifier.ql | |
postprocess: TestUtilities/PrettyPrintModels.ql |
Try making this change and re-generating the test results. It should remove the absolute model numbers, which is what's causing tests to fail at the moment.
this PR is related to github/securitylab#783