Skip to content

Commit 1704bfe

Browse files
authored
Merge pull request #15585 from atorralba/atorralba/go/promote-jwt-unsafe-verification
Go: Promote `go/missing-jwt-signature-check` from experimental
2 parents 1642501 + 8b8cebd commit 1704bfe

27 files changed

+346
-142
lines changed

go/ql/lib/ext/github.com.dgrijalva.jwt-go.model.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,19 @@ extensions:
33
pack: codeql/go-all
44
extensible: sinkModel
55
data:
6-
- ["github.com/dgrijalva/jwt-go", "Token", True, "SignedString", "", "", "Argument[0]", "credentials-key", "manual"]
76
- ["github.com/dgrijalva/jwt-go", "SigningMethod", True, "Sign", "", "", "Argument[1]", "credentials-key", "manual"]
7+
- ["github.com/dgrijalva/jwt-go", "Token", True, "SignedString", "", "", "Argument[0]", "credentials-key", "manual"]
8+
- ["github.com/dgrijalva/jwt-go", "Parser", True, "ParseUnverified", "", "", "Argument[0]", "jwt", "manual"]
9+
- addsTo:
10+
pack: codeql/go-all
11+
extensible: summaryModel
12+
data:
13+
- ["github.com/dgrijalva/jwt-go", "", True, "Parse", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
14+
- ["github.com/dgrijalva/jwt-go", "Parser", True, "Parse", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
15+
- ["github.com/dgrijalva/jwt-go", "", True, "ParseWithClaims", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
16+
- ["github.com/dgrijalva/jwt-go", "Parser", True, "ParseWithClaims", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
17+
- ["github.com/dgrijalva/jwt-go", "", True, "ParseECPrivateKeyFromPEM", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
18+
- ["github.com/dgrijalva/jwt-go", "", True, "ParseECPublicKeyFromPEM", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
19+
- ["github.com/dgrijalva/jwt-go", "", True, "ParseRSAPrivateKeyFromPEM", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
20+
- ["github.com/dgrijalva/jwt-go", "", True, "ParseRSAPrivateKeyFromPEMWithPassword", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
21+
- ["github.com/dgrijalva/jwt-go", "", True, "ParseRSAPublicKeyFromPEM", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
extensions:
2+
- addsTo:
3+
pack: codeql/go-all
4+
extensible: sinkModel
5+
data:
6+
- ["github.com/go-jose/go-jose/$ANYVERSION/jwt", "JSONWebToken", True, "UnsafeClaimsWithoutVerification", "", "", "Argument[-1]", "jwt", "manual"]
7+
- addsTo:
8+
pack: codeql/go-all
9+
extensible: summaryModel
10+
data:
11+
- ["github.com/go-jose/go-jose/$ANYVERSION/jwt", "", True, "ParseEncrypted", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
12+
- ["github.com/go-jose/go-jose/$ANYVERSION/jwt", "", True, "ParseSigned", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
13+
- ["github.com/go-jose/go-jose/$ANYVERSION/jwt", "NestedJSONWebToken", True, "ParseSignedAndEncrypted", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
14+
- ["github.com/go-jose/go-jose/$ANYVERSION/jwt", "NestedJSONWebToken", True, "Decrypt", "", "", "Argument[-1]", "ReturnValue[0]", "taint", "manual"]

go/ql/lib/ext/github.com.golang-jwt.jwt.model.yml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,21 @@ extensions:
33
pack: codeql/go-all
44
extensible: sinkModel
55
data:
6-
- ["github.com/golang-jwt/jwt", "Token", True, "SignedString", "", "", "Argument[0]", "credentials-key", "manual"]
76
- ["github.com/golang-jwt/jwt", "SigningMethod", True, "Sign", "", "", "Argument[1]", "credentials-key", "manual"]
7+
- ["github.com/golang-jwt/jwt", "Token", True, "SignedString", "", "", "Argument[0]", "credentials-key", "manual"]
8+
- ["github.com/golang-jwt/jwt", "Parser", True, "ParseUnverified", "", "", "Argument[0]", "jwt", "manual"]
9+
- addsTo:
10+
pack: codeql/go-all
11+
extensible: summaryModel
12+
data:
13+
- ["github.com/golang-jwt/jwt", "", True, "Parse", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
14+
- ["github.com/golang-jwt/jwt", "Parser", True, "Parse", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
15+
- ["github.com/golang-jwt/jwt", "", True, "ParseWithClaims", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
16+
- ["github.com/golang-jwt/jwt", "Parser", True, "ParseWithClaims", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
17+
- ["github.com/golang-jwt/jwt", "", True, "ParseECPrivateKeyFromPEM", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
18+
- ["github.com/golang-jwt/jwt", "", True, "ParseECPublicKeyFromPEM", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
19+
- ["github.com/golang-jwt/jwt", "", True, "ParseEdPrivateKeyFromPEM", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
20+
- ["github.com/golang-jwt/jwt", "", True, "ParseEdPublicKeyFromPEM", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
21+
- ["github.com/golang-jwt/jwt", "", True, "ParseRSAPrivateKeyFromPEM", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
22+
- ["github.com/golang-jwt/jwt", "", True, "ParseRSAPublicKeyFromPEM", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
23+
- ["github.com/golang-jwt/jwt", "", True, "RegisterSigningMethod", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
extensions:
2+
- addsTo:
3+
pack: codeql/go-all
4+
extensible: sinkModel
5+
data:
6+
- ["gopkg.in/square/go-jose.v2/jwt", "JSONWebToken", True, "UnsafeClaimsWithoutVerification", "", "", "Argument[-1]", "jwt", "manual"]
7+
- addsTo:
8+
pack: codeql/go-all
9+
extensible: summaryModel
10+
data:
11+
- ["gopkg.in/square/go-jose.v2/jwt", "", True, "ParseEncrypted", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
12+
- ["gopkg.in/square/go-jose.v2/jwt", "", True, "ParseSigned", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
13+
- ["gopkg.in/square/go-jose.v2/jwt", "NestedJSONWebToken", True, "ParseSignedAndEncrypted", "", "", "Argument[0]", "ReturnValue[0]", "taint", "manual"]
14+
- ["gopkg.in/square/go-jose.v2/jwt", "NestedJSONWebToken", True, "Decrypt", "", "", "Argument[-1]", "ReturnValue[0]", "taint", "manual"]

go/ql/lib/go.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import semmle.go.frameworks.GoMicro
5252
import semmle.go.frameworks.GoRestfulHttp
5353
import semmle.go.frameworks.Gqlgen
5454
import semmle.go.frameworks.Iris
55+
import semmle.go.frameworks.Jwt
5556
import semmle.go.frameworks.K8sIoApimachineryPkgRuntime
5657
import semmle.go.frameworks.K8sIoApiCoreV1
5758
import semmle.go.frameworks.K8sIoClientGo

go/ql/lib/semmle/go/frameworks/GoJose.qll

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,40 @@ private import semmle.go.security.HardcodedCredentials
99
private module GoJose {
1010
private class GoJoseKey extends HardcodedCredentials::Sink {
1111
GoJoseKey() {
12-
exists(Field f, string pkg |
13-
pkg =
14-
[
15-
package("github.com/square/go-jose", ""), package("github.com/go-jose/go-jose", ""),
16-
"gopkg.in/square/go-jose.v2"
17-
]
18-
|
19-
f.hasQualifiedName(pkg, ["Recipient", "SigningKey"], "Key") and
12+
exists(Field f |
13+
f.hasQualifiedName(goJosePackage(), ["Recipient", "SigningKey"], "Key") and
2014
f.getAWrite().getRhs() = this
2115
)
2216
}
2317
}
18+
19+
private string goJosePackage() {
20+
result =
21+
[
22+
package("github.com/square/go-jose", ""), package("github.com/go-jose/go-jose", ""),
23+
"gopkg.in/square/go-jose.v2"
24+
]
25+
}
26+
27+
/**
28+
* Provides classes and predicates for working with the `gopkg.in/square/go-jose/jwt` and
29+
* `github.com/go-jose/go-jose/jwt` packages.
30+
*/
31+
private module Jwt {
32+
private import semmle.go.security.MissingJwtSignatureCheckCustomizations::MissingJwtSignatureCheck
33+
34+
/** The method `JSONWebToken.Claims`. */
35+
private class GoJoseParseWithClaims extends JwtSafeParse {
36+
GoJoseParseWithClaims() {
37+
this.(Method).hasQualifiedName(goJoseJwtPackage(), "JSONWebToken", "Claims")
38+
}
39+
40+
override int getTokenArgNum() { result = -1 }
41+
}
42+
43+
/** Gets the package names `gopkg.in/square/go-jose/jwt` and `github.com/go-jose/go-jose/jwt`. */
44+
private string goJoseJwtPackage() {
45+
result = package(["gopkg.in/square/go-jose", "github.com/go-jose/go-jose"], "jwt")
46+
}
47+
}
2448
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Provides classes and predicates for working with the `github.com/golang-jwt/jwt` and
3+
* `github.com/dgrijalva/jwt-go` packages.
4+
*/
5+
6+
import go
7+
private import semmle.go.security.MissingJwtSignatureCheckCustomizations::MissingJwtSignatureCheck
8+
9+
/** The function `jwt.Parse` or the method `Parser.Parse`. */
10+
private class GolangJwtParse extends JwtSafeParse {
11+
GolangJwtParse() {
12+
this.hasQualifiedName(golangJwtPackage(), "Parse")
13+
or
14+
this.(Method).hasQualifiedName(golangJwtPackage(), "Parser", "Parse")
15+
}
16+
17+
override int getTokenArgNum() { result = 0 }
18+
}
19+
20+
/** The function `jwt.ParseWithClaims` or the method `Parser.ParseWithClaims`. */
21+
private class GolangJwtParseWithClaims extends JwtSafeParse {
22+
GolangJwtParseWithClaims() {
23+
this.hasQualifiedName(golangJwtPackage(), "ParseWithClaims")
24+
or
25+
this.(Method).hasQualifiedName(golangJwtPackage(), "Parser", "ParseWithClaims")
26+
}
27+
28+
override int getTokenArgNum() { result = 0 }
29+
}
30+
31+
/** The function `jwt.ParseFromRequest`. */
32+
private class GolangJwtParseFromRequest extends JwtSafeParse {
33+
GolangJwtParseFromRequest() {
34+
this.hasQualifiedName(golangJwtRequestPackage(), "ParseFromRequest")
35+
}
36+
37+
override int getTokenArgNum() { result = 0 }
38+
}
39+
40+
/** The function `jwt.ParseFromRequestWithClaims`. */
41+
private class GolangJwtParseFromRequestWithClaims extends JwtSafeParse {
42+
GolangJwtParseFromRequestWithClaims() {
43+
this.hasQualifiedName(golangJwtRequestPackage(), "ParseFromRequestWithClaims")
44+
}
45+
46+
override int getTokenArgNum() { result = 0 }
47+
}
48+
49+
/** Gets the pakcage names `github.com/golang-jwt/jwt` and `github.com/dgrijalva/jwt-go`. */
50+
private string golangJwtPackage() {
51+
result = package(["github.com/golang-jwt/jwt", "github.com/dgrijalva/jwt-go"], "")
52+
}
53+
54+
/** Gets the package names `github.com/golang-jwt/jwt/request` and `github.com/dgrijalva/jwt-go/request`. */
55+
private string golangJwtRequestPackage() {
56+
result = package(["github.com/golang-jwt/jwt", "github.com/dgrijalva/jwt-go"], "request")
57+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Provides a taint tracking flow for reasoning about JWT vulnerabilities.
3+
*
4+
* Note: for performance reasons, only import this file if `MissingJwtSignatureCheck::Config` or `MissingJwtSignatureCheck::Flow` are needed,
5+
* otherwise `MissingJwtSignatureCheckCustomizations` should be imported instead.
6+
*/
7+
8+
import go
9+
10+
/** Provides a taint-tracking flow for reasoning about JWT vulnerabilities. */
11+
module MissingJwtSignatureCheck {
12+
import MissingJwtSignatureCheckCustomizations::MissingJwtSignatureCheck
13+
14+
/** Config for reasoning about JWT vulnerabilities. */
15+
module Config implements DataFlow::ConfigSig {
16+
predicate isSource(DataFlow::Node source) {
17+
source instanceof Source and
18+
not SafeParse::flow(source, _)
19+
}
20+
21+
predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
22+
23+
predicate isAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
24+
any(AdditionalFlowStep s).step(nodeFrom, nodeTo)
25+
}
26+
}
27+
28+
/** Tracks taint flow for reasoning about JWT vulnerabilities. */
29+
module Flow = TaintTracking::Global<Config>;
30+
31+
private module SafeParseConfig implements DataFlow::ConfigSig {
32+
predicate isSource(DataFlow::Node source) { source instanceof Source }
33+
34+
predicate isSink(DataFlow::Node sink) { sink = any(JwtSafeParse jwtParse).getTokenArg() }
35+
36+
predicate isAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
37+
any(AdditionalFlowStep s).step(nodeFrom, nodeTo)
38+
}
39+
}
40+
41+
private module SafeParse = TaintTracking::Global<SafeParseConfig>;
42+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Provides default sources, sinks, and sanitizers for reasoning about
3+
* JWT vulnerabilities, as well as extension points for adding your own.
4+
*/
5+
6+
import go
7+
private import semmle.go.dataflow.ExternalFlow
8+
private import codeql.util.Unit
9+
10+
/**
11+
* Provides extension points for customizing the data-flow tracking configuration for reasoning
12+
* about JWT vulnerabilities.
13+
*/
14+
module MissingJwtSignatureCheck {
15+
/**
16+
* A data flow source for JWT vulnerabilities.
17+
*/
18+
abstract class Source extends DataFlow::Node { }
19+
20+
/**
21+
* A data flow sink for JWT vulnerabilities.
22+
*/
23+
abstract class Sink extends DataFlow::Node { }
24+
25+
/**
26+
* A sanitizer for JWT vulnerabilities.
27+
*/
28+
abstract class Sanitizer extends DataFlow::Node { }
29+
30+
/** An additional flow step for JWT vulnerabilities. */
31+
class AdditionalFlowStep extends Unit {
32+
/**
33+
* Holds if the step from `node1` to `node2` should be considered a flow
34+
* step for configurations related to JWT vulnerabilities.
35+
*/
36+
abstract predicate step(DataFlow::Node node1, DataFlow::Node node2);
37+
}
38+
39+
/** A function that parses and correctly validates a JWT token. */
40+
abstract class JwtSafeParse extends Function {
41+
/** Gets the position of the JWT argument in a call to this function. */
42+
abstract int getTokenArgNum();
43+
44+
/** Gets the JWT argument of a call to this function. */
45+
DataFlow::Node getTokenArg() {
46+
this.getTokenArgNum() != -1 and result = this.getACall().getArgument(this.getTokenArgNum())
47+
or
48+
this.getTokenArgNum() = -1 and result = this.getACall().getReceiver()
49+
}
50+
}
51+
52+
private class DefaultSource extends Source instanceof UntrustedFlowSource { }
53+
54+
private class DefaultSink extends Sink {
55+
DefaultSink() { sinkNode(this, "jwt") }
56+
}
57+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
2+
<qhelp>
3+
<overview>
4+
<p>Applications decoding a JSON Web Token (JWT) may be vulnerable when the
5+
signature is not correctly verified.</p>
6+
</overview>
7+
<recommendation>
8+
<p>Always verify the signature by using the appropriate methods provided by the JWT
9+
library, or use a library that verifies it by default.</p>
10+
</recommendation>
11+
<example>
12+
<p>The following (bad) example shows a case where a JWT is parsed without verifying the
13+
signature.</p>
14+
<sample src="MissingJwtSignatureCheckBad.go" />
15+
<p>The following (good) example uses the appropriate function for parsing a JWT
16+
and verifying its signature.</p>
17+
<sample src="MissingJwtSignatureCheckGood.go" />
18+
</example>
19+
<references>
20+
<li>JWT IO: <a href="https://jwt.io/introduction">Introduction to JSON Web Tokens</a>.</li>
21+
<li>jwt-go: <a href="https://pkg.go.dev/github.com/golang-jwt/jwt/v5">Documentation</a>.</li>
22+
<li>Go JOSE: <a href="https://pkg.go.dev/github.com/go-jose/go-jose/v3">Documentation</a>.</li>
23+
</references>
24+
25+
</qhelp>

0 commit comments

Comments
 (0)