Skip to content

Commit

Permalink
Merge pull request #34 from dvsekhvalnov/issue-33-deflate-limit
Browse files Browse the repository at this point in the history
Issue 33 deflate limit
  • Loading branch information
dvsekhvalnov committed Apr 19, 2024
2 parents 48ba0b7 + c3fff7c commit 0a0673d
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 17 deletions.
32 changes: 32 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Build and test jose2go project

on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]

jobs:

build-and-test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
version: ['1.20', '1.21', '1.22']
steps:
- name: Checkout library sources
uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.version }}

- name: Build
run: go build -v ./...

- name: Test
run: go test ./...

16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Extensively unit tested and cross tested (100+ tests) for compatibility with [jo
Used in production. GA ready. Current version is 1.6.

## Important
v1.7 introduced deflate decompression memory limits to avoid denial-of-service attacks aka 'deflate-bomb'. See [Customizing compression](#customizing-compression) section for details.

v1.6 security tuning options

v1.5 bug fix release
Expand Down Expand Up @@ -997,7 +999,21 @@ test, headers, err := Decode(token, func(headers map[string]interface{}, payload
})
```

### Customizing compression
There were denial-of-service attacks reported on JWT libraries that supports deflate compression by constructing malicious payload that explodes in terms of RAM on decompression. See for details: [#33](https://github.com/dvsekhvalnov/jose2go/issues/33)

As of v1.7.0 `jose2go` limits decompression buffer to 250Kb to limit memory consumption and additionaly provides a way to adjust the limit according to specific scenarios:

```Go
// Override compression alg with new limits (10Kb example)
jose.RegisterJwc(RegisterJwc(NewDeflate(10240)))
```

## Changelog
### 1.7
- 250Kb limit on decompression buffer
- ability to register deflate compressor with custom limits

### 1.6
- ability to deregister specific algorithms
- configurable min/max restrictions for PBES2-HS256+A128KW, PBES2-HS384+A192KW, PBES2-HS512+A256KW
Expand Down
60 changes: 47 additions & 13 deletions deflate.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,71 @@ package jose
import (
"bytes"
"compress/flate"
"io/ioutil"
"errors"
"io"
)

var ErrSizeExceeded = errors.New("Deflate stream size exceeded limit.")

func init() {
RegisterJwc(new(Deflate))
// 250Kb limited decompression buffer
RegisterJwc(NewDeflate(250 * 1024))
}

// Deflate compression algorithm implementation
type Deflate struct {}
type Deflate struct {
maxBufferSizeBytes int64
}

func NewDeflate(maxBufferSizeBytes int64) JwcAlgorithm {
return &Deflate{
maxBufferSizeBytes: maxBufferSizeBytes,
}
}

func (alg *Deflate) Name() string {
return DEF
}

func (alg *Deflate) Compress(plainText []byte) []byte {
var buf bytes.Buffer
deflate,_ := flate.NewWriter(&buf, 8) //level=DEFLATED
deflate, _ := flate.NewWriter(&buf, 8) //level=DEFLATED

deflate.Write(plainText)
deflate.Close()

return buf.Bytes()
}

func (alg *Deflate) Decompress(compressedText []byte) []byte {

enflated,_ := ioutil.ReadAll(
flate.NewReader(
bytes.NewReader(compressedText)))
return enflated
func (alg *Deflate) Decompress(compressedText []byte) ([]byte, error) {
enflated, err := io.ReadAll(
newMaxBytesReader(alg.maxBufferSizeBytes,
flate.NewReader(
bytes.NewReader(compressedText))))

return enflated, err
}

// Max bytes reader
type maxBytesReader struct {
reader io.Reader
limit int64
}

func newMaxBytesReader(limit int64, r io.Reader) io.Reader {
return &maxBytesReader{reader: r, limit: limit}
}

func (mbr *maxBytesReader) Read(p []byte) (n int, err error) {
if mbr.limit <= 0 {
return 0, ErrSizeExceeded
}

if int64(len(p)) > mbr.limit {
p = p[0:mbr.limit]
}

n, err = mbr.reader.Read(p)
mbr.limit -= int64(n)
return
}
6 changes: 4 additions & 2 deletions jose.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ type JwsAlgorithm interface {
// JwcAlgorithm is a contract for implementing compression algorithm
type JwcAlgorithm interface {
Compress(plainText []byte) []byte
Decompress(compressedText []byte) []byte
Decompress(compressedText []byte) ([]byte, error)
Name() string
}

Expand Down Expand Up @@ -427,7 +427,9 @@ func decrypt(parts [][]byte, key interface{}) (plainText []byte, headers map[str
return nil, nil, errors.New(fmt.Sprintf("jwt.decrypt(): Unknown compression algorithm '%v'", zip))
}

plainBytes = zipAlg.Decompress(plainBytes)
if plainBytes, err = zipAlg.Decompress(plainBytes); err != nil {
return nil, nil, err
}
}

return plainBytes, jwtHeader, nil
Expand Down
91 changes: 89 additions & 2 deletions sec_test/security_vulnerabilities_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ package sec_test

import (
"crypto/ecdsa"
"crypto/rsa"
"encoding/json"
"fmt"
"github.com/dvsekhvalnov/jose2go"
"strings"
"testing"
"time"

jose "github.com/dvsekhvalnov/jose2go"
"github.com/dvsekhvalnov/jose2go/arrays"
"github.com/dvsekhvalnov/jose2go/keys/ecc"
Rsa "github.com/dvsekhvalnov/jose2go/keys/rsa"
. "gopkg.in/check.v1"
"testing"
)

func Test(t *testing.T) { TestingT(t) }
Expand Down Expand Up @@ -93,8 +99,89 @@ func (s *SecurityTestSuite) Test_AAD_IntegerOverflow(c *C) {
c.Assert(test, IsNil)
}

func (s *SecurityTestSuite) Test_DeflateBomb(c *C) {
strU := strings.Repeat("U", 400000000)
strUU := strings.Repeat("U", 100000000)

payloadMap := map[string]string{
"U": strU,
"UU": strUU,
}

payloadBytes, _ := json.Marshal(payloadMap)

fmt.Println("Uncompressed payload length", len(payloadBytes))
test, _ := jose.Encrypt(string(payloadBytes), jose.RSA_OAEP, jose.A256GCM, PubKey(), jose.Zip(jose.DEF))
fmt.Println("Encoded & Compressed token length", len(test))

start := time.Now()
payload, headers, err := jose.Decode(test, PrivKey())
timeElapsed := time.Since(start)
fmt.Printf("The `decode` took %s\n", timeElapsed)

c.Assert(payload, Equals, "")
c.Assert(headers, IsNil)
c.Assert(err, Equals, jose.ErrSizeExceeded)
}

func Ecc256() *ecdsa.PrivateKey {
return ecc.NewPrivate([]byte{193, 227, 73, 203, 97, 236, 112, 36, 140, 232, 1, 3, 76, 56, 52, 225, 184, 142, 190, 17, 97, 203, 37, 175, 56, 116, 31, 120, 95, 207, 196, 196},
[]byte{123, 201, 103, 8, 239, 128, 149, 43, 83, 248, 210, 85, 95, 231, 43, 132, 30, 208, 69, 136, 98, 139, 29, 55, 138, 89, 73, 57, 80, 14, 201, 201},
[]byte{84, 73, 131, 102, 144, 215, 92, 175, 41, 240, 221, 2, 157, 219, 49, 179, 221, 184, 171, 169, 210, 213, 21, 197, 1, 36, 101, 232, 23, 212, 169, 220})
}

func PubKey() *rsa.PublicKey {
key, _ := Rsa.ReadPublic([]byte(pubKey))
return key
}

func PrivKey() *rsa.PrivateKey {
key, _ := Rsa.ReadPrivate([]byte(privKey))
return key
}

var pubKey = `-----BEGIN CERTIFICATE-----
MIICnTCCAYUCBEReYeAwDQYJKoZIhvcNAQEFBQAwEzERMA8GA1UEAxMIand0LTIw
NDgwHhcNMTQwMTI0MTMwOTE2WhcNMzQwMjIzMjAwMDAwWjATMREwDwYDVQQDEwhq
d3QtMjA0ODCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKhWb9KXmv45
+TKOKhFJkrboZbpbKPJ9Yp12xKLXf8060KfStEStIX+7dCuAYylYWoqiGpuLVVUL
5JmHgXmK9TJpzv9Dfe3TAc/+35r8r9IYB2gXUOZkebty05R6PLY0RO/hs2ZhrOoz
HMo+x216Gwz0CWaajcuiY5Yg1V8VvJ1iQ3rcRgZapk49RNX69kQrGS63gzj0gyHn
Rtbqc/Ua2kobCA83nnznCom3AGinnlSN65AFPP5jmri0l79+4ZZNIerErSW96mUF
8jlJFZI1yJIbzbv73tL+y4i0+BvzsWBs6TkHAp4pinaI8zT+hrVQ2jD4fkJEiRN9
lAqLPUd8CNkCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAnqBw3UHOSSHtU7yMi1+H
E+9119tMh7X/fCpcpOnjYmhW8uy9SiPBZBl1z6vQYkMPcURnDMGHdA31kPKICZ6G
LWGkBLY3BfIQi064e8vWHW7zX6+2Wi1zFWdJlmgQzBhbr8pYh9xjZe6FjPwbSEuS
0uE8dWSWHJLdWsA4xNX9k3pr601R2vPVFCDKs3K1a8P/Xi59kYmKMjaX6vYT879y
gWt43yhtGTF48y85+eqLdFRFANTbBFSzdRlPQUYa5d9PZGxeBTcg7UBkK/G+d6D5
sd78T2ymwlLYrNi+cSDYD6S4hwZaLeEK6h7p/OoG02RBNuT4VqFRu5DJ6Po+C6Jh
qQ==
-----END CERTIFICATE-----`

var privKey = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAqFZv0pea/jn5Mo4qEUmStuhlulso8n1inXbEotd/zTrQp9K0
RK0hf7t0K4BjKVhaiqIam4tVVQvkmYeBeYr1MmnO/0N97dMBz/7fmvyv0hgHaBdQ
5mR5u3LTlHo8tjRE7+GzZmGs6jMcyj7HbXobDPQJZpqNy6JjliDVXxW8nWJDetxG
BlqmTj1E1fr2RCsZLreDOPSDIedG1upz9RraShsIDzeefOcKibcAaKeeVI3rkAU8
/mOauLSXv37hlk0h6sStJb3qZQXyOUkVkjXIkhvNu/ve0v7LiLT4G/OxYGzpOQcC
nimKdojzNP6GtVDaMPh+QkSJE32UCos9R3wI2QIDAQABAoIBAQCUmHBvSkqUHaK/
IMU7q2FqOi0KWswDefEiJKQhRu9Wv5NOgW2FrfqDIXrDp7pg1dBezgeExHLX9v6d
FAOTwbj9/m6t3+r6k6fm7gp+ao3dfD6VgPd12L2oXQ0t5NVQ1UUBJ4/QUWps9h90
3AP4vK/COG1P+CAw4DDeZi9TlwF/Pr7e492GXcLBAUJODA6538ED2nYw8xQcbzbA
wr+w07UjRNimObtOfA0HCIpsx/6LkIqe6iGChisQNgt4yDd/fZ4GWOUIU1hqgK1P
6avVl7Q5Mk0PTi9t8ui1X4EEq6Uils45J5WkobuAnFkea/uKfs8Tn9bNrEoVWgdb
fBHq/8bNAoGBANKmjpE9e+L0RtxP+u4FN5YDoKE+i96VR7ru8H6yBKMcnD2uf5mV
RueEoL0FKHxlGBBo0dJWr1AIwpcPbTs3Dgx1/EQMZLg57QBZ7QcYETPiMwMvEM3k
Zf3G4YFYwUwIQXMYPt1ckr+RncRcq0GiKPDsvzzyNS+BBSmR5onAXd7bAoGBAMyT
6ggyqmiR/UwBn87em+GjbfX6YqxHHaQBdWwnnRX0JlGTNCxt6zLTgCIYxF4AA7eR
gfGTStwUJfAScjJirOe6Cpm1XDgxEQrT6oxAl17MR/ms/Z88WrT73G+4phVvDpVr
JcK+CCESnRI8xGLOLMkCc+5NpLajqWCOf1H2J8NbAoGAKTWmTGmf092AA1euOmRQ
5IsfIIxQ5qGDn+FgsRh4acSOGE8L7WrTrTU4EOJyciuA0qz+50xIDbs4/j5pWx1B
JVTrnhBin9vNLrVo9mtR6jmFS0ko226kOUpwEVLgtdQjobWLjtiuaMW+/Iw4gKWN
ptxZ6T1lBD8UWHaPiEFW2+MCgYAmfSWoyS96YQ0QwbV5TDRzrTXA84yg8PhIpOWc
pY9OVBLpghJs0XlQpK4UvCglr0cDwGJ8OsP4x+mjUzUc+aeiKURZSt/Ayqp0KQ6V
uIlCEpjwBnXpAYfnSQNeGZVVrwFFZ1VBYFNTNZdLmRcxp6yRXN7G1ODKY9w4CFc3
6mHsxQKBgQCxEA+KAmmXxL++x/XOElOscz3vFHC4HbpHpOb4nywpE9vunnHE2WY4
EEW9aZbF22jx0ESU2XJ1JlqffvfIEvHNb5tmBWn4HZEpPUHdaFNhb9WjkMuFaLzh
cydwnEftq+3G0X3KSxp4p7R7afcnpNNqfneYODgoXxTQ4Q7ZyKo72A==
-----END RSA PRIVATE KEY-----`

0 comments on commit 0a0673d

Please sign in to comment.