Skip to content

Go: Improved JWT query, JWT decoding without verification #14075

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 21 commits into from
Oct 11, 2023

Conversation

am0o0
Copy link
Contributor

@am0o0 am0o0 commented Aug 28, 2023

Some functions/methods of some JWT packages do not verify the JWT signature, and sometimes developers use it by mistake, if developers are using this intentionally then they should be careful about future contributions so I think especially in open source projects this is not recommended to using this method.
Also, I wrote a query that improves the current hardcoded JWT secret keys query because Some JWT packages use a function as input to return the secret on some conditions that developers specify. I used two Taint module configurations one for finding these functions and two for any constant key that can be tainted to first return the value of these key functions. So with this, we can be sure that we can find the right constant secret key (if it exists). I found some results with MRVA which are new!

@Kwstubbs
Copy link
Contributor

@github/codeql-go This PR is part of a bounty and is ready for review. If someone with write access could move this to ready for review I would appreciate it. Thanks.

@owen-mc owen-mc marked this pull request as ready for review September 19, 2023 10:30
@owen-mc owen-mc requested a review from a team as a code owner September 19, 2023 10:30
@github-actions
Copy link
Contributor

QHelp previews:

go/ql/src/experimental/CWE-347/NoVerification.qhelp

Use of JWT Methods that only decode user provided Token

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

The following code you can see an Example from a popular Library.

func main() {
	// BAD: only decode jwt without verification
	notVerifyJWT(token)

	// GOOD: decode with verification or verifiy plus decode
	notVerifyJWT(token)
	VerifyJWT(token)
}

func notVerifyJWT(signedToken string) {
	fmt.Println("only decoding JWT")
	DecodedToken, _, err := jwt.NewParser().ParseUnverified(signedToken, &CustomerInfo{})
	if claims, ok := DecodedToken.Claims.(*CustomerInfo); ok {
		fmt.Printf("DecodedToken:%v\n", claims)
	} else {
		log.Fatal("error", err)
	}
}
func LoadJwtKey(token *jwt.Token) (interface{}, error) {
	return ARandomJwtKey, nil
}
func verifyJWT(signedToken string) {
	fmt.Println("verifying JWT")
	DecodedToken, err := jwt.ParseWithClaims(signedToken, &CustomerInfo{}, LoadJwtKey)
	if claims, ok := DecodedToken.Claims.(*CustomerInfo); ok && DecodedToken.Valid {
		fmt.Printf("NAME:%v ,ID:%v\n", claims.Name, claims.ID)
	} else {
		log.Fatal(err)
	}
}

References

Copy link
Contributor

@owen-mc owen-mc left a comment

Choose a reason for hiding this comment

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

I've only had a quick look so far, but I've suggested some refactoring that should make it easier to read.

@owen-mc
Copy link
Contributor

owen-mc commented Sep 19, 2023

You're also failing a CI check with this message:

Query ID go/hardcoded-key is used in multiple queries:
 - go/ql/src/experimental/CWE-321/HardcodedKeys.ql
 - go/ql/src/experimental/CWE-347/NoVerification.ql
 - go/ql/src/experimental/CWE-321-V2/HardCodedKeys.ql
FAIL: duplicate query IDs found in src folders. Please assign these queries unique IDs.
@owen-mc
Copy link
Contributor

owen-mc commented Sep 19, 2023

And the tests you've added are all failing because they're missing the package clause at the top of the file.

@am0o0 am0o0 requested a review from a team as a code owner September 27, 2023 10:17
@am0o0
Copy link
Contributor Author

am0o0 commented Sep 27, 2023

@owen-mc thank you so much for your comprehensive reviews, it made my job much easier to resolve the issues of my PRs.
I couldn't find any experimental library directory to put JWT.qll in it.

…ry.qll

Co-authored-by: Rasmus Wriedt Larsen <rasmuswriedtlarsen@gmail.com>
@github-actions github-actions bot removed the Python label Sep 28, 2023
Copy link
Contributor

@owen-mc owen-mc left a comment

Choose a reason for hiding this comment

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

Oops, I forgot to submit this review. Sorry for the delay.

@github-actions
Copy link
Contributor

github-actions bot commented Oct 6, 2023

QHelp previews:

go/ql/src/experimental/CWE-347/ParseJWTWithoutVerification.qhelp

Use of JWT Methods that only decode user provided Token

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 an Example from a popular Library.

package main

import (
	"fmt"
	"log"
)

