Skip to content
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

JS: decoding JWT without signature verification #14088

Merged
merged 28 commits into from
Jun 20, 2024

Conversation

am0o0
Copy link
Contributor

@am0o0 am0o0 commented Aug 29, 2023

No description provided.

am0o0 and others added 2 commits August 30, 2023 20:47
… results now, ConstantString is suggested as a better alternative for finding constant sources
@JarLob
Copy link
Contributor

JarLob commented Oct 19, 2023

No-verification query should be under CWE-347.

… pkg, create a path query for jsonwebtoken package which is not work correctly
@am0o0 am0o0 changed the title JS: JWT constant key, no Verification issues JS: decoding JWT without signature verification Nov 2, 2023
…th-problem, update jsonwebtoken source and sinks
@am0o0
Copy link
Contributor Author

am0o0 commented Nov 7, 2023

@JarLob I created one query file for each JWT package, but these all can be merged in some cases where the JWT is decoded in an unsafe way by package1 and then the signature has been validated by package2. it is easy to merge all these together because all have the same template ( only two unverifiedDecode and verifiedDecode predicated are different). I tried my best to have a full cover of examples and tests.

@am0o0
Copy link
Contributor Author

am0o0 commented Nov 24, 2023

Hi @JarLob
I added a new query with the name JsonWebTokenLocalSource.ql which works with the strapi CVE and patch correctly.

@JarLob
Copy link
Contributor

JarLob commented May 13, 2024

@am0o0 Could you please change the pull request from draft to ready for review, so someone from JavaScript CodeQL team can take a look?

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

First you need to move everything into the experimental/ folders.

I'll do a review after you've done that.

Copy link
Contributor

github-actions bot commented May 23, 2024

QHelp previews:

javascript/ql/src/experimental/Security/CWE-347/decodeJwtWithoutVerification.qhelp

JWT missing secret or public key verification

A JSON Web Token (JWT) is used for authenticating and managing users in an application.

Only Decoding JWTs without checking if they have a valid signature or not can lead to security vulnerabilities.

Recommendation

Don't use methods that only decode JWT, Instead use methods that verify the signature of JWT.

Example

In the following code, you can see the proper usage of the most popular JWT libraries.

const express = require('express')
const app = express()
const jwtJsonwebtoken = require('jsonwebtoken');
const jwt_decode = require('jwt-decode');
const jwt_simple = require('jwt-simple');
const jose = require('jose')
const port = 3000

function getSecret() {
    return "A Safe generated random key"
}

app.get('/jose1', async (req, res) => {
    const UserToken = req.headers.authorization;
    // GOOD: with signature verification
    await jose.jwtVerify(UserToken, new TextEncoder().encode(getSecret()))
})

app.get('/jose2', async (req, res) => {
    const UserToken = req.headers.authorization;
    // GOOD: first without signature verification then with signature verification for same UserToken
    jose.decodeJwt(UserToken)
    await jose.jwtVerify(UserToken, new TextEncoder().encode(getSecret()))
})

app.get('/jwtSimple1', (req, res) => {
    const UserToken = req.headers.authorization;
    // GOOD: first without signature verification then with signature verification for same UserToken
    jwt_simple.decode(UserToken, getSecret(), false);
    jwt_simple.decode(UserToken, getSecret());
})

app.get('/jwtSimple2', (req, res) => {
    const UserToken = req.headers.authorization;
    // GOOD: with signature verification
    jwt_simple.decode(UserToken, getSecret(), true);
    jwt_simple.decode(UserToken, getSecret());
})

app.get('/jwtJsonwebtoken1', (req, res) => {
    const UserToken = req.headers.authorization;
    // GOOD: with signature verification
    jwtJsonwebtoken.verify(UserToken, getSecret())
})

app.get('/jwtJsonwebtoken2', (req, res) => {
    const UserToken = req.headers.authorization;
    // GOOD: first without signature verification then with signature verification for same UserToken
    jwtJsonwebtoken.decode(UserToken)
    jwtJsonwebtoken.verify(UserToken, getSecret())
})


app.listen(port, () => {
    console.log(`Example app listening on port ${port}`)
})

