Skip to content

Commit

Permalink
Merge pull request from GHSA-hj3v-m684-v259
Browse files Browse the repository at this point in the history
* Add WithMaxDecompressBufferSize option

* tweak test name

* Update documentation

* Tweak Changes
  • Loading branch information
lestrrat committed Mar 7, 2024
1 parent 778ed27 commit d43f2ce
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 15 deletions.
12 changes: 12 additions & 0 deletions Changes
Expand Up @@ -4,6 +4,18 @@ Changes
v2 has many incompatibilities with v1. To see the full list of differences between
v1 and v2, please read the Changes-v2.md file (https://github.com/lestrrat-go/jwx/blob/develop/v2/Changes-v2.md)

v2.0.21 UNRELEASED
* [jwe] Added `jwe.Settings(jwe.WithMaxDecompressBufferSize(int64))` to specify the
maximum size of a decompressed JWE payload. The default value is 10MB. If you
are compressing payloads greater than this and want to decompress it during
a call to `jwe.Decrypt`, you need to explicitly set a value large enough to
hold that data.

The same option can be passed to `jwe.Decrypt` to control this behavior on
a per-message basis.
* [jwe] Added documentation stating that `jwe.WithMaxBufferSize` option will be
renamed in future versions, i.e. v3

v2.0.20 20 Feb 2024
[New Features]
* [jwe] Added `jwe.Settings(WithMaxBufferSize(int64))` to set the maximum size of
Expand Down
30 changes: 28 additions & 2 deletions jwe/compress.go
Expand Up @@ -9,8 +9,34 @@ import (
"github.com/lestrrat-go/jwx/v2/internal/pool"
)

func uncompress(plaintext []byte) ([]byte, error) {
return io.ReadAll(flate.NewReader(bytes.NewReader(plaintext)))
func uncompress(src []byte, maxBufferSize int64) ([]byte, error) {
var dst bytes.Buffer
r := flate.NewReader(bytes.NewReader(src))
defer r.Close()
var buf [16384]byte
var sofar int64
for {
n, readErr := r.Read(buf[:])
sofar += int64(n)
if sofar > maxBufferSize {
return nil, fmt.Errorf(`compressed payload exceeds maximum allowed size`)
}
if readErr != nil {
// if we have a read error, and it's not EOF, then we need to stop
if readErr != io.EOF {
return nil, fmt.Errorf(`failed to read inflated data: %w`, readErr)
}
}

if _, err := dst.Write(buf[:n]); err != nil {
return nil, fmt.Errorf(`failed to write inflated data: %w`, err)
}

if readErr != nil {
// if it got here, then readErr == io.EOF, we're done
return dst.Bytes(), nil
}
}
}

func compress(plaintext []byte) ([]byte, error) {
Expand Down
52 changes: 40 additions & 12 deletions jwe/jwe.go
Expand Up @@ -28,6 +28,7 @@ import (

var muSettings sync.RWMutex
var maxPBES2Count = 10000
var maxDecompressBufferSize int64 = 10 * 1024 * 1024 // 10MB

func Settings(options ...GlobalOption) {
muSettings.Lock()
Expand All @@ -37,6 +38,8 @@ func Settings(options ...GlobalOption) {
switch option.Ident() {
case identMaxPBES2Count{}:
maxPBES2Count = option.Value().(int)
case identMaxDecompressBufferSize{}:
maxDecompressBufferSize = option.Value().(int64)
case identMaxBufferSize{}:
aescbc.SetMaxBufferSize(option.Value().(int64))
}
Expand Down Expand Up @@ -463,28 +466,50 @@ func encrypt(payload, cek []byte, options ...EncryptOption) ([]byte, error) {
}

type decryptCtx struct {
msg *Message
aad []byte
cek *[]byte
computedAad []byte
keyProviders []KeyProvider
protectedHeaders Headers
msg *Message
aad []byte
cek *[]byte
computedAad []byte
keyProviders []KeyProvider
protectedHeaders Headers
maxDecompressBufferSize int64
}

// Decrypt takes the key encryption algorithm and the corresponding
// key to decrypt the JWE message, and returns the decrypted payload.
// Decrypt takes encrypted payload, and information required to decrypt the
// payload (e.g. the key encryption algorithm and the corresponding
// key to decrypt the JWE message) in its optional arguments. See
// the examples and list of options that return a DecryptOption for possible
// values. Upon successful decryptiond returns the decrypted payload.
//
// The JWE message can be either compact or full JSON format.
//
// `alg` accepts a `jwa.KeyAlgorithm` for convenience so you can directly pass
// the result of `(jwk.Key).Algorithm()`, but in practice it must be of type
// When using `jwe.WithKeyEncryptionAlgorithm()`, you can pass a `jwa.KeyAlgorithm`
// for convenience: this is mainly to allow you to directly pass the result of `(jwk.Key).Algorithm()`.
// However, do note that while `(jwk.Key).Algorithm()` could very well contain key encryption
// algorithms, it could also contain other types of values, such as _signature algorithms_.
// In order for `jwe.Decrypt` to work properly, the `alg` parameter must be of type
// `jwa.KeyEncryptionAlgorithm` or otherwise it will cause an error.
//
// `key` must be a private key. It can be either in its raw format (e.g. *rsa.PrivateKey) or a jwk.Key
// When using `jwe.WithKey()`, the value must be a private key.
// It can be either in its raw format (e.g. *rsa.PrivateKey) or a jwk.Key
//
// When the encrypted message is also compressed, the decompressed payload must be
// smaller than the size specified by the `jwe.WithMaxDecompressBufferSize` setting,
// which defaults to 10MB. If the decompressed payload is larger than this size,
// an error is returned.
//
// You can opt to change the MaxDecompressBufferSize setting globally, or on a
// per-call basis by passing the `jwe.WithMaxDecompressBufferSize` option to
// either `jwe.Settings()` or `jwe.Decrypt()`:
//
// jwe.Settings(jwe.WithMaxDecompressBufferSize(10*1024*1024)) // changes value globally
// jwe.Decrypt(..., jwe.WithMaxDecompressBufferSize(250*1024)) // changes just for this call
func Decrypt(buf []byte, options ...DecryptOption) ([]byte, error) {
var keyProviders []KeyProvider
var keyUsed interface{}
var cek *[]byte
var dst *Message
perCallMaxDecompressBufferSize := maxDecompressBufferSize
//nolint:forcetypeassert
for _, option := range options {
switch option.Ident() {
Expand All @@ -506,6 +531,8 @@ func Decrypt(buf []byte, options ...DecryptOption) ([]byte, error) {
})
case identCEK{}:
cek = option.Value().(*[]byte)
case identMaxDecompressBufferSize{}:
perCallMaxDecompressBufferSize = option.Value().(int64)
}
}

Expand Down Expand Up @@ -565,6 +592,7 @@ func Decrypt(buf []byte, options ...DecryptOption) ([]byte, error) {
dctx.keyProviders = keyProviders
dctx.protectedHeaders = h
dctx.cek = cek
dctx.maxDecompressBufferSize = perCallMaxDecompressBufferSize

var lastError error
for _, recipient := range recipients {
Expand Down Expand Up @@ -741,7 +769,7 @@ func (dctx *decryptCtx) decryptContent(ctx context.Context, alg jwa.KeyEncryptio
}

if h2.Compression() == jwa.Deflate {
buf, err := uncompress(plaintext)
buf, err := uncompress(plaintext, dctx.maxDecompressBufferSize)
if err != nil {
return nil, fmt.Errorf(`jwe.Derypt: failed to uncompress payload: %w`, err)
}
Expand Down
86 changes: 86 additions & 0 deletions jwe/jwe_test.go
Expand Up @@ -971,3 +971,89 @@ func TestMaxBufferSize(t *testing.T) {
_, err = jwe.Encrypt([]byte("Lorem Ipsum"), jwe.WithContentEncryption(jwa.A128CBC_HS256), jwe.WithKey(jwa.RSA_OAEP, key))
require.Error(t, err, `jwe.Encrypt should fail`)
}

func TestMaxDecompressBufferSize(t *testing.T) {
// This payload size is intentionally set to a small value to avoid
// causing problems for regular users and CI/CD systems. If you wish to
// verify that root issue is fixed, you may want to try increasing the
// payload size to a larger value.
const payloadSize = 1 << 16

privkey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err, `rsa.GenerateKey should succeed`)

pubkey := &privkey.PublicKey

wrongPrivkey, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err, `rsa.GenerateKey should succeed`)
wrongPubkey := &wrongPrivkey.PublicKey

payload := strings.Repeat("x", payloadSize)

testcases := []struct {
Name string
GlobalMaxSize int64
PublicKey *rsa.PublicKey
Error bool
ProcessDecryptOptions func([]jwe.DecryptOption) []jwe.DecryptOption
}{
// This should work, because we set the MaxSize to be large (==payload size)
{
Name: "same as payload size",
GlobalMaxSize: payloadSize,
PublicKey: pubkey,
},
// This should fail, because we set the GlobalMaxSize to be smaller than the payload size
{
Name: "smaller than payload size",
GlobalMaxSize: payloadSize - 1,
PublicKey: pubkey,
Error: true,
},
// This should fail, because the public key does not match the
// private key used to decrypt the payload. In essence this way
// we do NOT trigger the root cause of this issue, but we bail out early
{
Name: "Wrong PublicKey",
GlobalMaxSize: payloadSize,
PublicKey: wrongPubkey,
Error: true,
},
{
Name: "global=payloadSize-1, per-call=payloadSize",
GlobalMaxSize: payloadSize - 1,
PublicKey: pubkey,
ProcessDecryptOptions: func(options []jwe.DecryptOption) []jwe.DecryptOption {
return append(options, jwe.WithMaxDecompressBufferSize(payloadSize))
},
},
// This should be the last test case to put the value back to default :)
{
Name: "Default 10MB globally",
GlobalMaxSize: 10 * 1024 * 1024,
PublicKey: pubkey,
},
}
for _, tc := range testcases {
tc := tc
t.Run(tc.Name, func(t *testing.T) {
jwe.Settings(jwe.WithMaxDecompressBufferSize(tc.GlobalMaxSize))

encrypted, err := jwe.Encrypt([]byte(payload), jwe.WithKey(jwa.RSA_OAEP, tc.PublicKey), jwe.WithContentEncryption("A128CBC-HS256"), jwe.WithCompress(jwa.Deflate))

require.NoError(t, err, `jwe.Encrypt should succeed`)

decryptOptions := []jwe.DecryptOption{jwe.WithKey(jwa.RSA_OAEP, privkey)}

if fn := tc.ProcessDecryptOptions; fn != nil {
decryptOptions = fn(decryptOptions)
}
_, err = jwe.Decrypt(encrypted, decryptOptions...)
if tc.Error {
require.Error(t, err, `jwe.Decrypt should fail`)
} else {
require.NoError(t, err, `jwe.Decrypt should succeed`)
}
})
}
}
23 changes: 22 additions & 1 deletion jwe/options.yaml
Expand Up @@ -4,6 +4,12 @@ interfaces:
- name: GlobalOption
comment: |
GlobalOption describes options that changes global settings for this package
- name: GlobalDecryptOption
comment: |
GlobalDecryptOption describes options that changes global settings and for each call of the `jwe.Decrypt` function
methods:
- globalOption
- decryptOption
- name: CompactOption
comment: |
CompactOption describes options that can be passed to `jwe.Compact`
Expand Down Expand Up @@ -141,6 +147,18 @@ options:
value of 10,000 is used.
This option has a global effect.
- ident: MaxDecompressBufferSize
interface: GlobalDecryptOption
argument_type: int64
comment: |
WithMaxDecompressBufferSize specifies the maximum buffer size for used when
decompressing the payload of a JWE message. If a compressed JWE payload
exceeds this amount when decompressed, jwe.Decrypt will return an error.
The default value is 10MB.
This option can be used for `jwe.Settings()`, which changes the behavior
globally, or for `jwe.Decrypt()`, which changes the behavior for that
specific call.
- ident: MaxBufferSize
interface: GlobalOption
argument_type: int64
Expand All @@ -149,4 +167,7 @@ options:
calculations, such as when AES-CBC is performed. The default value is 256MB.
If set to an invalid value, the default value is used.
This option has a global effect.
This option has a global effect.
Due to historical reasons this option has a vague name, but in future versions
it will be appropriately renamed.
35 changes: 35 additions & 0 deletions jwe/options_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions jwe/options_gen_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit d43f2ce

Please sign in to comment.