Skip to content

Commit 2f0e9ad

Browse files
mfridmanoxisto
authored andcommittedMar 21, 2025
Backporting 0951d18 to v4
1 parent 7b1c1c0 commit 2f0e9ad

File tree

2 files changed

+122
-3
lines changed

2 files changed

+122
-3
lines changed
 

‎jwt_test.go

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package jwt
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestSplitToken(t *testing.T) {
8+
t.Parallel()
9+
10+
tests := []struct {
11+
name string
12+
input string
13+
expected []string
14+
isValid bool
15+
}{
16+
{
17+
name: "valid token with three parts",
18+
input: "header.claims.signature",
19+
expected: []string{"header", "claims", "signature"},
20+
isValid: true,
21+
},
22+
{
23+
name: "invalid token with two parts only",
24+
input: "header.claims",
25+
expected: nil,
26+
isValid: false,
27+
},
28+
{
29+
name: "invalid token with one part only",
30+
input: "header",
31+
expected: nil,
32+
isValid: false,
33+
},
34+
{
35+
name: "invalid token with extra delimiter",
36+
input: "header.claims.signature.extra",
37+
expected: nil,
38+
isValid: false,
39+
},
40+
{
41+
name: "invalid empty token",
42+
input: "",
43+
expected: nil,
44+
isValid: false,
45+
},
46+
{
47+
name: "valid token with empty parts",
48+
input: "..signature",
49+
expected: []string{"", "", "signature"},
50+
isValid: true,
51+
},
52+
{
53+
// We are just splitting the token into parts, so we don't care about the actual values.
54+
// It is up to the caller to validate the parts.
55+
name: "valid token with all parts empty",
56+
input: "..",
57+
expected: []string{"", "", ""},
58+
isValid: true,
59+
},
60+
{
61+
name: "invalid token with just delimiters and extra part",
62+
input: "...",
63+
expected: nil,
64+
isValid: false,
65+
},
66+
{
67+
name: "invalid token with many delimiters",
68+
input: "header.claims.signature..................",
69+
expected: nil,
70+
isValid: false,
71+
},
72+
}
73+
74+
for _, tt := range tests {
75+
t.Run(tt.name, func(t *testing.T) {
76+
parts, ok := splitToken(tt.input)
77+
if ok != tt.isValid {
78+
t.Errorf("expected %t, got %t", tt.isValid, ok)
79+
}
80+
if ok {
81+
for i, part := range tt.expected {
82+
if parts[i] != part {
83+
t.Errorf("expected %s, got %s", part, parts[i])
84+
}
85+
}
86+
}
87+
})
88+
}
89+
}

‎parser.go

+33-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"strings"
88
)
99

10+
const tokenDelimiter = "."
11+
1012
type Parser struct {
1113
// If populated, only these methods will be considered valid.
1214
//
@@ -122,9 +124,10 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
122124
// It's only ever useful in cases where you know the signature is valid (because it has
123125
// been checked previously in the stack) and you want to extract values from it.
124126
func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Token, parts []string, err error) {
125-
parts = strings.Split(tokenString, ".")
126-
if len(parts) != 3 {
127-
return nil, parts, NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed)
127+
var ok bool
128+
parts, ok = splitToken(tokenString)
129+
if !ok {
130+
return nil, nil, NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed)
128131
}
129132

130133
token = &Token{Raw: tokenString}
@@ -174,3 +177,30 @@ func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Toke
174177

175178
return token, parts, nil
176179
}
180+
181+
// splitToken splits a token string into three parts: header, claims, and signature. It will only
182+
// return true if the token contains exactly two delimiters and three parts. In all other cases, it
183+
// will return nil parts and false.
184+
func splitToken(token string) ([]string, bool) {
185+
parts := make([]string, 3)
186+
header, remain, ok := strings.Cut(token, tokenDelimiter)
187+
if !ok {
188+
return nil, false
189+
}
190+
parts[0] = header
191+
claims, remain, ok := strings.Cut(remain, tokenDelimiter)
192+
if !ok {
193+
return nil, false
194+
}
195+
parts[1] = claims
196+
// One more cut to ensure the signature is the last part of the token and there are no more
197+
// delimiters. This avoids an issue where malicious input could contain additional delimiters
198+
// causing unecessary overhead parsing tokens.
199+
signature, _, unexpected := strings.Cut(remain, tokenDelimiter)
200+
if unexpected {
201+
return nil, false
202+
}
203+
parts[2] = signature
204+
205+
return parts, true
206+
}

0 commit comments

Comments
 (0)
Please sign in to comment.