Skip to content

Commit 4ddcc82

Browse files
Security hardening: CBC bounds check, panic removal, assertion signature verification
Three backwards-compatible security fixes: 1. CBC padding oracle fix (types/encrypted_assertion.go): Add bounds checking on padding length to prevent panics from crafted ciphertext. Validates padLength > 0, padLength <= len(data), and padLength <= blockSize before using it as a slice index. 2. Panic removal (saml.go, decode_response.go): Replace three panic() calls with error returns or nil returns: - SigningContext(): return nil instead of panicking on cert parse error - decryptAssertions(): return error instead of panicking on RemoveChild - validateAssertionSignatures(): return error instead of panicking on RemoveChild 3. Assertion signature verification within signed Response (decode_response.go): When the Response envelope is signed, assertion signatures are now verified if present. Previously they were skipped entirely, allowing potential XML wrapping attacks where assertion content is tampered with inside a signed envelope. Assertions without signatures are still accepted (covered by the Response signature).
1 parent d57d105 commit 4ddcc82

3 files changed

Lines changed: 67 additions & 13 deletions

File tree

decode_response.go

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,7 @@ func (sp *SAMLServiceProvider) decryptAssertions(el *etree.Element) error {
185185

186186
// Replace the original encrypted assertion with the decrypted one.
187187
if el.RemoveChild(encryptedElement) == nil {
188-
// Out of an abundance of caution, make sure removed worked
189-
panic("unable to remove encrypted assertion")
188+
return fmt.Errorf("unable to remove encrypted assertion element")
190189
}
191190

192191
el.AddChild(doc.Root())
@@ -233,9 +232,7 @@ func (sp *SAMLServiceProvider) validateAssertionSignatures(el *etree.Element) er
233232
// Replace the original unverified Assertion with the verified one. Note that
234233
// if the Response is not signed, only signed Assertions (and not the parent Response) can be trusted.
235234
if el.RemoveChild(unverifiedAssertion) == nil {
236-
// Out of an abundance of caution, check to make sure an Assertion was actually
237-
// removed. If it wasn't a programming error has occurred.
238-
panic("unable to remove assertion")
235+
return fmt.Errorf("unable to remove unverified assertion element")
239236
}
240237

241238
el.AddChild(assertion)
@@ -255,6 +252,42 @@ func (sp *SAMLServiceProvider) validateAssertionSignatures(el *etree.Element) er
255252
}
256253
}
257254

255+
// verifyAssertionSignaturesIfPresent iterates through assertions within a
256+
// signed Response and verifies any that carry their own signatures. Assertions
257+
// without signatures are left untouched (the Response envelope signature
258+
// covers them). This prevents XML wrapping attacks where assertion content is
259+
// tampered with inside a signed envelope.
260+
func (sp *SAMLServiceProvider) verifyAssertionSignaturesIfPresent(responseEl *etree.Element) error {
261+
verifyAssertion := func(ctx etreeutils.NSContext, assertionEl *etree.Element) error {
262+
if assertionEl.Parent() != responseEl {
263+
return nil
264+
}
265+
266+
detached, err := etreeutils.NSDetatch(ctx, assertionEl)
267+
if err != nil {
268+
return fmt.Errorf("unable to detach assertion for signature verification: %v", err)
269+
}
270+
271+
verified, err := sp.validationContext().Validate(detached)
272+
if err == dsig.ErrMissingSignature {
273+
// No signature on this assertion — that's fine, the Response
274+
// envelope signature covers it.
275+
return nil
276+
} else if err != nil {
277+
return fmt.Errorf("assertion signature verification failed: %v", err)
278+
}
279+
280+
// Replace the unverified assertion with the signature-verified version.
281+
if responseEl.RemoveChild(assertionEl) == nil {
282+
return fmt.Errorf("unable to remove unverified assertion element")
283+
}
284+
responseEl.AddChild(verified)
285+
return nil
286+
}
287+
288+
return etreeutils.NSFindIterate(responseEl, SAMLAssertionNamespace, AssertionTag, verifyAssertion)
289+
}
290+
258291
// ValidateEncodedResponse both decodes and validates, based on SP
259292
// configuration, an encoded, signed response. It will also appropriately
260293
// decrypt a response if the assertion was encrypted
@@ -310,6 +343,15 @@ func (sp *SAMLServiceProvider) ValidateEncodedResponse(encodedResponse string) (
310343
return nil, err
311344
}
312345

346+
// Even though the Response envelope is signed, verify assertion
347+
// signatures when present. This prevents XML wrapping attacks
348+
// where an attacker tampers with assertion content within a
349+
// signed envelope.
350+
err = sp.verifyAssertionSignaturesIfPresent(signedResponseEl)
351+
if err != nil {
352+
return nil, err
353+
}
354+
313355
responseSignatureValidated = true
314356

315357
err = xmlUnmarshalElement(signedResponseEl, decodedResponse)

saml.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -345,10 +345,13 @@ func (sp *SAMLServiceProvider) SigningContext() *dsig.SigningContext {
345345
if signing != nil {
346346
sp.signingContext, err = dsig.NewSigningContext(signing.Signer, [][]byte{signing.Cert})
347347
if err != nil {
348-
// Ideally this function should return the error, but updating the function signature would be backward incompatible.
349-
// In practice, this error should never happen because NewSigningContext only errors when passed a nil signer, and
350-
// sp.spSigningKeyStoreOverride only gets set after checking to ensure the signer is not nil.
351-
panic(err)
348+
// Ideally this function should return the error, but updating the function
349+
// signature would be backward incompatible. Returning nil avoids the previous
350+
// panic while preserving the existing API contract. In practice, this error
351+
// should never happen because NewSigningContext only errors when passed a nil
352+
// signer, and sp.spSigningKeyStoreOverride only gets set after checking to
353+
// ensure the signer is not nil.
354+
return nil
352355
}
353356
} else {
354357
sp.signingContext = dsig.NewDefaultSigningContext(sp.GetSigningKey())

types/encrypted_assertion.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,19 @@ func (ea *EncryptedAssertion) DecryptBytes(cert *tls.Certificate) ([]byte, error
7373
// Remove zero bytes
7474
data = bytes.TrimRight(data, "\x00")
7575

76-
// Calculate index to remove based on padding
77-
padLength := data[len(data)-1]
78-
lastGoodIndex := len(data) - int(padLength)
79-
return data[:lastGoodIndex], nil
76+
if len(data) == 0 {
77+
return nil, fmt.Errorf("CBC decrypted data is empty after trimming zero bytes")
78+
}
79+
80+
// Validate and remove padding. The pad length byte indicates
81+
// how many bytes to strip. Bounds-check to prevent panics from
82+
// crafted ciphertext.
83+
padLength := int(data[len(data)-1])
84+
if padLength == 0 || padLength > len(data) || padLength > k.BlockSize() {
85+
return nil, fmt.Errorf("invalid CBC padding length: %d (data length: %d, block size: %d)", padLength, len(data), k.BlockSize())
86+
}
87+
88+
return data[:len(data)-padLength], nil
8089
default:
8190
return nil, fmt.Errorf("unknown symmetric encryption method %#v", ea.EncryptionMethod.Algorithm)
8291
}

0 commit comments

Comments
 (0)