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 https://github.com/golang/go/issues/15656.

Signed-off-by: Alexander Yastrebov <yastrebov.alex@gmail.com>
This commit is contained in:
Alexander Yastrebov 2022-12-09 18:04:03 +01:00 committed by GitHub
parent 2f0984a28b
commit 9358574a7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 107 additions and 6 deletions

View File

@ -489,6 +489,7 @@ var setPaddingTestData = []struct {
tokenString string tokenString string
claims jwt.Claims claims jwt.Claims
paddedDecode bool paddedDecode bool
strictDecode bool
signingMethod jwt.SigningMethod signingMethod jwt.SigningMethod
keyfunc jwt.Keyfunc keyfunc jwt.Keyfunc
valid bool valid bool
@ -547,19 +548,108 @@ var setPaddingTestData = []struct {
keyfunc: paddedKeyFunc, keyfunc: paddedKeyFunc,
valid: true, valid: true,
}, },
// DecodeStrict tests, DecodePaddingAllowed=false
{
name: "Validated non-padded token with padding disabled, non-strict decode, non-tweaked signature",
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ.bI15h-7mN0f-2diX5I4ErgNQy1uM-rJS5Sz7O0iTWtWSBxY1h6wy8Ywxe5EZTEO6GiIfk7Lk-72Ex-c5aA40QKhPwWB9BJ8O_LfKpezUVBOn0jRItDnVdsk4ccl2zsOVkbA4U4QvdrSbOYMbwoRHzDXfTFpoeMWtn3ez0aENJ8dh4E1echHp5ByI9Pu2aBsvM1WVcMt_BySweCL3f4T7jNZeXDr7Txd00yUd2gdsHYPjXorOvsgaBKN5GLsWd1zIY5z-2gCC8CRSN-IJ4NNX5ifh7l-bOXE2q7szTqa9pvyE9y6TQJhNMSE2FotRce_TOPBWgGpQ-K2I7E8x7wZ8O" +
"g",
claims: nil,
paddedDecode: false,
strictDecode: false,
signingMethod: jwt.SigningMethodRS256,
keyfunc: defaultKeyFunc,
valid: true,
},
{
name: "Validated non-padded token with padding disabled, non-strict decode, tweaked signature",
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ.bI15h-7mN0f-2diX5I4ErgNQy1uM-rJS5Sz7O0iTWtWSBxY1h6wy8Ywxe5EZTEO6GiIfk7Lk-72Ex-c5aA40QKhPwWB9BJ8O_LfKpezUVBOn0jRItDnVdsk4ccl2zsOVkbA4U4QvdrSbOYMbwoRHzDXfTFpoeMWtn3ez0aENJ8dh4E1echHp5ByI9Pu2aBsvM1WVcMt_BySweCL3f4T7jNZeXDr7Txd00yUd2gdsHYPjXorOvsgaBKN5GLsWd1zIY5z-2gCC8CRSN-IJ4NNX5ifh7l-bOXE2q7szTqa9pvyE9y6TQJhNMSE2FotRce_TOPBWgGpQ-K2I7E8x7wZ8O" +
"h",
claims: nil,
paddedDecode: false,
strictDecode: false,
signingMethod: jwt.SigningMethodRS256,
keyfunc: defaultKeyFunc,
valid: true,
},
{
name: "Validated non-padded token with padding disabled, strict decode, non-tweaked signature",
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ.bI15h-7mN0f-2diX5I4ErgNQy1uM-rJS5Sz7O0iTWtWSBxY1h6wy8Ywxe5EZTEO6GiIfk7Lk-72Ex-c5aA40QKhPwWB9BJ8O_LfKpezUVBOn0jRItDnVdsk4ccl2zsOVkbA4U4QvdrSbOYMbwoRHzDXfTFpoeMWtn3ez0aENJ8dh4E1echHp5ByI9Pu2aBsvM1WVcMt_BySweCL3f4T7jNZeXDr7Txd00yUd2gdsHYPjXorOvsgaBKN5GLsWd1zIY5z-2gCC8CRSN-IJ4NNX5ifh7l-bOXE2q7szTqa9pvyE9y6TQJhNMSE2FotRce_TOPBWgGpQ-K2I7E8x7wZ8O" +
"g",
claims: nil,
paddedDecode: false,
strictDecode: true,
signingMethod: jwt.SigningMethodRS256,
keyfunc: defaultKeyFunc,
valid: true,
},
{
name: "Error for non-padded token with padding disabled, strict decode, tweaked signature",
tokenString: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJwYWRkZWRiYXIifQ.bI15h-7mN0f-2diX5I4ErgNQy1uM-rJS5Sz7O0iTWtWSBxY1h6wy8Ywxe5EZTEO6GiIfk7Lk-72Ex-c5aA40QKhPwWB9BJ8O_LfKpezUVBOn0jRItDnVdsk4ccl2zsOVkbA4U4QvdrSbOYMbwoRHzDXfTFpoeMWtn3ez0aENJ8dh4E1echHp5ByI9Pu2aBsvM1WVcMt_BySweCL3f4T7jNZeXDr7Txd00yUd2gdsHYPjXorOvsgaBKN5GLsWd1zIY5z-2gCC8CRSN-IJ4NNX5ifh7l-bOXE2q7szTqa9pvyE9y6TQJhNMSE2FotRce_TOPBWgGpQ-K2I7E8x7wZ8O" +
"h",
claims: nil,
paddedDecode: false,
strictDecode: true,
signingMethod: jwt.SigningMethodRS256,
keyfunc: defaultKeyFunc,
valid: false,
},
// DecodeStrict tests, DecodePaddingAllowed=true
{
name: "Validated padded token with padding enabled, non-strict decode, non-tweaked signature",
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3gr" +
"w==",
claims: nil,
paddedDecode: true,
strictDecode: false,
signingMethod: jwt.SigningMethodES256,
keyfunc: paddedKeyFunc,
valid: true,
},
{
name: "Validated padded token with padding enabled, non-strict decode, tweaked signature",
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3gr" +
"x==",
claims: nil,
paddedDecode: true,
strictDecode: false,
signingMethod: jwt.SigningMethodES256,
keyfunc: paddedKeyFunc,
valid: true,
},
{
name: "Validated padded token with padding enabled, strict decode, non-tweaked signature",
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3gr" +
"w==",
claims: nil,
paddedDecode: true,
strictDecode: true,
signingMethod: jwt.SigningMethodES256,
keyfunc: paddedKeyFunc,
valid: true,
},
{
name: "Error for padded token with padding enabled, strict decode, tweaked signature",
tokenString: "eyJ0eXAiOiJKV1QiLCJraWQiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJhbGciOiJFUzI1NiIsImlzcyI6Imh0dHBzOi8vY29nbml0by1pZHAuZXUtd2VzdC0yLmFtYXpvbmF3cy5jb20vIiwiY2xpZW50IjoiN0xUY29QWnJWNDR6ZVg2WUs5VktBcHZPM3EiLCJzaWduZXIiOiJhcm46YXdzOmVsYXN0aWNsb2FkYmFsYW5jaW5nIiwiZXhwIjoxNjI5NDcwMTAxfQ==.eyJzdWIiOiIxMjM0NTY3OC1hYmNkLTEyMzQtYWJjZC0xMjM0NTY3OGFiY2QiLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJ1c2VybmFtZSI6IjEyMzQ1Njc4LWFiY2QtMTIzNC1hYmNkLTEyMzQ1Njc4YWJjZCIsImV4cCI6MTYyOTQ3MDEwMSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5ldS13ZXN0LTIuYW1hem9uYXdzLmNvbS8ifQ==.sx0muJ754glJvwWgkHaPrOI3L1gaPjRLLUvOQRk0WitnqC5Dtt1knorcbOzlEcH9zwPM2jYYIAYQz_qEyM3gr" +
"x==",
claims: nil,
paddedDecode: true,
strictDecode: true,
signingMethod: jwt.SigningMethodES256,
keyfunc: paddedKeyFunc,
valid: false,
},
} }
// Extension of Parsing, this is to test out functionality specific to switching codecs with padding. // Extension of Parsing, this is to test out functionality specific to switching codecs with padding.
func TestSetPadding(t *testing.T) { func TestSetPadding(t *testing.T) {
for _, data := range setPaddingTestData { for _, data := range setPaddingTestData {
t.Run(data.name, func(t *testing.T) { t.Run(data.name, func(t *testing.T) {
jwt.DecodePaddingAllowed = data.paddedDecode
jwt.DecodeStrict = data.strictDecode
// If the token string is blank, use helper function to generate string // If the token string is blank, use helper function to generate string
jwt.DecodePaddingAllowed = data.paddedDecode
if data.tokenString == "" { if data.tokenString == "" {
data.tokenString = signToken(data.claims, data.signingMethod) data.tokenString = signToken(data.claims, data.signingMethod)
} }
// Parse the token // Parse the token
@ -581,7 +671,7 @@ func TestSetPadding(t *testing.T) {
}) })
jwt.DecodePaddingAllowed = false jwt.DecodePaddingAllowed = false
jwt.DecodeStrict = false
} }
} }

View File

@ -14,6 +14,12 @@ import (
// To use the non-recommended decoding, set this boolean to `true` prior to using this package. // To use the non-recommended decoding, set this boolean to `true` prior to using this package.
var DecodePaddingAllowed bool var DecodePaddingAllowed bool
// DecodeStrict will switch the codec used for decoding JWTs into strict mode.
// In this mode, the decoder requires that trailing padding bits are zero, as described in RFC 4648 section 3.5.
// 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.
// To use strict decoding, set this boolean to `true` prior to using this package.
var DecodeStrict bool
// TimeFunc provides the current time when parsing token to validate "exp" claim (expiration time). // TimeFunc provides the current time when parsing token to validate "exp" claim (expiration time).
// You can override it to use another time value. This is useful for testing or if your // You can override it to use another time value. This is useful for testing or if your
// server uses a different time zone than your tokens. // server uses a different time zone than your tokens.
@ -121,12 +127,17 @@ func EncodeSegment(seg []byte) string {
// Deprecated: In a future release, we will demote this function to a non-exported function, since it // Deprecated: In a future release, we will demote this function to a non-exported function, since it
// should only be used internally // should only be used internally
func DecodeSegment(seg string) ([]byte, error) { func DecodeSegment(seg string) ([]byte, error) {
encoding := base64.RawURLEncoding
if DecodePaddingAllowed { if DecodePaddingAllowed {
if l := len(seg) % 4; l > 0 { if l := len(seg) % 4; l > 0 {
seg += strings.Repeat("=", 4-l) seg += strings.Repeat("=", 4-l)
} }
return base64.URLEncoding.DecodeString(seg) encoding = base64.URLEncoding
} }
return base64.RawURLEncoding.DecodeString(seg) if DecodeStrict {
encoding = encoding.Strict()
}
return encoding.DecodeString(seg)
} }