In the following code, you can see the improper usage of the most popular JWT libraries.

const express = require('express')
const app = express()
const jwtJsonwebtoken = require('jsonwebtoken');
const jwt_decode = require('jwt-decode');
const jwt_simple = require('jwt-simple');
const jose = require('jose')
const port = 3000

function getSecret() {
    return "A Safe generated random key"
}
app.get('/jose', (req, res) => {
    const UserToken = req.headers.authorization;
    // BAD: no signature verification
    jose.decodeJwt(UserToken)
})

app.get('/jwtDecode', (req, res) => {
    const UserToken = req.headers.authorization;
    // BAD: no signature verification
    jwt_decode(UserToken)
})

app.get('/jwtSimple', (req, res) => {
    const UserToken = req.headers.authorization;
    // jwt.decode(token, key, noVerify, algorithm)
    // BAD: no signature verification
    jwt_simple.decode(UserToken, getSecret(), true);
})

app.get('/jwtJsonwebtoken', (req, res) => {
    const UserToken = req.headers.authorization;
    // BAD: no signature verification
    jwtJsonwebtoken.decode(UserToken)
})

app.listen(port, () => {
    console.log(`Example app listening on port ${port}`)
})

References

Copy link
Contributor

@erik-krogh erik-krogh left a comment

Choose a reason for hiding this comment

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

A first review round.

Primarily: you shouldn't add one new query for each library you're adding a model for, you should just have one query that models all the libraries.

@@ -0,0 +1,66 @@
/**
* @name This query is for seeing if we can have two taint config within on query file
Copy link
Contributor

Choose a reason for hiding this comment

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

This does not look like a file you meant to include?
This look like code you used for testing.

Copy link
Contributor Author

@am0o0 am0o0 May 25, 2024

Choose a reason for hiding this comment

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

I wrote this file for more clarification about what I wanted to do but I failed to express what I needed to get my query to the right point which I failed. this is what I want:
the same source has a Path to two different sinks, one is a dangerous sink, and the other is a guard sink. if one source has a path to both sinks then the source code doesn't contain any vulnerability, but if the this source only has a path to the dangerous sink then the source code contains a vulnerability and should detect it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, I think I know why.
The DataFlow::PathNode includes which DataFlow::Configuration it came from.
So you need to check that the DataFlow::Node that you get from calling .getNode() on the source is the same between the two "sources", you can't use the DataFlow::PathNode from one configuration directly in another.

@am0o0
Copy link
Contributor Author

am0o0 commented May 25, 2024

@erik-krogh sorry this PR was one of my first PRs and I didn't apply what I learnt from you and your colleagues to this PR. I hope the changes are acceptable now.

@erik-krogh
Copy link
Contributor

The decodeJwtWithoutVerification.qlref and decodeJwtWithoutVerificationLocalSource.qlref tests are failing.
Could you rerun those and update the expected outputs.

@am0o0
Copy link
Contributor Author

am0o0 commented Jun 6, 2024

I forgot to update this branch, the tests are still OK. are you sure about failing?

@erik-krogh
Copy link
Contributor

I forgot to update this branch, the tests are still OK. are you sure about failing?

Yeah, they're passing now 🤷

The decodeJwtWithoutVerificationDoesNotWork.ql file still needs to be deleted though.

I think changing the from-where-select to the below might fix your issue (it's the fix I described further up):

from ConfigurationUnverifiedDecode cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where
  cfg.hasFlowPath(source, sink) and
  not exists(ConfigurationVerifiedDecode cfg2 |
    cfg2.hasFlowPath(any(DataFlow::PathNode p | p.getNode() = source.getNode()), _)
  )
select source.getNode(), source, sink, "Decoding JWT $@.", sink.getNode(),
  "without signature verification"

If that works you can use the new approach in your queries.

@erik-krogh erik-krogh merged commit e84028d into github:main Jun 20, 2024
12 checks passed
@erik-krogh
Copy link
Contributor

Thanks. Everything looks good for a merge into experimental/ now 👍

Thanks again for the contribution.

@am0o0 am0o0 deleted the amammad-js-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.

3 participants