func main() {
	// BAD: only decode jwt without verification
	notVerifyJWT(token)

	// GOOD: decode with verification or verifiy plus decode
	notVerifyJWT(token)
	VerifyJWT(token)
}

func notVerifyJWT(signedToken string) {
	fmt.Println("only decoding JWT")
	DecodedToken, _, err := jwt.NewParser().ParseUnverified(signedToken, &CustomerInfo{})
	if claims, ok := DecodedToken.Claims.(*CustomerInfo); ok {
		fmt.Printf("DecodedToken:%v\n", claims)
	} else {
		log.Fatal("error", err)
	}
}
func LoadJwtKey(token *jwt.Token) (interface{}, error) {
	return ARandomJwtKey, nil
}
func verifyJWT(signedToken string) {
	fmt.Println("verifying JWT")
	DecodedToken, err := jwt.ParseWithClaims(signedToken, &CustomerInfo{}, LoadJwtKey)
	if claims, ok := DecodedToken.Claims.(*CustomerInfo); ok && DecodedToken.Valid {
		fmt.Printf("NAME:%v ,ID:%v\n", claims.Name, claims.ID)
	} else {
		log.Fatal(err)
	}
}

References

@owen-mc
Copy link
Contributor

owen-mc commented Oct 6, 2023

Please rename ParseJWTWithoutVerification.ql.qhelp to ParseJWTWithoutVerification.qhelp

Copy link
Contributor

@owen-mc owen-mc left a comment

Choose a reason for hiding this comment

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

Please create a qhelp file for HardCodedKeys.ql.

Comment on lines 22 to 36
exists(FuncDef fd, DataFlow::Node n, DataFlow::ResultNode rn |
GolangJwtKeyFunc::flow(n, _) and fd = n.asExpr()
|
rn.getRoot() = fd and
rn.getIndex() = 0 and
sink = rn
)
or
exists(Function fd, DataFlow::ResultNode rn | GolangJwtKeyFunc::flow(fd.getARead(), _) |
// sink is result of a method
sink = rn and
// the method is belong to a function in which is used as a JWT function key
rn.getRoot() = fd.getFuncDecl() and
rn.getIndex() = 0
)
Copy link
Contributor

Choose a reason for hiding this comment

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

These two exists have a huge amount in common. It would be better to combine them into one exists.

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 If I merge them it can be much longer that it is.

@github-actions
Copy link
Contributor

QHelp previews:

go/ql/src/experimental/CWE-321-V2/HardcodedKeys.qhelp

errors/warnings:

A fatal error occurred: Failed to read path ./go/ql/src/experimental/CWE-321-V2/HardcodedKeys.ql
(eventual cause: NoSuchFileException "./go/ql/src/experimental/CWE-321-V2/HardcodedKeys.ql")
go/ql/src/experimental/CWE-347/ParseJWTWithoutVerification.qhelp

Use of JWT Methods that only decode user provided Token

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 an Example from a popular Library.

package main

import (
	"fmt"
	"log"

	"github.com/golang-jwt/jwt/v5"
)

func main() {
	// BAD: only decode jwt without verification
	notVerifyJWT(token)

	// GOOD: decode with verification or verify plus decode
	notVerifyJWT(token)
	VerifyJWT(token)
}

func notVerifyJWT(signedToken string) {
	fmt.Println("only decoding JWT")
	DecodedToken, _, err := jwt.NewParser().ParseUnverified(signedToken, &CustomerInfo{})
	if claims, ok := DecodedToken.Claims.(*CustomerInfo); ok {
		fmt.Printf("DecodedToken:%v\n", claims)
	} else {
		log.Fatal("error", err)
	}
}
func LoadJwtKey(token *jwt.Token) (interface{}, error) {
	return ARandomJwtKey, nil
}
func verifyJWT(signedToken string) {
	fmt.Println("verifying JWT")
	DecodedToken, err := jwt.ParseWithClaims(signedToken, &CustomerInfo{}, LoadJwtKey)
	if claims, ok := DecodedToken.Claims.(*CustomerInfo); ok && DecodedToken.Valid {
		fmt.Printf("NAME:%v ,ID:%v\n", claims.Name, claims.ID)
	} else {
		log.Fatal(err)
	}
}

References

@github-actions
Copy link
Contributor

github-actions bot commented Oct 10, 2023

QHelp previews:

go/ql/src/experimental/CWE-321-V2/HardCodedKeys.qhelp

Decoding JWT with hardcoded key

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

