Public mode

No access to your tenant or devices. Every item links to its source. What this means

Sources need attention Customize Public modeDiscuss ChangeIntel on Discord

gosaml2 CBC Padding Panic — Unauthenticated Process Crash

Exploitation and risk

Source status

CISA KEV lists vulnerabilities known to be exploited. FIRST EPSS estimates the likelihood of exploitation in the next 30 days. An unavailable score does not mean zero risk.

Which CVEs have scores here?

This view includes predictions for CVE identifiers explicitly listed in public NuGet advisories. Microsoft release pages have their own prediction coverage. Check source status for the scope and latest checks.

No exploitation or prediction data is available here for these CVEs. This does not mean they are safe or unlikely to be exploited.

osv · GHSA-hwqm-qvj9-4jr2

Published

## 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.

Source published
18 Mar 2026 20:19:11 UTC
Source modified
10 Sep 2026 03:50:39 UTC
Saved here
24 Oct 2026 13:19:41 UTC
Source terms
GitHub Advisory Database · CC-BY-4.0

Original source record · Cite this exact observation · Complete original publisher JSON

Source: osv · GitHub Advisory Database · CC-BY-4.0. The publisher's wording is preserved. ChangeIntel groups the information and labels changes. The JSON record includes additional source fields.

CVSS_V4 · CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

Publisher rating · HIGH

osv · GO-2026-4760

Published

CBC Padding Panic — Unauthenticated Process Crash in github.com/russellhaering/gosaml2

Source published
27 Mar 2026 18:39:44 UTC
Source modified
30 Mar 2026 14:29:33 UTC
Saved here
24 Oct 2026 13:19:41 UTC
Source terms
OSV · upstream source license applies

Original source record · Cite this exact observation · Complete original publisher JSON

Source: osv · OSV · upstream source license applies. The publisher's wording is preserved. ChangeIntel groups the information and labels changes. The JSON record includes additional source fields.

ChangeIntel

An IT change radar: releases, security, known issues, retirements, documentation changes, and service status from public sources. Every item links to supporting evidence; dates and statuses can change after they are read.

Sources read 15 Nov 11:08 UTC · 256 of 275 readable · 252 fully read · documentation 469/469 current