Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
293 changes: 293 additions & 0 deletions go/ql/lib/semmle/go/security/JWT.qll
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()
}
Comment thread
am0o0 marked this conversation as resolved.

/**
* 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() {
Comment thread
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"
])
Comment thread
am0o0 marked this conversation as resolved.
Outdated
|
call = f.getACall() and
nodeFrom = call.getArgument(0) and
nodeTo = call
Comment thread
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
)
Comment thread
am0o0 marked this conversation as resolved.
}
21 changes: 21 additions & 0 deletions go/ql/src/experimental/CWE-321-V2/Example.go
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)
}
}
64 changes: 64 additions & 0 deletions go/ql/src/experimental/CWE-321-V2/HardCodedKeys.ql
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 {
Comment thread
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(), _) |
Comment thread
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
Copy link
Copy Markdown
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
Copy Markdown
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.

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()
Comment thread Fixed
}
}

module JwtPaseWithConstantKey = TaintTracking::Global<JwtPaseWithConstantKeyConfig>;
Comment thread
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"
Loading