Skip to content

Commit f9d6604

Browse files
Merge pull request from GHSA-6gc3-crp7-25w5
Configurable deflate-bomb protection
2 parents 156f1b9 + 56f4c23 commit f9d6604

File tree

5 files changed

+68
-24
lines changed

5 files changed

+68
-24
lines changed

decode_logout_request.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func (sp *SAMLServiceProvider) ValidateEncodedLogoutRequestPOST(encodedRequest s
4949
}
5050

5151
// Parse the raw request - parseResponse is generic
52-
doc, el, err := parseResponse(raw)
52+
doc, el, err := parseResponse(raw, sp.MaximumDecompressedBodySize)
5353
if err != nil {
5454
return nil, err
5555
}

decode_response.go

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,19 @@ import (
2121
"crypto/x509"
2222
"encoding/base64"
2323
"fmt"
24-
"io/ioutil"
24+
"io"
2525

2626
"encoding/xml"
2727

2828
"github.com/beevik/etree"
29+
rtvalidator "github.com/mattermost/xml-roundtrip-validator"
2930
"github.com/russellhaering/gosaml2/types"
3031
dsig "github.com/russellhaering/goxmldsig"
3132
"github.com/russellhaering/goxmldsig/etreeutils"
32-
rtvalidator "github.com/mattermost/xml-roundtrip-validator"
33+
)
34+
35+
const (
36+
defaultMaxDecompressedResponseSize = 5 * 1024 * 1024
3337
)
3438

3539
func (sp *SAMLServiceProvider) validationContext() *dsig.ValidationContext {
@@ -174,7 +178,7 @@ func (sp *SAMLServiceProvider) decryptAssertions(el *etree.Element) error {
174178
return fmt.Errorf("unable to decrypt encrypted assertion: %v", derr)
175179
}
176180

177-
doc, _, err := parseResponse(raw)
181+
doc, _, err := parseResponse(raw, sp.MaximumDecompressedBodySize)
178182
if err != nil {
179183
return fmt.Errorf("unable to create element from decrypted assertion bytes: %v", err)
180184
}
@@ -250,17 +254,17 @@ func (sp *SAMLServiceProvider) validateAssertionSignatures(el *etree.Element) er
250254
}
251255
}
252256