Using a hard-coded secret key for parsing JWT tokens in open source projects can leave the application using the token vulnerable to authentication bypasses.

A JWT token is safe for enforcing authentication and access control as long as it can't be forged by a malicious actor. However, when a project exposes this secret publicly, these seemingly unforgeable tokens can now be easily forged. Since the authentication as well as access control is typically enforced through these JWT tokens, an attacker armed with the secret can create a valid authentication token for any user and may even gain access to other privileged parts of the application.

Recommendation

Generating a cryptographically secure secret key during application initialization and using this generated key for future JWT parsing requests can prevent this vulnerability.

Example

The following code uses a hard-coded string as a secret for parsing user provided JWTs. In this case, an attacker can very easily forge a token by using the hard-coded secret.

package main

import (
	"fmt"
	"log"

	"github.com/go-jose/go-jose/v3/jwt"
)

var JwtKey = []byte("AllYourBase")

func main() {
	// BAD: usage of a harcoded Key
	verifyJWT(JWTFromUser)
}

func LoadJwtKey(token *jwt.Token) (interface{}, error) {
	return JwtKey, nil
}
func verifyJWT(signedToken string) {
	fmt.Println("verifying JWT")
	DecodedToken, err := jwt.ParseWithClaims(signedToken, &CustomerInfo{}, LoadJwtKey)
	if claims, ok := DecodedToken.Claims.(*CustomerInfo); ok && DecodedToken.Valid {
		fmt.Printf("NAME:%v ,ID:%v\n", claims.Name, claims.ID)
	} else {
		log.Fatal(err)
	}
}

References

go/ql/src/experimental/CWE-347/ParseJWTWithoutVerification.qhelp

Use of JWT Methods that only decode user provided Token

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 an Example from a popular Library.

package main

import (
	"fmt"
	"log"

	"github.com/golang-jwt/jwt/v5"
)

func main() {
	// BAD: only decode jwt without verification
	notVerifyJWT(token)

	// GOOD: decode with verification or verify plus decode
	notVerifyJWT(token)
	VerifyJWT(token)
}

func notVerifyJWT(signedToken string) {
	fmt.Println("only decoding JWT")
	DecodedToken, _, err := jwt.NewParser().ParseUnverified(signedToken, &CustomerInfo{})
	if claims, ok := DecodedToken.Claims.(*CustomerInfo); ok {
		fmt.Printf("DecodedToken:%v\n", claims)
	} else {
		log.Fatal("error", err)
	}
}
func LoadJwtKey(token *jwt.Token) (interface{}, error) {
	return ARandomJwtKey, nil
}
func verifyJWT(signedToken string) {
	fmt.Println("verifying JWT")
	DecodedToken, err := jwt.ParseWithClaims(signedToken, &CustomerInfo{}, LoadJwtKey)
	if claims, ok := DecodedToken.Claims.(*CustomerInfo); ok && DecodedToken.Valid {
		fmt.Printf("NAME:%v ,ID:%v\n", claims.Name, claims.ID)
	} else {
		log.Fatal(err)
	}
}

References

@owen-mc
Copy link
Contributor

owen-mc commented Oct 10, 2023

Two problems:

  • /home/runner/work/codeql/codeql/go/ql/src/experimental/CWE-321-V2/HardCodedKeys.qhelp: Could not find sample ExampleGood.go
  • go/ql/src/experimental/CWE-347/ParseJWTWithoutVerification.ql would change by autoformatting.
@owen-mc
Copy link
Contributor

owen-mc commented Oct 11, 2023

I made a suggestion to fix the formatting check and committed it. I didn't know I could do that. So you may have to pull to get that commit on your local machine.

@am0o0
Copy link
Contributor Author

am0o0 commented Oct 11, 2023

@owen-mc Thanks, I think when I opened the pull request I gave access to maintainers to perform their changes.

Copy link
Contributor

@owen-mc owen-mc left a comment

Choose a reason for hiding this comment

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

Thank you for your contribution!

@owen-mc
Copy link
Contributor

owen-mc commented Oct 11, 2023

@amammad One last thing - could you edit the description of this PR with a brief summary of it?

@am0o0
Copy link
Contributor Author

am0o0 commented Oct 11, 2023

@owen-mc Its done!

@owen-mc owen-mc merged commit 96543b8 into github:main Oct 11, 2023
@am0o0 am0o0 deleted the amammad-go-JWT branch September 14, 2024 11:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
4 participants