Read the docs
TI-2026-66389 Aliases GHSA-hwqm-qvj9-4jr2

gosaml2 CBC Padding Panic — Unauthenticated Process Crash

Priority LOW 38CWE-129Published · Updated

Key facts

Attack vector
—
Privileges required
—
User interaction
—
Attack complexity
—
Impact type
—
Confidentiality · integrity · availability
—
Fix status
Fixed

No CVSS vector from any source. Fix status from GitHub.

Summary

## Summary The AES-CBC decryption path in `DecryptBytes()` panics on crafted ciphertext whose plaintext is all zero bytes. After decryption, `bytes.TrimRight(data, "\x00")` empties the slice, then `data[len(data)-1]` panics with `index out of range [-1]`. There is no `recover()` in the library. The panic propagates through `ValidateEncodedResponse` and kills the goroutine (or the entire process in non-`net/http` servers). An attacker needs only the SP's public RSA key (published in SAML metadata) to construct the payload — no valid signature is required. ## Affected Version All versions of `github.com/russellhaering/gosaml2` through latest (`v0.9.0` and HEAD) that support AES-CBC encrypted assertions. ## Vulnerable Code **`types/encrypted_assertion.go:65-79`** — `DecryptBytes`, AES-CBC branch: ```go case MethodAES128CBC, MethodAES256CBC, MethodTripleDESCBC: if len(data)%k.BlockSize() != 0 { return nil, fmt.Errorf("encrypted data is not a multiple of the expected CBC block size %d: actual size %d", k.BlockSize(), len(data)) } nonce, data := data[:k.BlockSize()], data[k.BlockSize():] c := cipher.NewCBCDecrypter(k, nonce) c.CryptBlocks(data, data) // Remove zero bytes data = bytes.TrimRight(data, "\x00") // <-- empties the slice if plaintext is all zeros // Calculate index to remove based on padding padLength := data[len(data)-1] // <-- PANIC: index out of range [-1] lastGoodIndex := len(data) - int(padLength) return data[:lastGoodIndex], nil ``` ## Attack Details | Property | Value | |---|---| | **Attack Vector** | Network (unauthenticated HTTP POST to ACS endpoint) | | **Authentication Required** | None | | **Attacker Knowledge** | SP's public RSA certificate (published in SAML metadata) | | **Signature Required** | No — decryption happens before assertion signature validation | | **Payload Size** | Single HTTP POST (~2 KB) | | **Repeatability** | Unlimited — attacker can send the payload repeatedly | | **Affected Configurations** | Any SP with `SPKeyStore` configured (encrypted assertion support) | | **Trigger Condition** | AES-CBC plaintext that is all `0x00` bytes after decryption | ## Impact - **Process crash**: In gRPC servers, custom frameworks, CLI tools, and background workers, the unrecovered panic kills the entire OS process immediately. - **Goroutine crash**: In `net/http` servers, the built-in per-goroutine recovery catches the panic, returning HTTP 500 and logging the full stack trace. The server survives but the request-handling goroutine is terminated abnormally. - **Denial of service**: The attack is unauthenticated and repeatable. A single crafted HTTP request is sufficient. Automated retries can keep the service down indefinitely. - **No valid signature needed**: The SAML Response does not need to be signed. On the unsigned-response code path (`decode_response.go:346`), `decryptAssertions()` is called **before** any assertion signature validation. ## Reproduction ### Prerequisites - Docker (for the vulnerable server) - Python 3.8+ with `cryptography` and `requests` packages ### Files | File | Description | |---|---| | `server.go` | Minimal SAML SP using gosaml2 — the victim | | `poc.py` | Attacker script — builds and sends the crafted payload | | `Dockerfile` | Multi-stage build for the vulnerable server | | `run.sh` | Build and orchestration script | ### Steps ```bash # 1. Build the vulnerable server ./run.sh build # 2. Start the server ./run.sh start # 3. Run the attacker script pip install cryptography requests ./run.sh attack # Or do everything in one command: ./run.sh all ``` ### Expected Output **Attacker terminal (`poc.py`):** ``` ======================================================== CVE: CBC Padding Panic — Unauthenticated Process Crash Target: gosaml2 (github.com/russellhaering/gosaml2) File: types/encrypted_assertion.go:77 Impact: Remote DoS — single HTTP request kills process ======================================================== [*] Target: http://localhost:9999 [*] Checking server health... [+] Server is alive ======================================================== Phase 1: Obtain SP public certificate from metadata ======================================================== [*] GET http://localhost:9999/metadata [+] Retrieved SP certificate (xxx bytes) ======================================================== Phase 2: Build crafted EncryptedAssertion payload ======================================================== [+] Extracted RSA public key (size=2048 bits) [*] Generated AES-128 key: <hex> [+] RSA-OAEP encrypted AES key (256 bytes) [+] AES-128-CBC ciphertext: IV(<hex>) + 16 bytes [*] Plaintext is all zeros — will trigger empty-slice panic after TrimRight [+] Built SAML Response (xxx bytes XML, xxx bytes b64) ======================================================== Phase 3: Send payload to /acs ======================================================== [*] POST http://localhost:9999/acs [*] The server will decrypt our ciphertext, hit the all-zero plaintext edge case, and panic in DecryptBytes()... [*] Got HTTP 500 — goroutine panicked but net/http recovered it ======================================================== Phase 4: Verify server status ======================================================== [*] Server is still responding (net/http recovered the goroutine panic) [*] But the panic stack trace in server logs confirms the vulnerability. [*] In non-HTTP servers, the process would be dead. ======================================================== VULNERABILITY CONFIRMED types/encrypted_assertion.go:77 — index out of range [-1] Stack trace: types/encrypted_assertion.go:77 (padLength := data[len(data)-1]) decode_response.go:176 (decryptAssertions) decode_response.go:346 (ValidateEncodedResponse) ======================================================== ``` **Server logs (panic stack trace):** ``` http: panic serving 127.0.0.1:xxxxx: runtime error: index out of range [-1] goroutine XX [running]: net/http.(*conn).serve.func1() /usr/local/go/src/net/http/server.go:1898 +0xbe github.com/russellhaering/gosaml2/types.(*EncryptedAssertion).DecryptBytes(...) types/encrypted_assertion.go:77 +0x... github.com/russellhaering/gosaml2.(*SAMLServiceProvider).decryptAssertions.func1(...) decode_response.go:176 +0x... github.com/russellhaering/gosaml2.(*SAMLServiceProvider).decryptAssertions(...) decode_response.go:196 +0x... github.com/russellhaering/gosaml2.(*SAMLServiceProvider).ValidateEncodedResponse(...) decode_response.go:346 +0x... ``` ## Suggested Fix Replace the unsafe zero-byte trimming and unchecked index with proper PKCS#7 unpadding and bounds checks: ```go case MethodAES128CBC, MethodAES256CBC, MethodTripleDESCBC: if len(data)%k.BlockSize() != 0 { return nil, fmt.Errorf("ciphertext not multiple of block size") } nonce, data := data[:k.BlockSize()], data[k.BlockSize():] c := cipher.NewCBCDecrypter(k, nonce) c.CryptBlocks(data, data) // Validate decrypted data is non-empty if len(data) == 0 { return nil, fmt.Errorf("decrypted data is empty") } // Proper PKCS#7 unpadding with bounds checks padLength := int(data[len(data)-1]) if padLength < 1 || padLength > k.BlockSize() || padLength > len(data) { return nil, fmt.Errorf("invalid padding length: %d", padLength) } // Verify all padding bytes are consistent for i := len(data) - padLength; i < len(data); i++ { if data[i] != byte(padLength) { return nil, fmt.Errorf("invalid PKCS#7 padding") } } return data[:len(data)-padLength], nil ``` Key changes: 1. **Remove `bytes.TrimRight(data, "\x00")`** entirely — it corrupts valid PKCS#7-padded data and creates the empty-slice condition. 2. **Bounds-check `padLength`** before using it as a slice index. 3. **Validate all padding bytes** match (proper PKCS#7 verification). 4. **Return errors** instead of panicking on malformed input.

Why it matters now

Fix Fixed github.com/russellhaering/gosaml2 (Go): 0.11.0

SSVC decision points

Not available: CISA has not published SSVC decision points for this record.

Affected products

VendorProductAffected versionsFixed inStatusSource
—github.com/russellhaering/gosaml2 (Go)before 0.11.00.11.0Patch availableGHSA-hwqm-qvj9-4jr2
  • github.com/russellhaering/gosaml2 (Go) Vendor— Affected before 0.11.0 Fixed in 0.11.0 Patch available GHSA-hwqm-qvj9-4jr2

Remediation

  • Fix · GHSA-hwqm-qvj9-4jr2 github.com/russellhaering/gosaml2 (Go) Fixed in 0.11.0 Advisory →

Timeline

  1. 2026-10-03 Threatist record created from GHSA-hwqm-qvj9-4jr2

References

Sources

This product uses the NVD API but is not endorsed or certified by the NVD. Sources and licences