253-
//ValidateEncodedResponse both decodes and validates, based on SP
254-
//configuration, an encoded, signed response. It will also appropriately
255-
//decrypt a response if the assertion was encrypted
257+
// ValidateEncodedResponse both decodes and validates, based on SP
258+
// configuration, an encoded, signed response. It will also appropriately
259+
// decrypt a response if the assertion was encrypted
256260
func (sp *SAMLServiceProvider) ValidateEncodedResponse(encodedResponse string) (*types.Response, error) {
257261
raw, err := base64.StdEncoding.DecodeString(encodedResponse)
258262
if err != nil {
259263
return nil, err
260264
}
261265

262266
// Parse the raw response
263-
doc, el, err := parseResponse(raw)
267+
doc, el, err := parseResponse(raw, sp.MaximumDecompressedBodySize)
264268
if err != nil {
265269
return nil, err
266270
}
@@ -330,7 +334,7 @@ func DecodeUnverifiedBaseResponse(encodedResponse string) (*types.UnverifiedBase
330334

331335
var response *types.UnverifiedBaseResponse
332336

333-
err = maybeDeflate(raw, func(maybeXML []byte) error {
337+
err = maybeDeflate(raw, defaultMaxDecompressedResponseSize, func(maybeXML []byte) error {
334338
response = &types.UnverifiedBaseResponse{}
335339
return xml.Unmarshal(maybeXML, response)
336340
})
@@ -344,26 +348,37 @@ func DecodeUnverifiedBaseResponse(encodedResponse string) (*types.UnverifiedBase
344348
// maybeDeflate invokes the passed decoder over the passed data. If an error is
345349
// returned, it then attempts to deflate the passed data before re-invoking
346350
// the decoder over the deflated data.
347-
func maybeDeflate(data []byte, decoder func([]byte) error) error {
351+
func maybeDeflate(data []byte, maxSize int64, decoder func([]byte) error) error {
348352
err := decoder(data)
349353
if err == nil {
350354
return nil
351355
}
352356

353-
deflated, err := ioutil.ReadAll(flate.NewReader(bytes.NewReader(data)))
357+
// Default to 5MB max size
358+
if maxSize == 0 {
359+
maxSize = defaultMaxDecompressedResponseSize
360+
}
361+
362+
lr := io.LimitReader(flate.NewReader(bytes.NewReader(data)), maxSize+1)
363+
364+
deflated, err := io.ReadAll(lr)
354365
if err != nil {
355366
return err
356367
}
357368

369+
if int64(len(deflated)) > maxSize {
370+
return fmt.Errorf("deflated response exceeds maximum size of %d bytes", maxSize)
371+
}
372+
358373
return decoder(deflated)
359374
}
360375

361376
// parseResponse is a helper function that was refactored out so that the XML parsing behavior can be isolated and unit tested
362-
func parseResponse(xml []byte) (*etree.Document, *etree.Element, error) {
377+
func parseResponse(xml []byte, maxSize int64) (*etree.Document, *etree.Element, error) {
363378
var doc *etree.Document
364379
var rawXML []byte
365380

366-
err := maybeDeflate(xml, func(xml []byte) error {
381+
err := maybeDeflate(xml, maxSize, func(xml []byte) error {
367382
doc = etree.NewDocument()
368383
rawXML = xml
369384
return doc.ReadFromBytes(xml)
@@ -395,7 +410,7 @@ func DecodeUnverifiedLogoutResponse(encodedResponse string) (*types.LogoutRespon
395410

396411
var response *types.LogoutResponse
397412

398-
err = maybeDeflate(raw, func(maybeXML []byte) error {
413+
err = maybeDeflate(raw, defaultMaxDecompressedResponseSize, func(maybeXML []byte) error {
399414
response = &types.LogoutResponse{}
400415
return xml.Unmarshal(maybeXML, response)
401416
})
@@ -413,7 +428,7 @@ func (sp *SAMLServiceProvider) ValidateEncodedLogoutResponsePOST(encodedResponse
413428
}
414429

415430
// Parse the raw response
416-
doc, el, err := parseResponse(raw)
431+
doc, el, err := parseResponse(raw, sp.MaximumDecompressedBodySize)
417432
if err != nil {
418433
return nil, err
419434
}

decode_response_test.go

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ import (
2525
"time"
2626

2727
"github.com/jonboulle/clockwork"
28-
"github.com/russellhaering/goxmldsig"
29-
"github.com/stretchr/testify/require"
3028
rtvalidator "github.com/mattermost/xml-roundtrip-validator"
29+
dsig "github.com/russellhaering/goxmldsig"
30+
"github.com/stretchr/testify/require"
3131
)
3232

3333
const (
@@ -169,7 +169,7 @@ func TestDecodeColonsInLocalNames(t *testing.T) {
169169
t.Skip()
170170
}
171171

172-
_, _, err := parseResponse([]byte(`<x::Root/>`))
172+
_, _, err := parseResponse([]byte(`<x::Root/>`), 0)
173173
require.Error(t, err)
174174
}
175175

@@ -180,7 +180,7 @@ func TestDecodeDoubleColonInjectionAttackResponse(t *testing.T) {
180180
t.Skip()
181181
}
182182

183-
_, _, err := parseResponse([]byte(doubleColonAssertionInjectionAttackResponse))
183+
_, _, err := parseResponse([]byte(doubleColonAssertionInjectionAttackResponse), 0)
184184
require.Error(t, err)
185185
}
186186

@@ -194,7 +194,7 @@ func TestMalFormedInput(t *testing.T) {
194194
}
195195

196196
sp := &SAMLServiceProvider{
197-
Clock: dsig.NewFakeClock(clockwork.NewFakeClockAt(time.Date(2019, 8, 12, 12, 00, 52, 718, time.UTC))),
197+
Clock: dsig.NewFakeClock(clockwork.NewFakeClockAt(time.Date(2019, 8, 12, 12, 00, 52, 718, time.UTC))),
198198
AssertionConsumerServiceURL: "https://saml2.test.astuart.co/sso/saml2",
199199
SignAuthnRequests: true,
200200
IDPCertificateStore: &certStore,
@@ -203,4 +203,27 @@ func TestMalFormedInput(t *testing.T) {
203203
base64Input := base64.StdEncoding.EncodeToString([]byte(badInput))
204204
_, err = sp.RetrieveAssertionInfo(base64Input)
205205
require.Errorf(t, err, "parent is nil")
206-
}
206+
}
207+
208+
func TestCompressionBombInput(t *testing.T) {
209+
bs, err := ioutil.ReadFile("./testdata/saml_compressed.post")
210+
require.NoError(t, err, "couldn't read compressed post")
211+
212+
block, _ := pem.Decode([]byte(oktaCert))
213+
214+
idpCert, err := x509.ParseCertificate(block.Bytes)
215+
require.NoError(t, err, "couldn't parse okta cert pem block")
216+
217+
sp := SAMLServiceProvider{
218+
AssertionConsumerServiceURL: "https://f1f51ddc.ngrok.io/api/sso/saml2/acs/58cafd0573d4f375b8e70e8e",
219+
SPKeyStore: dsig.TLSCertKeyStore(cert),
220+
IDPCertificateStore: &dsig.MemoryX509CertificateStore{
221+
Roots: []*x509.Certificate{idpCert},
222+
},
223+
Clock: dsig.NewFakeClock(clockwork.NewFakeClockAt(time.Date(2017, 3, 17, 20, 00, 0, 0, time.UTC))),
224+
MaximumDecompressedBodySize: 2048,
225+
}
226+
227+
_, err = sp.RetrieveAssertionInfo(string(bs))
228+
require.NoError(t, err, "Assertion info should be retrieved with no error")
229+
}

saml.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,14 @@ type SAMLServiceProvider struct {
7474
SkipSignatureValidation bool
7575
AllowMissingAttributes bool
7676
Clock *dsig.Clock
77-
signingContextMu sync.RWMutex
78-
signingContext *dsig.SigningContext
77+
78+
// MaximumDecompressedBodySize is the maximum size to which a compressed
79+
// SAML document will be decompressed. If a compresed document is exceeds
80+
// this size during decompression an error will be returned.
81+
MaximumDecompressedBodySize int64
82+
83+
signingContextMu sync.RWMutex
84+
signingContext *dsig.SigningContext
7985
}
8086

8187
// RequestedAuthnContext controls which authentication mechanisms are requested of

saml_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ func TestSAMLCommentInjection(t *testing.T) {
353353
*/
354354

355355
// To show that we are not vulnerable, we want to prove that we get the canonicalized value using our parser
356-
_, el, err := parseResponse([]byte(commentInjectionAttackResponse))
356+
_, el, err := parseResponse([]byte(commentInjectionAttackResponse), 0)
357357
require.NoError(t, err)
358358
decodedResponse := &types.Response{}
359359
err = xmlUnmarshalElement(el, decodedResponse)

0 commit comments

Comments
 (0)