NIST deprecates RSA-2048 from 2030. Find every one of them before attackers do.
🐹 Go
Not in CISA KEV

gosaml2 CBC Padding Panic — Unauthenticated Process CrashGHSA-hwqm-qvj9-4jr2

GHSA-hwqm-qvj9-4jr2 is a security vulnerability in github.com/russellhaering/gosaml2. A fix is available for github.com/russellhaering/gosaml2 — see the affected versions and patch details below.

Also known asGO-2026-4760
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Sep 10, 2026 · OSV.dev, FIRST.org (EPSS)

Real-World Exposure

1 pkg affected
🐹github.com/russellhaering/gosaml2

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.

Description

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:

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

PropertyValue
Attack VectorNetwork (unauthenticated HTTP POST to ACS endpoint)
Authentication RequiredNone
Attacker KnowledgeSP's public RSA certificate (published in SAML metadata)
Signature RequiredNo — decryption happens before assertion signature validation
Payload SizeSingle HTTP POST (~2 KB)
RepeatabilityUnlimited — attacker can send the payload repeatedly
Affected ConfigurationsAny SP with SPKeyStore configured (encrypted assertion support)
Trigger ConditionAES-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

FileDescription
server.goMinimal SAML SP using gosaml2 — the victim
poc.pyAttacker script — builds and sends the crafted payload
DockerfileMulti-stage build for the vulnerable server
run.shBuild and orchestration script

Steps

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

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.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐹Gogithub.com/russellhaering/gosaml2all versions0.11.0go get github.com/russellhaering/gosaml2@v0.11.0

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/russellhaering/gosaml2, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update github.com/russellhaering/gosaml2 to 0.11.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-hwqm-qvj9-4jr2 is resolved across your whole dependency graph.

  3. Workarounds

    Cap what an attacker can consume: apply request size, rate and timeout limits in front of the affected component, and run it with memory and CPU limits so exhaustion degrades one worker rather than the whole service.

Frequently Asked Questions

## 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
O3 Security · Impact-Aware SCA

Is GHSA-hwqm-qvj9-4jr2 in your dependencies?

Find it across Go, including transitive dependencies.

gosaml2 CBC Padding Panic — Unauthenticated Process…