From 2db5dc802ffaef754d786ea02b65c184601b037b Mon Sep 17 00:00:00 2001 From: Mateus Caruccio Date: Mon, 17 Nov 2025 12:45:21 +0000 Subject: [PATCH] Fix CVE-2025-30204/GHSA-mh63-6h87-95cp --- jwt_test.go | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++ parser.go | 38 ++++++++++++++++++++--- 2 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 jwt_test.go diff --git a/jwt_test.go b/jwt_test.go new file mode 100644 index 00000000..a31e5509 --- /dev/null +++ b/jwt_test.go @@ -0,0 +1,89 @@ +package jwt + +import ( + "testing" +) + +func TestSplitToken(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + expected []string + isValid bool + }{ + { + name: "valid token with three parts", + input: "header.claims.signature", + expected: []string{"header", "claims", "signature"}, + isValid: true, + }, + { + name: "invalid token with two parts only", + input: "header.claims", + expected: nil, + isValid: false, + }, + { + name: "invalid token with one part only", + input: "header", + expected: nil, + isValid: false, + }, + { + name: "invalid token with extra delimiter", + input: "header.claims.signature.extra", + expected: nil, + isValid: false, + }, + { + name: "invalid empty token", + input: "", + expected: nil, + isValid: false, + }, + { + name: "valid token with empty parts", + input: "..signature", + expected: []string{"", "", "signature"}, + isValid: true, + }, + { + // We are just splitting the token into parts, so we don't care about the actual values. + // It is up to the caller to validate the parts. + name: "valid token with all parts empty", + input: "..", + expected: []string{"", "", ""}, + isValid: true, + }, + { + name: "invalid token with just delimiters and extra part", + input: "...", + expected: nil, + isValid: false, + }, + { + name: "invalid token with many delimiters", + input: "header.claims.signature..................", + expected: nil, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parts, ok := splitToken(tt.input) + if ok != tt.isValid { + t.Errorf("expected %t, got %t", tt.isValid, ok) + } + if ok { + for i, part := range tt.expected { + if parts[i] != part { + t.Errorf("expected %s, got %s", part, parts[i]) + } + } + } + }) + } +} \ No newline at end of file diff --git a/parser.go b/parser.go index d6901d9a..ea82e712 100644 --- a/parser.go +++ b/parser.go @@ -7,6 +7,9 @@ import ( "strings" ) +// backport https://github.com/golang-jwt/jwt/commit/0951d184286dece21f73c85673fd308786ffe9c3 +const tokenDelimiter = "." + type Parser struct { ValidMethods []string // If populated, only these methods will be considered valid UseJSONNumber bool // Use JSON Number format in JSON decoder @@ -94,11 +97,11 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf // been checked previously in the stack) and you want to extract values from // it. func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Token, parts []string, err error) { - parts = strings.Split(tokenString, ".") - if len(parts) != 3 { - return nil, parts, NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed) + var ok bool + parts, ok = splitToken(tokenString) + if !ok { + return nil, nil, NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed) } - token = &Token{Raw: tokenString} // parse Header @@ -146,3 +149,30 @@ func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Toke return token, parts, nil } + +// splitToken splits a token string into three parts: header, claims, and signature. It will only +// return true if the token contains exactly two delimiters and three parts. In all other cases, it +// will return nil parts and false. +func splitToken(token string) ([]string, bool) { + parts := make([]string, 3) + header, remain, ok := strings.Cut(token, tokenDelimiter) + if !ok { + return nil, false + } + parts[0] = header + claims, remain, ok := strings.Cut(remain, tokenDelimiter) + if !ok { + return nil, false + } + parts[1] = claims + // One more cut to ensure the signature is the last part of the token and there are no more + // delimiters. This avoids an issue where malicious input could contain additional delimiters + // causing unecessary overhead parsing tokens. + signature, _, unexpected := strings.Cut(remain, tokenDelimiter) + if unexpected { + return nil, false + } + parts[2] = signature + + return parts, true +}