-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
68392e7
V1
am0o0 40ff16b
Merge branch 'main' into amammad-go-JWT
am0o0 bc6a0fc
move to CWE-347
am0o0 2136929
clean tests
am0o0 1e12a86
Merge branch 'main' into amammad-go-JWT
am0o0 a96b001
clean tests
am0o0 da864bf
fix QLDoc
am0o0 c78f390
add go generate support, upgrade JWT.qll
am0o0 8d47a7b
Update python/ql/lib/semmle/python/security/dataflow/PathInjectionQue…
am0o0 f0f60c3
move JWT.qll to experimental
am0o0 aa127b1
do review improvements
am0o0 7d73808
fix a test mistake, add comments for JWT extension points
am0o0 7d36c23
fix qhelp and PascalCase issues
am0o0 2579791
fix examples
am0o0 38b0ed8
fix issues according to codereview
am0o0 82483a2
fix tests
am0o0 db9f74b
fix tests
am0o0 877605d
change c to C for fixing the qhelp error :)
am0o0 4499048
better query quality thanks to owen
am0o0 5e27323
fix qldoc
am0o0 8a3aa2c
Fix formatting
owen-mc 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
This file contains hidden or 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 @@ | ||
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) | ||
} | ||
} |
This file contains hidden or 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,41 @@ | ||
<!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> | ||
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. | ||
</p> | ||
|
||
<p> | ||
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. | ||
</p> | ||
|
||
</overview> | ||
<recommendation> | ||
|
||
<p> | ||
Generating a cryptographically secure secret key during application initialization and using this generated key for future JWT parsing requests can prevent this vulnerability. | ||
</p> | ||
|
||
</recommendation> | ||
<example> | ||
|
||
<p> | ||
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. | ||
</p> | ||
|
||
<sample src="ExampleGood.go" /> | ||
|
||
</example> | ||
<references> | ||
<li> | ||
CVE-2022-0664: | ||
<a href="https://nvd.nist.gov/vuln/detail/CVE-2022-0664">Use of Hard-coded Cryptographic Key in Go github.com/gravitl/netmaker prior to 0.8.5,0.9.4,0.10.0,0.10.1. </a> | ||
</li> | ||
</references> | ||
|
||
</qhelp> |
This file contains hidden or 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,65 @@ | ||
/** | ||
* @name Decoding JWT with hardcoded key | ||
* @description Decoding JWT Secret with a Constant value lead to authentication or authorization bypass | ||
* @kind path-problem | ||
* @problem.severity error | ||
* @id go/parse-jwt-with-hardcoded-key | ||
* @tags security | ||
* experimental | ||
* external/cwe/cwe-321 | ||
*/ | ||
|
||
import go | ||
import experimental.frameworks.JWT | ||
|
||
module JwtParseWithConstantKeyConfig implements DataFlow::ConfigSig { | ||
predicate isSource(DataFlow::Node source) { source.asExpr() instanceof StringLit } | ||
|
||
predicate isSink(DataFlow::Node sink) { | ||
// first part is the JWT Parsing Functions that get a func type as an argument | ||
// Find a node that has flow to a key Function argument | ||
// then find the first result node of this Function which is the secret key | ||
exists(FuncDef fd, DataFlow::Node n, DataFlow::ResultNode rn | | ||
GolangJwtKeyFunc::flow(n, _) and | ||
sink = rn and | ||
fd = n.asExpr() and | ||
rn.getRoot() = fd and | ||
rn.getIndex() = 0 | ||
) | ||
or | ||
exists(Function f, DataFlow::ResultNode rn | | ||
GolangJwtKeyFunc::flow(f.getARead(), _) and | ||
// 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() = f.getFuncDecl() and | ||
rn.getIndex() = 0 | ||
) | ||
or | ||
// second part is the JWT Parsing Functions that get a string or byte as an argument | ||
sink = any(JwtParse jp).getKeyArg() | ||
} | ||
} | ||
|
||
module GolangJwtKeyFuncConfig implements DataFlow::ConfigSig { | ||
predicate isSource(DataFlow::Node source) { | ||
source = any(Function f).getARead() | ||
or | ||
source.asExpr() = any(FuncDef fd) | ||
} | ||
|
||
predicate isSink(DataFlow::Node sink) { | ||
sink = any(JwtParseWithKeyFunction parseJwt).getKeyFuncArg() | ||
} | ||
} | ||
|
||
module JwtParseWithConstantKey = TaintTracking::Global<JwtParseWithConstantKeyConfig>; | ||
|
||
module GolangJwtKeyFunc = TaintTracking::Global<GolangJwtKeyFuncConfig>; | ||
|
||
import JwtParseWithConstantKey::PathGraph | ||
|
||
from JwtParseWithConstantKey::PathNode source, JwtParseWithConstantKey::PathNode sink | ||
where JwtParseWithConstantKey::flowPath(source, sink) | ||
select sink.getNode(), source, sink, "This $@.", source.getNode(), | ||
"Constant Key is used as JWT Secret key" |
This file contains hidden or 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,39 @@ | ||
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) | ||
} | ||
} |
34 changes: 34 additions & 0 deletions
34
go/ql/src/experimental/CWE-347/ParseJWTWithoutVerification.qhelp
This file contains hidden or 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> | ||
In the following code you can see an Example from a popular Library. | ||
</p> | ||
|
||
<sample src="Example.go" /> | ||
|
||
</example> | ||
<references> | ||
<li> | ||
<a href="https://github.com/argoproj/argo-cd/security/advisories/GHSA-q9hr-j4rf-8fjc">JWT audience claim is not verified</a> | ||
</li> | ||
</references> | ||
|
||
</qhelp> |
56 changes: 56 additions & 0 deletions
56
go/ql/src/experimental/CWE-347/ParseJWTWithoutVerification.ql
This file contains hidden or 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,56 @@ | ||
/** | ||
* @name Use of JWT Methods that only decode user provided Token | ||
* @description Using JWT methods without verification can cause to authorization or authentication bypass | ||
* @kind path-problem | ||
* @problem.severity error | ||
* @id go/parse-jwt-without-verification | ||
* @tags security | ||
* experimental | ||
* external/cwe/cwe-321 | ||
*/ | ||
|
||
import go | ||
import experimental.frameworks.JWT | ||
|
||
module WithValidationConfig implements DataFlow::ConfigSig { | ||
predicate isSource(DataFlow::Node source) { source instanceof UntrustedFlowSource } | ||
|
||
predicate isSink(DataFlow::Node sink) { | ||
sink = any(JwtParse jwtParse).getTokenArg() or | ||
sink = any(JwtParseWithKeyFunction jwtParseWithKeyFunction).getTokenArg() | ||
} | ||
|
||
predicate isAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { | ||
golangJwtIsAdditionalFlowStep(nodeFrom, nodeTo) | ||
or | ||
goJoseIsAdditionalFlowStep(nodeFrom, nodeTo) | ||
} | ||
} | ||
|
||
module NoValidationConfig implements DataFlow::ConfigSig { | ||
predicate isSource(DataFlow::Node source) { | ||
source instanceof UntrustedFlowSource and | ||
not WithValidation::flow(source, _) | ||
} | ||
|
||
predicate isSink(DataFlow::Node sink) { | ||
sink = any(JwtUnverifiedParse parseUnverified).getTokenArg() | ||
} | ||
|
||
predicate isAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { | ||
golangJwtIsAdditionalFlowStep(nodeFrom, nodeTo) | ||
or | ||
goJoseIsAdditionalFlowStep(nodeFrom, nodeTo) | ||
} | ||
} | ||
|
||
module WithValidation = TaintTracking::Global<WithValidationConfig>; | ||
|
||
module NoValidation = TaintTracking::Global<NoValidationConfig>; | ||
|
||
import NoValidation::PathGraph | ||
|
||
from NoValidation::PathNode source, NoValidation::PathNode sink | ||
where NoValidation::flowPath(source, sink) | ||
select sink.getNode(), source, sink, "This JWT is parsed without verification and received from $@.", | ||
source.getNode(), "this user-controlled source" | ||
owen-mc marked this conversation as resolved.
Show resolved
Hide resolved
|
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.
Uh oh!
There was an error while loading. Please reload this page.