Skip to content

Commit 4323f8e

Browse files
committed
jws: improve fix for CVE-2025-22868
The fix for CVE-2025-22868 relies on strings.Count, which isn't ideal because it precludes failing fast when the token contains an unexpected number of periods. Moreover, Verify still allocates more than necessary. Eschew strings.Count in favor of strings.Cut. Some benchmark results: goos: darwin goarch: amd64 pkg: golang.org/x/oauth2/jws cpu: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz │ old │ new │ │ sec/op │ sec/op vs base │ Verify/full_of_periods-8 24988.00n ± 1% 53.70n ± 0% -99.79% (p=0.000 n=20) Verify/two_trailing_periods-8 3.546m ± 2% 3.420m ± 1% -3.55% (p=0.000 n=20) geomean 297.7µ 13.55µ -95.45% │ old │ new │ │ B/op │ B/op vs base │ Verify/full_of_periods-8 16.00 ± 0% 16.00 ± 0% ~ (p=1.000 n=20) ¹ Verify/two_trailing_periods-8 2.001Mi ± 0% 1.001Mi ± 0% -49.98% (p=0.000 n=20) geomean 5.658Ki 4.002Ki -29.27% ¹ all samples are equal │ old │ new │ │ allocs/op │ allocs/op vs base │ Verify/full_of_periods-8 1.000 ± 0% 1.000 ± 0% ~ (p=1.000 n=20) ¹ Verify/two_trailing_periods-8 12.000 ± 0% 9.000 ± 0% -25.00% (p=0.000 n=20) geomean 3.464 3.000 -13.40% ¹ all samples are equal Also, remove all remaining calls to strings.Split.
1 parent bd36f69 commit 4323f8e

File tree

2 files changed

+50
-11
lines changed

2 files changed

+50
-11
lines changed

jws/jws.go

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,13 @@ func (h *Header) encode() (string, error) {
116116
// Decode decodes a claim set from a JWS payload.
117117
func Decode(payload string) (*ClaimSet, error) {
118118
// decode returned id token to get expiry
119-
s := strings.Split(payload, ".")
120-
if len(s) < 2 {
119+
headerClaims, _, ok := split(payload)
120+
if !ok {
121121
// TODO(jbd): Provide more context about the error.
122122
return nil, errors.New("jws: invalid token received")
123123
}
124-
decoded, err := base64.RawURLEncoding.DecodeString(s[1])
124+
_, claims, _ := strings.Cut(headerClaims, ".") // guaranteed to succeed by split
125+
decoded, err := base64.RawURLEncoding.DecodeString(claims)
125126
if err != nil {
126127
return nil, err
127128
}
@@ -165,13 +166,11 @@ func Encode(header *Header, c *ClaimSet, key *rsa.PrivateKey) (string, error) {
165166
// Verify tests whether the provided JWT token's signature was produced by the private key
166167
// associated with the supplied public key.
167168
func Verify(token string, key *rsa.PublicKey) error {
168-
if strings.Count(token, ".") != 2 {
169+
signedContent, sig, ok := split(token)
170+
if !ok {
169171
return errors.New("jws: invalid token received, token must have 3 parts")
170172
}
171-
172-
parts := strings.SplitN(token, ".", 3)
173-
signedContent := parts[0] + "." + parts[1]
174-
signatureString, err := base64.RawURLEncoding.DecodeString(parts[2])
173+
signatureString, err := base64.RawURLEncoding.DecodeString(sig)
175174
if err != nil {
176175
return err
177176
}
@@ -180,3 +179,23 @@ func Verify(token string, key *rsa.PublicKey) error {
180179
h.Write([]byte(signedContent))
181180
return rsa.VerifyPKCS1v15(key, crypto.SHA256, h.Sum(nil), signatureString)
182181
}
182+
183+
func split(token string) (headerClaims, sig string, ok bool) {
184+
const delim = '.'
185+
i := strings.IndexByte(token, delim)
186+
if i < 0 { // no period
187+
return
188+
}
189+
j := strings.IndexByte(token[i+1:], delim)
190+
if j < 0 { // only one period
191+
return
192+
}
193+
k := i + j + 1
194+
headerClaims = token[:k]
195+
sig, _, found := strings.Cut(token[k+1:], string(delim))
196+
if found { // more than two periods
197+
return "", "", false
198+
}
199+
ok = true
200+
return
201+
}

jws/jws_test.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,29 @@ func TestSignAndVerify(t *testing.T) {
4141
}
4242

4343
func TestVerifyFailsOnMalformedClaim(t *testing.T) {
44-
err := Verify("abc.def", nil)
45-
if err == nil {
46-
t.Error("got no errors; want improperly formed JWT not to be verified")
44+
cases := []struct {
45+
desc string
46+
token string
47+
}{
48+
{
49+
desc: "no periods",
50+
token: "aa",
51+
}, {
52+
desc: "only one period",
53+
token: "a.a",
54+
}, {
55+
desc: "more than two periods",
56+
token: "a.a.a.a",
57+
},
58+
}
59+
for _, tc := range cases {
60+
f := func(t *testing.T) {
61+
err := Verify(tc.token, nil)
62+
if err == nil {
63+
t.Error("got no errors; want improperly formed JWT not to be verified")
64+
}
65+
}
66+
t.Run(tc.desc, f)
4767
}
4868
}
4969

0 commit comments

Comments
 (0)