Skip to content

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

Merged
merged 23 commits into from
Aug 21, 2024
Merged

Conversation

am0o0
Copy link
Contributor

@am0o0 am0o0 commented Aug 29, 2023

this PR is related to github/securitylab#783


// only decode, no verification
String JwtToken2 = request.getParameter("JWT2");
decodeToken(JwtToken2);
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

@JarLob
Copy link
Contributor

JarLob commented Nov 6, 2023

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.

@am0o0
Copy link
Contributor Author

am0o0 commented Nov 22, 2023

@JarLob except for the tests that I don't know how to create the tests in java, I think everything is good now.

@JarLob
Copy link
Contributor

JarLob commented May 13, 2024

@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?

@am0o0 am0o0 marked this pull request as ready for review May 13, 2024 16:26
@am0o0 am0o0 requested a review from a team as a code owner May 13, 2024 16:26
@am0o0
Copy link
Contributor Author

am0o0 commented May 13, 2024

@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.

@JarLob
Copy link
Contributor

JarLob commented Jun 26, 2024

@am0o0 We are sunsetting our codeql bug bounty program. To be eligible for the reward please work on the pull request ASAP.

@am0o0
Copy link
Contributor Author

am0o0 commented Jul 1, 2024

@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.

@am0o0
Copy link
Contributor Author

am0o0 commented Jul 13, 2024

@JarLob @owen-mc this PR is ready for review as I finalized the tests.

@smowton
Copy link
Contributor

smowton commented Jul 18, 2024

Do you need anything particular to servlet-api-4.0.1, or can you just use java/ql/test/stubs/servlet-api-2.4 instead of introducing new stubs?

@am0o0
Copy link
Contributor Author

am0o0 commented Jul 18, 2024

I tried it just now, and it failed:

codeql test run Auth0NoVerifier.qlref --show-extractor-output
Executing 1 tests in 1 directories.
Extracting test database in /home/am/CodeQL-home/codeql-repo-amammad/java/ql/test/experimental/query-tests/security/CWE-347.
[2024-07-18 20:59:59] [build-err] Exception in thread "main" Compilation errors were reported by javac.
[2024-07-18 20:59:59] [build-err]       at com.semmle.extractor.java.JavaExtractor.main(JavaExtractor.java:810)
[2024-07-18 20:59:59] [ERROR] Spawned process exited abnormally (code 1; tried to run: [/home/am/CodeQL-home/java/tools/linux/jdk-extractor-java/bin/java, -cp, /home/am/CodeQL-home/java/tools/semmle-extractor-java.jar:/home/am/CodeQL-home/java/tools/lombok-javac-extend.jar, -Xmx500m, com.semmle.extractor.java.JavaExtractor, --strict-javac-errors, --javac-args, -source, 8, -target, 8, -cp, /home/am/CodeQL-home/codeql-repo-amammad/java/ql/test/experimental/query-tests/security/CWE-347/../../../stubs/auth0-java-jwt-4.4.0:/home/am/CodeQL-home/codeql-repo-amammad/java/ql/test/experimental/query-tests/security/CWE-347/../../../../../test/stubs/servlet-api-2.4/, -encoding, UTF-8, JwtNoVerifier.java])
Could not extract a dataset in /home/am/CodeQL-home/codeql-repo-amammad/java/ql/test/experimental/query-tests/security/CWE-347: Extraction command /home/am/CodeQL-home/java/tools/linux/jdk-extractor-java/bin/java failed with status 1.
Extraction command /home/am/CodeQL-home/java/tools/linux/jdk-extractor-java/bin/java failed with status 1.
[1/1] FAILED(EXTRACTION) /home/am/CodeQL-home/codeql-repo-amammad/java/ql/test/experimental/query-tests/security/CWE-347/Auth0NoVerifier.qlref
Compiling queries in /home/am/CodeQL-home/codeql-repo-amammad/java/ql/test/experimental/query-tests/security/CWE-347.
Completed in 3.8s (extract 2.2s comp 0ms eval 0ms).
0 tests passed; 1 tests failed:
  FAILED: /home/am/CodeQL-home/codeql-repo-amammad/java/ql/test/experimental/query-tests/security/CWE-347/Auth0NoVerifier.qlref

@owen-mc
Copy link
Contributor

owen-mc commented Jul 18, 2024

@am0o0 Can you be more specific about what you tried?

@am0o0
Copy link
Contributor Author

am0o0 commented Jul 18, 2024

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.

@am0o0
Copy link
Contributor Author

am0o0 commented Jul 18, 2024

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.😅

@smowton
Copy link
Contributor

smowton commented Jul 19, 2024

You can see what the extractions are in CWE-347/CWE-347.testproj/log/javac-output-*.log -- in this case it was simply that WebServlet is new, and getWriter throws IOException in Servlet 2.5. I have taken the liberty of creating #17020 to fix that and polish this PR.

@smowton
Copy link
Contributor

smowton commented Jul 19, 2024

So, looking at this I suggest this could be simplified:

All you're trying to do is notice when the result of decode flows to getClaim or similar ("get-payload"), but does NOT flow to a verify method.

To do this you only need one dataflow config, and you don't need flow states.

Suggest simply:

Source = result of the decode method
Sink = either a verify or a get-payload method
No additional flow steps needed

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.

@smowton
Copy link
Contributor

smowton commented Jul 19, 2024

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)

smowton
smowton previously approved these changes Jul 30, 2024
Copy link
Contributor

github-actions bot commented Jul 30, 2024

QHelp previews:

java/ql/src/experimental/Security/CWE/CWE-347/Auth0NoVerifier.qhelp

Missing JWT signature check

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.

Recommendation

Don't use information from a JWT without verifying that JWT.

Example

The 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

Copy link
Contributor

@JarLob JarLob left a 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.

@am0o0
Copy link
Contributor Author

am0o0 commented Jul 30, 2024

@JarLob Could you please upload the DB, I lost it and I can't create it again because of some errors :)

@am0o0
Copy link
Contributor Author

am0o0 commented Jul 31, 2024

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.

@am0o0
Copy link
Contributor Author

am0o0 commented Jul 31, 2024

@JarLob the CVE can be detected with the local source version of the final query now. Also I managed to create the DB.

@JarLob
Copy link
Contributor

JarLob commented Jul 31, 2024 via email

@am0o0
Copy link
Contributor Author

am0o0 commented Jul 31, 2024

@JarLob I think I'm wrong maybe, could you confirm that this was the sink :) ?
image

@JarLob
Copy link
Contributor

JarLob commented Jul 31, 2024 via email

@JarLob
Copy link
Contributor

JarLob commented Jul 31, 2024 via email

@JarLob
Copy link
Contributor

JarLob commented Jul 31, 2024 via email

Comment on lines 19 to 22
exists(Variable v |
source.asExpr() = v.getInitializer() and
v.getType().hasName("String")
) and
Copy link
Contributor

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.

Copy link
Contributor

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?)

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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))

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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

@smowton
Copy link
Contributor

smowton commented Aug 19, 2024

@JarLob are you able to confirm this latest draft is satisfactory from your point of view?

@JarLob
Copy link
Contributor

JarLob commented Aug 19, 2024

Yes, it is.

smowton
smowton previously approved these changes Aug 19, 2024
@@ -0,0 +1 @@
experimental/Security/CWE/CWE-347/Auth0NoVerifier.ql
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

@smowton smowton merged commit 15989ce into github:main Aug 21, 2024
10 checks passed
@am0o0 am0o0 deleted the amammad-java-JWT branch September 14, 2024 11:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants