You've already forked oauth2-proxy
mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2025-06-23 00:40:46 +02:00
Fix secretBytes adding unintended padding (#556)
* Fix secretBytes adding unintended padding * Add more SecretBytes test scenarios * Add CHANGELOG entry about breaking secret padding change * Add SecretBytes tests explanation comments
This commit is contained in:
@ -32,8 +32,16 @@
|
|||||||
- In some scenarios `X-Forwarded-User` will now be empty. Use `X-Forwarded-Email` instead.
|
- In some scenarios `X-Forwarded-User` will now be empty. Use `X-Forwarded-Email` instead.
|
||||||
- In some scenarios, this may break setting Basic Auth on upstream or responses.
|
- In some scenarios, this may break setting Basic Auth on upstream or responses.
|
||||||
Use `--prefer-email-to-user` to restore falling back to the Email in these cases.
|
Use `--prefer-email-to-user` to restore falling back to the Email in these cases.
|
||||||
|
- [#556](https://github.com/oauth2-proxy/oauth2-proxy/pull/556) Remove unintentional auto-padding of secrets that were too short
|
||||||
|
- Previously, after cookie-secrets were opportunistically base64 decoded to raw bytes,
|
||||||
|
they were padded to have a length divisible by 4.
|
||||||
|
- This led to wrong sized secrets being valid AES lengths of 16, 24, or 32 bytes. Or it led to confusing errors
|
||||||
|
reporting an invalid length of 20 or 28 when the user input cookie-secret was not that length.
|
||||||
|
- Now we will only base64 decode a cookie-secret to raw bytes if it is 16, 24, or 32 bytes long. Otherwise, we will convert
|
||||||
|
the direct cookie-secret to bytes without silent padding added.
|
||||||
|
|
||||||
## Changes since v5.1.1
|
## Changes since v5.1.1
|
||||||
|
- [#556](https://github.com/oauth2-proxy/oauth2-proxy/pull/556) Remove unintentional auto-padding of secrets that were too short (@NickMeves)
|
||||||
- [#538](https://github.com/oauth2-proxy/oauth2-proxy/pull/538) Refactor sessions/utils.go functionality to other areas (@NickMeves)
|
- [#538](https://github.com/oauth2-proxy/oauth2-proxy/pull/538) Refactor sessions/utils.go functionality to other areas (@NickMeves)
|
||||||
- [#503](https://github.com/oauth2-proxy/oauth2-proxy/pull/503) Implements --real-client-ip-header option to select the header from which to obtain a proxied client's IP (@Izzette)
|
- [#503](https://github.com/oauth2-proxy/oauth2-proxy/pull/503) Implements --real-client-ip-header option to select the header from which to obtain a proxied client's IP (@Izzette)
|
||||||
- [#529](https://github.com/oauth2-proxy/oauth2-proxy/pull/529) Add local test environments for testing changes and new features (@JoelSpeed)
|
- [#529](https://github.com/oauth2-proxy/oauth2-proxy/pull/529) Add local test environments for testing changes and new features (@JoelSpeed)
|
||||||
|
@ -898,7 +898,7 @@ func NewProcessCookieTest(opts ProcessCookieTestOpts, modifiers ...OptionsModifi
|
|||||||
}
|
}
|
||||||
pcTest.opts.ClientID = "asdfljk"
|
pcTest.opts.ClientID = "asdfljk"
|
||||||
pcTest.opts.ClientSecret = "lkjfdsig"
|
pcTest.opts.ClientSecret = "lkjfdsig"
|
||||||
pcTest.opts.Cookie.Secret = "0123456789abcdefabcd"
|
pcTest.opts.Cookie.Secret = "0123456789abcdef0123456789abcdef"
|
||||||
// First, set the CookieRefresh option so proxy.AesCipher is created,
|
// First, set the CookieRefresh option so proxy.AesCipher is created,
|
||||||
// needed to encrypt the access_token.
|
// needed to encrypt the access_token.
|
||||||
pcTest.opts.Cookie.Refresh = time.Hour
|
pcTest.opts.Cookie.Refresh = time.Hour
|
||||||
|
@ -233,7 +233,7 @@ func TestCookieRefreshMustBeLessThanCookieExpire(t *testing.T) {
|
|||||||
o := testOptions()
|
o := testOptions()
|
||||||
assert.Equal(t, nil, o.Validate())
|
assert.Equal(t, nil, o.Validate())
|
||||||
|
|
||||||
o.Cookie.Secret = "0123456789abcdefabcd"
|
o.Cookie.Secret = "0123456789abcdef"
|
||||||
o.Cookie.Refresh = o.Cookie.Expire
|
o.Cookie.Refresh = o.Cookie.Expire
|
||||||
assert.NotEqual(t, nil, o.Validate())
|
assert.NotEqual(t, nil, o.Validate())
|
||||||
|
|
||||||
|
@ -19,27 +19,22 @@ import (
|
|||||||
|
|
||||||
// SecretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary
|
// SecretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary
|
||||||
func SecretBytes(secret string) []byte {
|
func SecretBytes(secret string) []byte {
|
||||||
b, err := base64.URLEncoding.DecodeString(addPadding(secret))
|
b, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(secret, "="))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return []byte(addPadding(string(b)))
|
// Only return decoded form if a valid AES length
|
||||||
|
// Don't want unintentional decoding resulting in invalid lengths confusing a user
|
||||||
|
// that thought they used a 16, 24, 32 length string
|
||||||
|
for _, i := range []int{16, 24, 32} {
|
||||||
|
if len(b) == i {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// If decoding didn't work or resulted in non-AES compliant length,
|
||||||
|
// assume the raw string was the intended secret
|
||||||
return []byte(secret)
|
return []byte(secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
func addPadding(secret string) string {
|
|
||||||
padding := len(secret) % 4
|
|
||||||
switch padding {
|
|
||||||
case 1:
|
|
||||||
return secret + "==="
|
|
||||||
case 2:
|
|
||||||
return secret + "=="
|
|
||||||
case 3:
|
|
||||||
return secret + "="
|
|
||||||
default:
|
|
||||||
return secret
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cookies are stored in a 3 part (value + timestamp + signature) to enforce that the values are as originally set.
|
// cookies are stored in a 3 part (value + timestamp + signature) to enforce that the values are as originally set.
|
||||||
// additionally, the 'value' is encrypted so it's opaque to the browser
|
// additionally, the 'value' is encrypted so it's opaque to the browser
|
||||||
|
|
||||||
|
@ -1,14 +1,87 @@
|
|||||||
package encryption
|
package encryption
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestSecretBytesEncoded(t *testing.T) {
|
||||||
|
for _, secretSize := range []int{16, 24, 32} {
|
||||||
|
t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) {
|
||||||
|
secret := make([]byte, secretSize)
|
||||||
|
_, err := io.ReadFull(rand.Reader, secret)
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
|
||||||
|
// We test both padded & raw Base64 to ensure we handle both
|
||||||
|
// potential user input routes for Base64
|
||||||
|
base64Padded := base64.URLEncoding.EncodeToString(secret)
|
||||||
|
sb := SecretBytes(base64Padded)
|
||||||
|
assert.Equal(t, secret, sb)
|
||||||
|
assert.Equal(t, len(sb), secretSize)
|
||||||
|
|
||||||
|
base64Raw := base64.RawURLEncoding.EncodeToString(secret)
|
||||||
|
sb = SecretBytes(base64Raw)
|
||||||
|
assert.Equal(t, secret, sb)
|
||||||
|
assert.Equal(t, len(sb), secretSize)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A string that isn't intended as Base64 and still decodes (but to unintended length)
|
||||||
|
// will return the original secret as bytes
|
||||||
|
func TestSecretBytesEncodedWrongSize(t *testing.T) {
|
||||||
|
for _, secretSize := range []int{15, 20, 28, 33, 44} {
|
||||||
|
t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) {
|
||||||
|
secret := make([]byte, secretSize)
|
||||||
|
_, err := io.ReadFull(rand.Reader, secret)
|
||||||
|
assert.Equal(t, nil, err)
|
||||||
|
|
||||||
|
// We test both padded & raw Base64 to ensure we handle both
|
||||||
|
// potential user input routes for Base64
|
||||||
|
base64Padded := base64.URLEncoding.EncodeToString(secret)
|
||||||
|
sb := SecretBytes(base64Padded)
|
||||||
|
assert.NotEqual(t, secret, sb)
|
||||||
|
assert.NotEqual(t, len(sb), secretSize)
|
||||||
|
// The given secret is returned as []byte
|
||||||
|
assert.Equal(t, base64Padded, string(sb))
|
||||||
|
|
||||||
|
base64Raw := base64.RawURLEncoding.EncodeToString(secret)
|
||||||
|
sb = SecretBytes(base64Raw)
|
||||||
|
assert.NotEqual(t, secret, sb)
|
||||||
|
assert.NotEqual(t, len(sb), secretSize)
|
||||||
|
// The given secret is returned as []byte
|
||||||
|
assert.Equal(t, base64Raw, string(sb))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSecretBytesNonBase64(t *testing.T) {
|
||||||
|
trailer := "equals=========="
|
||||||
|
assert.Equal(t, trailer, string(SecretBytes(trailer)))
|
||||||
|
|
||||||
|
raw16 := "asdflkjhqwer)(*&"
|
||||||
|
sb16 := SecretBytes(raw16)
|
||||||
|
assert.Equal(t, raw16, string(sb16))
|
||||||
|
assert.Equal(t, 16, len(sb16))
|
||||||
|
|
||||||
|
raw24 := "asdflkjhqwer)(*&CJEN#$%^"
|
||||||
|
sb24 := SecretBytes(raw24)
|
||||||
|
assert.Equal(t, raw24, string(sb24))
|
||||||
|
assert.Equal(t, 24, len(sb24))
|
||||||
|
|
||||||
|
raw32 := "asdflkjhqwer)(*&1234lkjhqwer)(*&"
|
||||||
|
sb32 := SecretBytes(raw32)
|
||||||
|
assert.Equal(t, raw32, string(sb32))
|
||||||
|
assert.Equal(t, 32, len(sb32))
|
||||||
|
}
|
||||||
|
|
||||||
func TestSignAndValidate(t *testing.T) {
|
func TestSignAndValidate(t *testing.T) {
|
||||||
seed := "0123456789abcdef"
|
seed := "0123456789abcdef"
|
||||||
key := "cookie-name"
|
key := "cookie-name"
|
||||||
|
Reference in New Issue
Block a user