-
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 8 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,293 @@ | ||
| import go | ||
|
|
||
| /** | ||
| * A abstract class which responsible for parsing a JWT token which the key parameter is a function type | ||
| */ | ||
| abstract class JwtParseWithKeyFunction extends Function { | ||
| /** | ||
| * Gets argument number that responsible for a function returning the secret key | ||
| */ | ||
| abstract int getKeyFuncArgNum(); | ||
|
|
||
| /** | ||
| * Gets argument number that responsible for JWT | ||
| * | ||
| * `-1` means the receiver is a argument node that responsible for JWT. | ||
| * In this case, we must declare some additional taint steps. | ||
| */ | ||
| abstract int getTokenArgNum(); | ||
|
|
||
| /** | ||
| * Gets Argument as DataFlow node that responsible for JWT | ||
| */ | ||
| DataFlow::Node getTokenArg() { | ||
| this.getTokenArgNum() != -1 and result = this.getACall().getArgument(this.getTokenArgNum()) | ||
| or | ||
| this.getTokenArgNum() = -1 and result = this.getACall().getReceiver() | ||
| } | ||
|
|
||
| /** | ||
| * Gets Argument as DataFlow node that responsible for a function returning the secret key | ||
| */ | ||
| DataFlow::Node getKeyFuncArg() { result = this.getACall().getArgument(this.getKeyFuncArgNum()) } | ||
| } | ||
|
|
||
| /** | ||
| * A abstract class which responsible for parsing a JWT token which the key parameter can be a string or byte type | ||
| */ | ||
| abstract class JwtParse extends Function { | ||
| /** | ||
| * Gets argument number that responsible for secret key | ||
| */ | ||
| abstract int getKeyArgNum(); | ||
|
|
||
| /** | ||
| * Gets argument number that responsible for JWT | ||
| * | ||
| * `-1` means the receiver is a argument node that responsible for JWT. | ||
| * In this case, we must declare some additional taint steps. | ||
| */ | ||
| abstract int getTokenArgNum(); | ||
|
|
||
| /** | ||
| * Gets Argument as DataFlow node that responsible for JWT | ||
| */ | ||
| DataFlow::Node getTokenArg() { | ||
| this.getTokenArgNum() != -1 and result = this.getACall().getArgument(this.getTokenArgNum()) | ||
| or | ||
| this.getTokenArgNum() = -1 and result = this.getACall().getReceiver() | ||
| } | ||
|
|
||
| /** | ||
| * Gets Argument as DataFlow node that responsible for secret key | ||
| */ | ||
| DataFlow::Node getKeyArg() { result = this.getACall().getArgument(this.getKeyArgNum()) } | ||
| } | ||
|
|
||
| /** | ||
| * A abstract class which responsible for parsing a JWT without verifying it | ||
| */ | ||
| abstract class JwtUnverifiedParse extends Function { | ||
| /** | ||
| * Gets argument number that responsible for JWT | ||
| * | ||
| * `-1` means the receiver is a argument node that responsible for JWT. | ||
| * In this case, we must declare some additional taint steps. | ||
| */ | ||
| abstract int getTokenArgNum(); | ||
|
|
||
| /** | ||
| * Gets Argument as DataFlow node that responsible for JWT | ||
| */ | ||
| DataFlow::Node getTokenNode() { | ||
|
am0o0 marked this conversation as resolved.
Outdated
|
||
| this.getTokenArgNum() != -1 and result = this.getACall().getArgument(this.getTokenArgNum()) | ||
| or | ||
| this.getTokenArgNum() = -1 and result = this.getACall().getReceiver() | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Gets `github.com/golang-jwt/jwt` and `github.com/dgrijalva/jwt-go`(previous name of `golang-jwt`) JWT packages | ||
| */ | ||
| string golangJwtPackage() { | ||
| result = package(["github.com/golang-jwt/jwt", "github.com/dgrijalva/jwt-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 JwtParseWithKeyFunction { | ||
| GolangJwtParse() { | ||
| exists(Function f | f.hasQualifiedName(golangJwtPackage(), "Parse") | this = f) | ||
| or | ||
| exists(Method f | f.hasQualifiedName(golangJwtPackage(), "Parser", "Parse") | this = f) | ||
| } | ||
|
|
||
| override int getKeyFuncArgNum() { result = 1 } | ||
|
|
||
| override int getTokenArgNum() { result = 0 } | ||
| } | ||
|
|
||
| /** | ||
| * 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 JwtParseWithKeyFunction { | ||
| GolangJwtParseWithClaims() { | ||
| exists(Function f | f.hasQualifiedName(golangJwtPackage(), "ParseWithClaims") | this = f) | ||
| or | ||
| exists(Method f | f.hasQualifiedName(golangJwtPackage(), "Parser", "ParseWithClaims") | | ||
| this = f | ||
| ) | ||
| } | ||
|
|
||
| override int getKeyFuncArgNum() { result = 2 } | ||
|
|
||
| override int getTokenArgNum() { result = 0 } | ||
| } | ||
|
|
||
| /** | ||
| * A class that contains the following method: | ||
| * | ||
| * func (p *Parser) ParseUnverified(tokenString string, claims Claims) | ||
| */ | ||
| class GolangJwtParseUnverified extends JwtUnverifiedParse { | ||
| GolangJwtParseUnverified() { | ||
| exists(Method f | f.hasQualifiedName(golangJwtPackage(), "Parser", "ParseUnverified") | | ||
| this = f | ||
| ) | ||
| } | ||
|
|
||
| override int getTokenArgNum() { result = 0 } | ||
| } | ||
|
|
||
| /** | ||
| * Gets `github.com/golang-jwt/jwt` and `github.com/dgrijalva/jwt-go`(previous name of `golang-jwt`) JWT packages | ||
| */ | ||
| string golangJwtRequestPackage() { | ||
| result = package(["github.com/golang-jwt/jwt", "github.com/dgrijalva/jwt-go"], "request") | ||
| } | ||
|
|
||
| /** | ||
| * A class that contains the following function: | ||
| * | ||
| * func ParseFromRequest(req *http.Request, extractor Extractor, keyFunc jwt.Keyfunc, options ...ParseFromRequestOption) | ||
| */ | ||
| class GolangJwtParseFromRequest extends JwtParseWithKeyFunction { | ||
| GolangJwtParseFromRequest() { | ||
| exists(Function f | f.hasQualifiedName(golangJwtRequestPackage(), "ParseFromRequest") | | ||
| this = f | ||
| ) | ||
| } | ||
|
|
||
| override int getKeyFuncArgNum() { result = 2 } | ||
|
|
||
| override int getTokenArgNum() { result = 0 } | ||
| } | ||
|
|
||
| /** | ||
| * A class that contains the following function: | ||
| * | ||
| * func ParseFromRequestWithClaims(req *http.Request, extractor Extractor, claims jwt.Claims, keyFunc jwt.Keyfunc) | ||
| */ | ||
| class GolangJwtParseFromRequestWithClaims extends JwtParseWithKeyFunction { | ||
| GolangJwtParseFromRequestWithClaims() { | ||
| exists(Function f | | ||
| f.hasQualifiedName(golangJwtRequestPackage(), "ParseFromRequestWithClaims") | ||
| | | ||
| this = f | ||
| ) | ||
| } | ||
|
|
||
| override int getKeyFuncArgNum() { result = 3 } | ||
|
|
||
| override int getTokenArgNum() { result = 0 } | ||
| } | ||
|
|
||
| /** | ||
| * Gets `gopkg.in/square/go-jose` and `github.com/go-jose/go-jose` jwt package | ||
| */ | ||
| string goJoseJwtPackage() { | ||
| result = package(["gopkg.in/square/go-jose", "github.com/go-jose/go-jose"], "jwt") | ||
| } | ||
|
|
||
| /** | ||
| * A class that contains the following method: | ||
| * | ||
| * func (t *JSONWebToken) Claims(key interface{}, dest ...interface{}) | ||
| */ | ||
| class GoJoseParseWithClaims extends JwtParse { | ||
| GoJoseParseWithClaims() { | ||
| exists(Method f | f.hasQualifiedName(goJoseJwtPackage(), "JSONWebToken", "Claims") | this = f) | ||
| } | ||
|
|
||
| override int getKeyArgNum() { result = 0 } | ||
|
|
||
| override int getTokenArgNum() { result = -1 } | ||
| } | ||
|
|
||
| /** | ||
| * A class that contains the following method: | ||
| * | ||
| * func (t *JSONWebToken) UnsafeClaimsWithoutVerification(dest ...interface{}) | ||
| */ | ||
| class GoJoseUnsafeClaims extends JwtUnverifiedParse { | ||
| GoJoseUnsafeClaims() { | ||
| exists(Method f | | ||
| f.hasQualifiedName(goJoseJwtPackage(), "JSONWebToken", "UnsafeClaimsWithoutVerification") | ||
| | | ||
| this = f | ||
| ) | ||
| } | ||
|
|
||
| override int getTokenArgNum() { result = -1 } | ||
| } | ||
|
|
||
| /** | ||
| * Holds for general additioanl steps related to parsing the secret keys in `golang-jwt/jwt`,`dgrijalva/jwt-go` packages | ||
| */ | ||
| predicate golangJwtIsAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { | ||
| exists(Function f, DataFlow::CallNode call | | ||
| f.hasQualifiedName(package("github.com/golang-jwt/jwt", ""), | ||
| [ | ||
| "ParseECPrivateKeyFromPEM", "ParseECPublicKeyFromPEM", "ParseEdPrivateKeyFromPEM", | ||
| "ParseEdPublicKeyFromPEM", "ParseRSAPrivateKeyFromPEM", "ParseRSAPublicKeyFromPEM", | ||
| "RegisterSigningMethod" | ||
| ]) or | ||
| f.hasQualifiedName(package("github.com/dgrijalva/jwt-go", ""), | ||
| [ | ||
| "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(Function f, DataFlow::CallNode call | | ||
| f instanceof GolangJwtParse | ||
| or | ||
| f instanceof GolangJwtParseWithClaims | ||
| | | ||
| call = f.getACall() and | ||
| nodeFrom = call.getArgument(0) and | ||
| nodeTo = call | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * Holds for general additioanl steps related to parsing the secret keys in `go-jose` package | ||
| */ | ||
| predicate goJoseIsAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { | ||
| exists(Function f, DataFlow::CallNode call | | ||
| f.hasQualifiedName(goJoseJwtPackage(), ["ParseEncrypted", "ParseSigned",]) | ||
| | | ||
| call = f.getACall() and | ||
| nodeFrom = call.getArgument(0) and | ||
| nodeTo = call | ||
| ) | ||
| or | ||
| exists(Method m, DataFlow::CallNode call | | ||
| m.hasQualifiedName(goJoseJwtPackage(), "NestedJSONWebToken", "ParseSignedAndEncrypted") | ||
| | | ||
| call = m.getACall() and | ||
| nodeFrom = call.getArgument(0) and | ||
| nodeTo = call | ||
| ) | ||
| or | ||
| exists(Method f, DataFlow::CallNode call | | ||
| f.hasQualifiedName(goJoseJwtPackage(), "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,64 @@ | ||
| /** | ||
| * @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 semmle.go.security.JWT | ||
|
|
||
| module JwtPaseWithConstantKeyConfig implements DataFlow::ConfigSig { | ||
|
am0o0 marked this conversation as resolved.
Outdated
|
||
| 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 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. |
||
| 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 JwtPaseWithConstantKey = TaintTracking::Global<JwtPaseWithConstantKeyConfig>; | ||
|
am0o0 marked this conversation as resolved.
Outdated
|
||
|
|
||
| module GolangJwtKeyFunc = TaintTracking::Global<GolangJwtKeyFuncConfig>; | ||
|
|
||
| import JwtPaseWithConstantKey::PathGraph | ||
|
|
||
| from JwtPaseWithConstantKey::PathNode source, JwtPaseWithConstantKey::PathNode sink | ||
| where JwtPaseWithConstantKey::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.