Skip to content

Commit 9358574

Browse files
Allow strict base64 decoding (#259)
By default base64 decoder works in non-strict mode which allows tweaking signatures having padding without failing validation. This creates a potential problem if application treats token value as an identifier. For example ES256 signature has length of 64 bytes and two padding symbols (stripped by default). Therefore its base64-encoded value can only end with A, Q, g and w. In non-strict mode last symbol could be tweaked resulting in 16 distinct token values having the same signature and passing validation. This change adds backward-compatible global config variable DecodeStrict (similar to existing DecodePaddingAllowed) that enables strict base64 decoder mode. See also golang/go#15656. Signed-off-by: Alexander Yastrebov <[email protected]>
1 parent 2f0984a commit 9358574

File tree

2 files changed

+107
-6
lines changed

2 files changed

+107
-6
lines changed

parser_test.go

+94-4
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ var setPaddingTestData = []struct {
489489
tokenString string
490490
claims jwt.Claims
491491
paddedDecode bool
492+
strictDecode bool
492493
signingMethod jwt.SigningMethod
493494
keyfunc jwt.Keyfunc
494495
valid bool
@@ -547,19 +548,108 @@ var setPaddingTestData = []struct {
547548
keyfunc: paddedKeyFunc,
548549
valid: true,
549550
},
551+
// DecodeStrict tests, DecodePaddingAllowed=false
552+
{
553+
name: "Validated non-padded token with padding disabled, non-strict decode, non-tweaked signature",
554+
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ.bI15h-7mN0f-2diX5I4ErgNQy1uM-rJS5Sz7O0iTWtWSBxY1h6wy8Ywxe5EZTEO6GiIfk7Lk-72Ex-c5aA40QKhPwWB9BJ8O_LfKpezUVBOn0jRItDnVdsk4ccl2zsOVkbA4U4QvdrSbOYMbwoRHzDXfTFpoeMWtn3ez0aENJ8dh4E1echHp5ByI9Pu2aBsvM1WVcMt_BySweCL3f4T7jNZeXDr7Txd00yUd2gdsHYPjXorOvsgaBKN5GLsWd1zIY5z-2gCC8CRSN-IJ4NNX5ifh7l-bOXE2q7szTqa9pvyE9y6TQJhNMSE2FotRce_TOPBWgGpQ-K2I7E8x7wZ8O" +
555+
"g",
556+
claims: nil,
557+
paddedDecode: false,
558+
strictDecode: false,
559+
signingMethod: jwt.SigningMethodRS256,
560+
keyfunc: defaultKeyFunc,
561+
valid: true,
562+
},
563+
{
564+
name: "Validated non-padded token with padding disabled, non-strict decode, tweaked signature",
565+
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ.bI15h-7mN0f-2diX5I4ErgNQy1uM-rJS5Sz7O0iTWtWSBxY1h6wy8Ywxe5EZTEO6GiIfk7Lk-72Ex-c5aA40QKhPwWB9BJ8O_LfKpezUVBOn0jRItDnVdsk4ccl2zsOVkbA4U4QvdrSbOYMbwoRHzDXfTFpoeMWtn3ez0aENJ8dh4E1echHp5ByI9Pu2aBsvM1WVcMt_BySweCL3f4T7jNZeXDr7Txd00yUd2gdsHYPjXorOvsgaBKN5GLsWd1zIY5z-2gCC8CRSN-IJ4NNX5ifh7l-bOXE2q7szTqa9pvyE9y6TQJhNMSE2FotRce_TOPBWgGpQ-K2I7E8x7wZ8O" +
566+
"h",
567+
claims: nil,
568+
paddedDecode: false,
569+
strictDecode: false,
570+
signingMethod: jwt.SigningMethodRS256,
571+
keyfunc: defaultKeyFunc,
572+
valid: true,
573+
},
574+
{
575+
name: "Validated non-padded token with padding disabled, strict decode, non-tweaked signature",
576+
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ.bI15h-7mN0f-2diX5I4ErgNQy1uM-rJS5Sz7O0iTWtWSBxY1h6wy8Ywxe5EZTEO6GiIfk7Lk-72Ex-c5aA40QKhPwWB9BJ8O_LfKpezUVBOn0jRItDnVdsk4ccl2zsOVkbA4U4QvdrSbOYMbwoRHzDXfTFpoeMWtn3ez0aENJ8dh4E1echHp5ByI9Pu2aBsvM1WVcMt_BySweCL3f4T7jNZeXDr7Txd00yUd2gdsHYPjXorOvsgaBKN5GLsWd1zIY5z-2gCC8CRSN-IJ4NNX5ifh7l-bOXE2q7szTqa9pvyE9y6TQJhNMSE2FotRce_TOPBWgGpQ-K2I7E8x7wZ8O" +
577+
"g",
578+
claims: nil,
579+
paddedDecode: false,
580+
strictDecode: true,
581+
signingMethod: jwt.SigningMethodRS256,
582+
keyfunc: defaultKeyFunc,
583+
valid: true,
584+
},
585+
{
586+
name: "Error for non-padded token with padding disabled, strict decode, tweaked signature",
587+
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ.bI15h-7mN0f-2diX5I4ErgNQy1uM-rJS5Sz7O0iTWtWSBxY1h6wy8Ywxe5EZTEO6GiIfk7Lk-72Ex-c5aA40QKhPwWB9BJ8O_LfKpezUVBOn0jRItDnVdsk4ccl2zsOVkbA4U4QvdrSbOYMbwoRHzDXfTFpoeMWtn3ez0aENJ8dh4E1echHp5ByI9Pu2aBsvM1WVcMt_BySweCL3f4T7jNZeXDr7Txd00yUd2gdsHYPjXorOvsgaBKN5GLsWd1zIY5z-2gCC8CRSN-IJ4NNX5ifh7l-bOXE2q7szTqa9pvyE9y6TQJhNMSE2FotRce_TOPBWgGpQ-K2I7E8x7wZ8O" +
588+
"h",
589+
claims: nil,
590+
paddedDecode: false,
591+
strictDecode: true,
592+
signingMethod: jwt.SigningMethodRS256,
593+
keyfunc: defaultKeyFunc,
594+
valid: false,
595+
},
596+
// DecodeStrict tests, DecodePaddingAllowed=true
597+
{
598+
name: "Validated padded token with padding enabled, non-strict decode, non-tweaked signature",
599+
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3gr" +
600+
"w==",
601+
claims: nil,
602+
paddedDecode: true,
603+
strictDecode: false,
604+
signingMethod: jwt.SigningMethodES256,
605+
keyfunc: paddedKeyFunc,
606+
valid: true,
607+
},
608+
{
609+
name: "Validated padded token with padding enabled, non-strict decode, tweaked signature",
610+
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3gr" +
611+
"x==",
612+
claims: nil,
613+
paddedDecode: true,
614+
strictDecode: false,
615+
signingMethod: jwt.SigningMethodES256,
616+
keyfunc: paddedKeyFunc,
617+
valid: true,
618+
},
619+
{
620+
name: "Validated padded token with padding enabled, strict decode, non-tweaked signature",
621+
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3gr" +
622+
"w==",
623+
claims: nil,
624+
paddedDecode: true,
625+
strictDecode: true,
626+
signingMethod: jwt.SigningMethodES256,
627+
keyfunc: paddedKeyFunc,
628+
valid: true,
629+
},
630+
{
631+
name: "Error for padded token with padding enabled, strict decode, tweaked signature",
632+
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3gr" +
633+
"x==",
634+
claims: nil,
635+
paddedDecode: true,
636+
strictDecode: true,
637+
signingMethod: jwt.SigningMethodES256,
638+
keyfunc: paddedKeyFunc,
639+
valid: false,
640+
},
550641
}
551642

552643
// Extension of Parsing, this is to test out functionality specific to switching codecs with padding.
553644
func TestSetPadding(t *testing.T) {
554645
for _, data := range setPaddingTestData {
555646
t.Run(data.name, func(t *testing.T) {
556-
557-
// If the token string is blank, use helper function to generate string
558647
jwt.DecodePaddingAllowed = data.paddedDecode
648+
jwt.DecodeStrict = data.strictDecode
559649

650+
// If the token string is blank, use helper function to generate string
560651
if data.tokenString == "" {
561652
data.tokenString = signToken(data.claims, data.signingMethod)
562-
563653
}
564654

565655
// Parse the token
@@ -581,7 +671,7 @@ func TestSetPadding(t *testing.T) {
581671

582672
})
583673
jwt.DecodePaddingAllowed = false
584-
674+
jwt.DecodeStrict = false
585675
}
586676
}
587677

token.go

+13-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ import (
1414
// To use the non-recommended decoding, set this boolean to `true` prior to using this package.
1515
var DecodePaddingAllowed bool
1616

17+
// DecodeStrict will switch the codec used for decoding JWTs into strict mode.
18+
// In this mode, the decoder requires that trailing padding bits are zero, as described in RFC 4648 section 3.5.
19+
// Note that this is a global variable, and updating it will change the behavior on a package level, and is also NOT go-routine safe.
20+
// To use strict decoding, set this boolean to `true` prior to using this package.
21+
var DecodeStrict bool
22+
1723
// TimeFunc provides the current time when parsing token to validate "exp" claim (expiration time).
1824
// You can override it to use another time value. This is useful for testing or if your
1925
// server uses a different time zone than your tokens.
@@ -121,12 +127,17 @@ func EncodeSegment(seg []byte) string {
121127
// Deprecated: In a future release, we will demote this function to a non-exported function, since it
122128
// should only be used internally
123129
func DecodeSegment(seg string) ([]byte, error) {
130+
encoding := base64.RawURLEncoding
131+
124132
if DecodePaddingAllowed {
125133
if l := len(seg) % 4; l > 0 {
126134
seg += strings.Repeat("=", 4-l)
127135
}
128-
return base64.URLEncoding.DecodeString(seg)
136+
encoding = base64.URLEncoding
129137
}
130138

131-
return base64.RawURLEncoding.DecodeString(seg)
139+
if DecodeStrict {
140+
encoding = encoding.Strict()
141+
}
142+
return encoding.DecodeString(seg)
132143
}

0 commit comments

Comments
 (0)