99 "net"
1010 "net/http"
1111 "net/url"
12+ "strings"
1213 "time"
1314
1415 "github.com/golang-jwt/jwt/v5"
@@ -52,38 +53,39 @@ func (s Scanner) Keywords() []string {
5253 }
5354}
5455
56+ var jwtOptions = []jwt.ParserOption {
57+ jwt .WithValidMethods ([]string {
58+ // HMAC-based algorithms
59+ // jwt.SigningMethodHS256.Alg(),
60+ // jwt.SigningMethodHS384.Alg(),
61+ // jwt.SigningMethodHS512.Alg(),
62+
63+ // Public key-based algorithms
64+ jwt .SigningMethodRS256 .Alg (),
65+ jwt .SigningMethodRS384 .Alg (),
66+ jwt .SigningMethodRS512 .Alg (),
67+ jwt .SigningMethodEdDSA .Alg (),
68+ jwt .SigningMethodES256 .Alg (),
69+ jwt .SigningMethodES384 .Alg (),
70+ jwt .SigningMethodES512 .Alg (),
71+ jwt .SigningMethodPS256 .Alg (),
72+ jwt .SigningMethodPS384 .Alg (),
73+ jwt .SigningMethodPS512 .Alg (),
74+ }),
75+ jwt .WithIssuedAt (),
76+ jwt .WithPaddingAllowed (),
77+ jwt .WithLeeway (time .Minute ),
78+ }
79+
80+ var jwtParser = jwt .NewParser (jwtOptions ... )
81+
82+ var jwtValidator = jwt .NewValidator (jwtOptions ... )
83+
5584// FromData will find and optionally verify JWT secrets in a given set of bytes.
5685func (s Scanner ) FromData (ctx context.Context , verify bool , data []byte ) (results []detectors.Result , err error ) {
5786 client := cmp .Or (s .client , common .SaneHttpClient ())
5887 seenMatches := make (map [string ]struct {})
5988
60- var validator * jwt.Validator = nil
61- if verify {
62- validator = jwt .NewValidator (
63- jwt .WithValidMethods ([]string {
64- // HMAC-based algorithms
65- jwt .SigningMethodHS256 .Alg (),
66- jwt .SigningMethodHS384 .Alg (),
67- jwt .SigningMethodHS512 .Alg (),
68-
69- // Public key-based algorithms
70- jwt .SigningMethodRS256 .Alg (),
71- jwt .SigningMethodRS384 .Alg (),
72- jwt .SigningMethodRS512 .Alg (),
73- jwt .SigningMethodEdDSA .Alg (),
74- jwt .SigningMethodES256 .Alg (),
75- jwt .SigningMethodES384 .Alg (),
76- jwt .SigningMethodES512 .Alg (),
77- jwt .SigningMethodPS256 .Alg (),
78- jwt .SigningMethodPS384 .Alg (),
79- jwt .SigningMethodPS512 .Alg (),
80- }),
81- jwt .WithIssuedAt (),
82- jwt .WithPaddingAllowed (),
83- jwt .WithLeeway (time .Minute ),
84- )
85- }
86-
8789 for _ , matchGroups := range keyPat .FindAllStringSubmatch (string (data ), - 1 ) {
8890 match := matchGroups [1 ]
8991
@@ -93,12 +95,28 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
9395 seenMatches [match ] = struct {}{}
9496
9597 claims := jwt.MapClaims {}
96- parsedToken , _ , err := jwt . NewParser ( jwt . WithPaddingAllowed ()) .ParseUnverified (match , claims )
97- if err != nil {
98+ parsedToken , tokenParts , err := jwtParser .ParseUnverified (match , claims )
99+ if err != nil || len ( tokenParts ) != 3 {
98100 // skip malformed tokens; no need to do claims validation or signature verification
99101 continue
100102 }
101103
104+ switch parsedToken .Method .Alg () {
105+ case "HS256" , "HS384" , "HS512" :
106+ // The JWT *might* be valid, but we can't in general do signature verification on HMAC-based algorithms.
107+ // We don't have a suitable status to represent this situation in trufflehog.
108+ // (The `unknown` status is intended to indicate that an error occurred to to external environment conditions, like trannsient network errors.)
109+ // So instead, to avoid possible false positives, totally skip HMAC-based JWTs; don't even create results for them.
110+ continue
111+ }
112+
113+ // Decode signature
114+ parsedToken .Signature , err = jwtParser .DecodeSegment (tokenParts [2 ])
115+ if err != nil {
116+ // skip JWTs with malformed signatures
117+ continue
118+ }
119+
102120 issString , _ := claims .GetIssuer ()
103121
104122 iatString := ""
@@ -127,7 +145,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
127145 }
128146
129147 if verify {
130- isVerified , verificationErr := verifyJWT (ctx , client , validator , parsedToken )
148+ isVerified , verificationErr := verifyJWT (ctx , client , tokenParts , parsedToken )
131149 s1 .Verified = isVerified
132150 s1 .SetVerificationError (verificationErr , match )
133151 }
@@ -194,21 +212,12 @@ func limitReader(reader io.Reader) io.Reader {
194212//
195213// - If the JWT uses public key cryptography and the OIDC Discovery protocol, we can fetch the public key and perform signature verification
196214// - In all cases, we can perform claims validation (e.g., checking expiration time) and sometimes get a definite answer that a JWT is *not* live
197- func verifyJWT (ctx context.Context , client * http.Client , validator * jwt. Validator , parsedToken * jwt.Token ) (bool , error ) {
198- if err := validator .Validate (parsedToken .Claims ); err != nil {
215+ func verifyJWT (ctx context.Context , client * http.Client , tokenParts [] string , parsedToken * jwt.Token ) (bool , error ) {
216+ if err := jwtValidator .Validate (parsedToken .Claims ); err != nil {
199217 // though we have not checked the signature, the token is definitely invalid
200218 return false , nil
201219 }
202220
203- switch parsedToken .Method .Alg () {
204- case "HS256" , "HS384" , "HS512" :
205- // The JWT *might* be valid, but we can't in general do signature verification on HMAC-based algorithms.
206- // We don't have a suitable status to represent this situation in trufflehog.
207- // (The `unknown` status is intended to indicate that an error occurred to to external environment conditions, like trannsient network errors.)
208- // So instead, to avoid possible false positives, we choose to give this JWT `unverified` status, even though that's not technically correct.
209- return false , nil
210- }
211-
212221 // Use the OIDC Discovery protocol to fetch the public signing key,
213222 // being careful to avoid possible DoS from a potentially malicious JWKS server.
214223 issuer , err := parsedToken .Claims .GetIssuer ()
@@ -285,14 +294,13 @@ func verifyJWT(ctx context.Context, client *http.Client, validator *jwt.Validato
285294 return false , fmt .Errorf ("failed to export matching key: %w" , err )
286295 }
287296
288- err = parsedToken .Method .Verify (parsedToken .Raw , parsedToken .Signature , rawMatchingKey )
289-
297+ err = parsedToken .Method .Verify (strings .Join (tokenParts [0 :2 ], "." ), parsedToken .Signature , rawMatchingKey )
290298 if err != nil {
291299 // signature invalid
292300 return false , nil
293301 }
294302
295- // signature valid and claims check out
303+ // signature valid and claims check out
296304 return true , nil
297305}
298306
0 commit comments