-
Notifications
You must be signed in to change notification settings - Fork 2k
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 1 commit
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
The table of contents is too big for display.
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,263 @@ | ||
| import go | ||
|
|
||
| /** | ||
| * func (p *Parser) Parse(tokenString string, keyFunc Keyfunc) | ||
| * func Parse(tokenString string, keyFunc Keyfunc) | ||
| */ | ||
| class GolangJwtParse extends Function { | ||
| GolangJwtParse() { | ||
| exists(DataFlow::Function f | | ||
| f.hasQualifiedName([ | ||
| "github.com/golang-jwt/jwt", "github.com/golang-jwt/jwt/v4", | ||
| "github.com/golang-jwt/jwt/v5", "github.com/dgrijalva/jwt-go", | ||
| "github.com/dgrijalva/jwt-go/v4", | ||
| ], "Parse") | ||
| | | ||
| this = f | ||
| ) | ||
| or | ||
| exists(DataFlow::Method f | | ||
| f.hasQualifiedName([ | ||
| "github.com/golang-jwt/jwt.Parser", "github.com/golang-jwt/jwt/v4.Parser", | ||
| "github.com/golang-jwt/jwt/v5.Parser", "github.com/dgrijalva/jwt-go.Parser", | ||
| "github.com/dgrijalva/jwt-go/v4.Parser" | ||
| ], "Parse") | ||
| | | ||
| this = f | ||
| ) | ||
| } | ||
|
|
||
| int getKeyFuncArgNum() { result = 1 } | ||
|
|
||
| DataFlow::Node getKeyFuncArg() { result = this.getACall().getArgument(this.getKeyFuncArgNum()) } | ||
| } | ||
|
owen-mc marked this conversation as resolved.
Outdated
|
||
|
|
||
| /** | ||
| * func (p *Parser) Parse(tokenString string, keyFunc Keyfunc) | ||
| * func Parse(tokenString string, keyFunc Keyfunc) | ||
| */ | ||
|
|
||
| class GolangJwtValidField extends DataFlow::FieldReadNode { | ||
| GolangJwtValidField() { | ||
| this.getField() | ||
| .hasQualifiedName([ | ||
| "github.com/golang-jwt/jwt", "github.com/golang-jwt/jwt/v4", | ||
| "github.com/golang-jwt/jwt/v5", "github.com/dgrijalva/jwt-go", | ||
| "github.com/dgrijalva/jwt-go/v4" | ||
| ] + ".Token", "Valid") | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) | ||
| * func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) | ||
| */ | ||
|
|
||
| class GolangJwtParseWithClaims extends Function { | ||
| GolangJwtParseWithClaims() { | ||
| exists(DataFlow::Function f | | ||
| f.hasQualifiedName([ | ||
| "github.com/golang-jwt/jwt", "github.com/golang-jwt/jwt/v4", | ||
| "github.com/golang-jwt/jwt/v5", "github.com/dgrijalva/jwt-go", | ||
| "github.com/dgrijalva/jwt-go/v4" | ||
| ], "ParseWithClaims") | ||
| | | ||
| this = f | ||
| ) | ||
| or | ||
| exists(DataFlow::Method f | | ||
| f.hasQualifiedName([ | ||
| "github.com/golang-jwt/jwt.Parser", "github.com/golang-jwt/jwt/v4.Parser", | ||
| "github.com/golang-jwt/jwt/v5.Parser", "github.com/dgrijalva/jwt-go.Parser", | ||
| "github.com/dgrijalva/jwt-go/v4.Parser" | ||
| ], "ParseWithClaims") | ||
| | | ||
| this = f | ||
| ) | ||
| } | ||
|
|
||
| int getKeyFuncArgNum() { result = 2 } | ||
|
|
||
| DataFlow::Node getKeyFuncArg() { result = this.getACall().getArgument(this.getKeyFuncArgNum()) } | ||
| } | ||
|
|
||
| /** | ||
| * func (p *Parser) ParseUnverified(tokenString string, claims Claims) | ||
| */ | ||
|
|
||
| class GolangJwtParseUnverified extends Function { | ||
| GolangJwtParseUnverified() { | ||
| exists(DataFlow::Method f | | ||
| f.hasQualifiedName([ | ||
| "github.com/golang-jwt/jwt.Parser", "github.com/golang-jwt/jwt/v4.Parser", | ||
| "github.com/golang-jwt/jwt/v5.Parser", "github.com/dgrijalva/jwt-go.Parser", | ||
| "github.com/dgrijalva/jwt-go/v4.Parser" | ||
| ], "ParseUnverified") | ||
| | | ||
| this = f | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * func ParseFromRequest(req *http.Request, extractor Extractor, keyFunc jwt.Keyfunc, options ...ParseFromRequestOption) | ||
| */ | ||
|
|
||
| class GolangJwtParseFromRequest extends Function { | ||
| GolangJwtParseFromRequest() { | ||
| exists(DataFlow::Function f | | ||
| f.hasQualifiedName([ | ||
| "github.com/golang-jwt/jwt/request", "github.com/golang-jwt/jwt/v4/request", | ||
| "github.com/dgrijalva/jwt-go/request", "github.com/golang-jwt/jwt/v4/request", | ||
| "github.com/dgrijalva/jwt-go/v5/request" | ||
| ], "ParseFromRequest") | ||
| | | ||
| this = f | ||
| ) | ||
| } | ||
|
|
||
| int getKeyFuncArgNum() { result = 2 } | ||
|
|
||
| DataFlow::Node getKeyFuncArg() { result = this.getACall().getArgument(this.getKeyFuncArgNum()) } | ||
| } | ||
|
|
||
| /** | ||
| * func ParseFromRequestWithClaims(req *http.Request, extractor Extractor, claims jwt.Claims, keyFunc jwt.Keyfunc) | ||
| */ | ||
|
|
||
| class GolangJwtParseFromRequestWithClaims extends Function { | ||
| GolangJwtParseFromRequestWithClaims() { | ||
| exists(DataFlow::Function f | | ||
| f.hasQualifiedName([ | ||
| "github.com/golang-jwt/jwt/request", "github.com/golang-jwt/jwt/v4/request", | ||
| "github.com/dgrijalva/jwt-go/request", "github.com/golang-jwt/jwt/v4/request", | ||
| "github.com/dgrijalva/jwt-go/v5/request" | ||
| ], "ParseFromRequestWithClaims") | ||
| | | ||
| this = f | ||
| ) | ||
| } | ||
|
|
||
| int getKeyFuncArgNum() { result = 3 } | ||
|
|
||
| DataFlow::Node getKeyFuncArg() { result = this.getACall().getArgument(this.getKeyFuncArgNum()) } | ||
| } | ||
|
|
||
| /** | ||
| *func (t *JSONWebToken) Claims(key interface{}, dest ...interface{}) | ||
| */ | ||
|
|
||
| class GoJoseClaims extends Function { | ||
| GoJoseClaims() { | ||
| exists(DataFlow::Method f | | ||
| f.hasQualifiedName([ | ||
| "gopkg.in/square/go-jose/jwt.JSONWebToken", "gopkg.in/square/go-jose.v2/jwt.JSONWebToken", | ||
| "gopkg.in/square/go-jose.v3/jwt.JSONWebToken", | ||
| "github.com/go-jose/go-jose/jwt.JSONWebToken", | ||
| "github.com/go-jose/go-jose/v3/jwt.JSONWebToken" | ||
| ], "Claims") | ||
| | | ||
| this = f | ||
| ) | ||
| } | ||
|
|
||
| int getKeyFuncArgNum() { result = 1 } | ||
|
|
||
| DataFlow::Node getKeyFuncArg() { result = this.getACall().getArgument(this.getKeyFuncArgNum()) } | ||
| } | ||
|
|
||
| /** | ||
| * func (t *JSONWebToken) UnsafeClaimsWithoutVerification(dest ...interface{}) | ||
| */ | ||
|
|
||
| class GoJoseUnsafeClaims extends Function { | ||
| GoJoseUnsafeClaims() { | ||
| exists(DataFlow::Method f | | ||
| f.hasQualifiedName([ | ||
| "gopkg.in/square/go-jose/jwt.JSONWebToken", "gopkg.in/square/go-jose.v2/jwt.JSONWebToken", | ||
| "gopkg.in/square/go-jose.v3/jwt.JSONWebToken", | ||
| "github.com/go-jose/go-jose/jwt.JSONWebToken", | ||
| "github.com/go-jose/go-jose/v3/jwt.JSONWebToken" | ||
| ], "UnsafeClaimsWithoutVerification") | ||
| | | ||
| this = f | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| predicate golangJwtIsAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { | ||
| exists(DataFlow::Function f, DataFlow::CallNode call | | ||
| f.hasQualifiedName([ | ||
| "github.com/golang-jwt/jwt", "github.com/golang-jwt/jwt/v4", "github.com/golang-jwt/jwt/v5", | ||
| "github.com/dgrijalva/jwt-go", "github.com/dgrijalva/jwt-go/v4" | ||
| ], | ||
| [ | ||
| "ParseECPrivateKeyFromPEM", "ParseECPublicKeyFromPEM", "ParseEdPrivateKeyFromPEM", | ||
| "ParseEdPublicKeyFromPEM", "ParseRSAPrivateKeyFromPEM", "ParseRSAPublicKeyFromPEM", | ||
| "RegisterSigningMethod" | ||
| ]) | ||
|
am0o0 marked this conversation as resolved.
Outdated
|
||
| | | ||
| call = f.getACall() and | ||
| nodeFrom = call.getArgument(0) and | ||
| nodeTo = call | ||
|
am0o0 marked this conversation as resolved.
Outdated
|
||
| ) | ||
| or | ||
| exists(DataFlow::Function f, DataFlow::CallNode call | | ||
| f instanceof GolangJwtParse | ||
| or | ||
| f instanceof GolangJwtParseWithClaims | ||
| | | ||
| call = f.getACall() and | ||
| nodeFrom = call.getArgument(0) and | ||
| nodeTo = call | ||
| ) | ||
| or | ||
| exists(DataFlow::FieldReadNode f | f instanceof GolangJwtValidField | | ||
| nodeFrom = f.getBase() and | ||
| nodeTo = f | ||
| ) | ||
| } | ||
|
|
||
| predicate test(DataFlow::Function f, DataFlow::CallNode call) { | ||
| f.hasQualifiedName([ | ||
| "gopkg.in/square/go-jose/jwt", "gopkg.in/square/go-jose.v2/jwt", | ||
| "gopkg.in/square/go-jose.v3/jwt", "github.com/go-jose/go-jose/jwt", | ||
| "github.com/go-jose/go-jose/v3/jwt" | ||
| ], ["ParseEncrypted", "ParseSigned",]) and | ||
| call = f.getACall().getArgument(0) | ||
| } | ||
|
|
||
| predicate goJoseIsAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { | ||
| exists(DataFlow::Function f, DataFlow::CallNode call | | ||
| f.hasQualifiedName([ | ||
| "gopkg.in/square/go-jose/jwt", "gopkg.in/square/go-jose.v2/jwt", | ||
| "gopkg.in/square/go-jose.v3/jwt", "github.com/go-jose/go-jose/jwt", | ||
| "github.com/go-jose/go-jose/v3/jwt" | ||
| ], ["ParseEncrypted", "ParseSigned",]) | ||
| | | ||
| call = f.getACall() and | ||
| nodeFrom = call.getArgument(0) and | ||
| nodeTo = call | ||
| ) | ||
| or | ||
| exists(DataFlow::Function f, DataFlow::CallNode call | | ||
| f.hasQualifiedName([ | ||
| "gopkg.in/square/go-jose/jwt.NestedJSONWebToken", | ||
| "gopkg.in/square/go-jose.v2/jwt.NestedJSONWebToken", | ||
| "gopkg.in/square/go-jose.v3/jwt.NestedJSONWebToken", | ||
| "github.com/go-jose/go-jose/jwt.NestedJSONWebToken", | ||
| "github.com/go-jose/go-jose/v3/jw.NestedJSONWebTokent" | ||
| ], "ParseSignedAndEncrypted") | ||
| | | ||
| call = f.getACall() and | ||
| nodeFrom = call.getArgument(0) and | ||
| nodeTo = call | ||
| ) | ||
| or | ||
| exists(DataFlow::Method f, DataFlow::CallNode call | | ||
| f.hasQualifiedName([ | ||
| "gopkg.in/square/go-jose/jwt.NestedJSONWebToken", | ||
| "gopkg.in/square/go-jose.v2/jwt.NestedJSONWebToken", | ||
| "gopkg.in/square/go-jose.v3/jwt.NestedJSONWebToken", | ||
| "github.com/go-jose/go-jose/jwt.NestedJSONWebToken", | ||
| "github.com/go-jose/go-jose/v3/jw.NestedJSONWebTokent" | ||
| ], "Decrypt") | ||
| | | ||
| call = f.getACall() and | ||
| nodeFrom = call.getReceiver() and | ||
| nodeTo = call | ||
| ) | ||
|
am0o0 marked this conversation as resolved.
|
||
| } | ||
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,33 @@ | ||
|
|
||
|
|
||
|
|
||
| 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) | ||
| } | ||
| } |
34 changes: 34 additions & 0 deletions
34
go/ql/src/experimental/CWE-321-NoVerification/NoVerification.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> | ||
| 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> |
57 changes: 57 additions & 0 deletions
57
go/ql/src/experimental/CWE-321-NoVerification/NoVerification.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,57 @@ | ||
| /** | ||
| * @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/hardcoded-key | ||
| * @tags security | ||
| * experimental | ||
| * external/cwe/cwe-321 | ||
| */ | ||
|
|
||
| import go | ||
| import semmle.go.security.JWT | ||
|
|
||
| module WithValidationConfig implements DataFlow::ConfigSig { | ||
| predicate isSource(DataFlow::Node source) { source instanceof UntrustedFlowSource } | ||
|
|
||
| predicate isSink(DataFlow::Node sink) { | ||
| sink = any(GolangJwtValidField parse) or | ||
| sink = any(GoJoseClaims parse).getACall().getReceiver() | ||
| } | ||
|
|
||
| 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(GolangJwtParseUnverified parseunverified).getACall().getArgument(0) | ||
| or | ||
| sink = any(GoJoseUnsafeClaims parse).getACall().getReceiver() | ||
| } | ||
|
|
||
| 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 $@.", source.getNode(), "decode" |
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.