-
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
Changes from 7 commits
68392e7
40ff16b
bc6a0fc
2136929
1e12a86
a96b001
da864bf
c78f390
8d47a7b
f0f60c3
aa127b1
7d73808
7d36c23
2579791
38b0ed8
82483a2
db9f74b
877605d
4499048
5e27323
8a3aa2c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,281 @@ | ||
| import go | ||
|
|
||
| /** | ||
| * A class that contains the following function and method: | ||
| * | ||
| * 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()) } | ||
| } | ||
|
|
||
| /** | ||
| * A class that contains the following function and method: | ||
| * | ||
| * 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") | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * A class that contains the following function and method: | ||
| * | ||
| * 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()) } | ||
| } | ||
|
|
||
| /** | ||
| * A class that contains the following method: | ||
| * | ||
| * 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 | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * A class that contains the following function: | ||
| * | ||
| * 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/dgrijalva/jwt-go/v4/request" | ||
| ], "ParseFromRequest") | ||
| | | ||
| this = f | ||
| ) | ||
| } | ||
|
|
||
| int getKeyFuncArgNum() { result = 2 } | ||
|
|
||
| DataFlow::Node getKeyFuncArg() { result = this.getACall().getArgument(this.getKeyFuncArgNum()) } | ||
| } | ||
|
|
||
| /** | ||
| * A class that contains the following function: | ||
| * | ||
| * 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/dgrijalva/jwt-go/v4/request" | ||
| ], "ParseFromRequestWithClaims") | ||
| | | ||
| this = f | ||
| ) | ||
| } | ||
|
|
||
| int getKeyFuncArgNum() { result = 3 } | ||
|
|
||
| DataFlow::Node getKeyFuncArg() { result = this.getACall().getArgument(this.getKeyFuncArgNum()) } | ||
| } | ||
|
|
||
| /** | ||
| * A class that contains the following method: | ||
| * | ||
| *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()) } | ||
| } | ||
|
|
||
| /** | ||
| * A class that contains the following method: | ||
| * | ||
| * 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 | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Holds if there are additioanl steps related to parsing the secret keys | ||
| */ | ||
| 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" | ||
| ], | ||
| [ | ||
| "ParseECPrivateKeyFromPEM", "ParseECPublicKeyFromPEM", "ParseEdPrivateKeyFromPEM", | ||
| "ParseEdPublicKeyFromPEM", "ParseRSAPrivateKeyFromPEM", "ParseRSAPublicKeyFromPEM", | ||
| "RegisterSigningMethod" | ||
| ]) or | ||
| f.hasQualifiedName(["github.com/dgrijalva/jwt-go", "github.com/dgrijalva/jwt-go/v4"], | ||
| [ | ||
| "ParseECPrivateKeyFromPEM", "ParseECPublicKeyFromPEM", "ParseRSAPrivateKeyFromPEM", | ||
| "ParseRSAPrivateKeyFromPEMWithPassword", "ParseRSAPublicKeyFromPEM" | ||
| ]) | ||
|
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 | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * Holds if there are additioanl steps related to parsing the secret keys | ||
| */ | ||
| 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.NestedJSONWebToken" | ||
| ], "Decrypt") | ||
| | | ||
| call = f.getACall() and | ||
| nodeFrom = call.getReceiver() and | ||
| nodeTo = call | ||
| ) | ||
|
am0o0 marked this conversation as resolved.
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
|
|
||
|
|
||
| var JwtKey = []byte("AllYourBase") | ||
|
|
||
| func main() { | ||
| // BAD: usage of a harcoded Key | ||
| verifyJWT(token) | ||
| } | ||
|
|
||
| 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) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| /** | ||
| * @name Decoding JWT with hardcoded key | ||
| * @description Decoding JWT Secrect with a Constant value lead to authentication or authorization bypass | ||
|
owen-mc marked this conversation as resolved.
Outdated
|
||
| * @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 JwtConfig implements DataFlow::ConfigSig { | ||
| predicate isSource(DataFlow::Node source) { source.asExpr() instanceof StringLit } | ||
|
|
||
| predicate isSink(DataFlow::Node sink) { | ||
| 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(), _) | | ||
|
am0o0 marked this conversation as resolved.
Outdated
|
||
| // 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 | ||
| ) | ||
|
Comment on lines
+22
to
+31
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These two
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| } | ||
| } | ||
|
|
||
| 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(GolangJwtParse parseWithClaims).getKeyFuncArg(), | ||
| any(GolangJwtParseWithClaims parseWithClaims).getKeyFuncArg(), | ||
| any(GolangJwtParseFromRequest parseWithClaims).getKeyFuncArg(), | ||
| any(GolangJwtParseFromRequestWithClaims parseWithClaims).getKeyFuncArg(), | ||
| any(GoJoseClaims parseWithClaims).getKeyFuncArg(), | ||
| ] | ||
|
owen-mc marked this conversation as resolved.
Outdated
|
||
| } | ||
| } | ||
|
|
||
| module Jwt = TaintTracking::Global<JwtConfig>; | ||
|
|
||
| module GolangJwtKeyFunc = TaintTracking::Global<GolangJwtKeyFuncConfig>; | ||
|
|
||
| import Jwt::PathGraph | ||
|
|
||
| from Jwt::PathNode source, Jwt::PathNode sink | ||
| where Jwt::flowPath(source, sink) | ||
| select sink.getNode(), source, sink, "This $@.", source.getNode(), | ||
| "Constant Key is used as JWT Secret key" | ||
Uh oh!
There was an error while loading. Please reload this page.