GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-HWQM-QVJ9-4JR2

Vulnerability from github – Published: 2026-03-18 20:19 – Updated: 2026-03-18 20:19
VLAI
Summary
gosaml2 CBC Padding Panic — Unauthenticated Process Crash
Details

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-79DecryptBytes, 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

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

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

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 0.10.0"
      },
      "package": {
        "ecosystem": "Go",
        "name": "github.com/russellhaering/gosaml2"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.11.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-129"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-18T20:19:11Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "## Summary\n\nThe 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\u0027s public RSA key (published in SAML metadata) to construct the payload \u2014 no valid signature is required.\n\n## Affected Version\n\nAll versions of `github.com/russellhaering/gosaml2` through latest (`v0.9.0` and HEAD) that support AES-CBC encrypted assertions.\n\n## Vulnerable Code\n\n**`types/encrypted_assertion.go:65-79`** \u2014 `DecryptBytes`, AES-CBC branch:\n\n```go\ncase MethodAES128CBC, MethodAES256CBC, MethodTripleDESCBC:\n    if len(data)%k.BlockSize() != 0 {\n        return nil, fmt.Errorf(\"encrypted data is not a multiple of the expected CBC block size %d: actual size %d\", k.BlockSize(), len(data))\n    }\n    nonce, data := data[:k.BlockSize()], data[k.BlockSize():]\n    c := cipher.NewCBCDecrypter(k, nonce)\n    c.CryptBlocks(data, data)\n\n    // Remove zero bytes\n    data = bytes.TrimRight(data, \"\\x00\")      // \u003c-- empties the slice if plaintext is all zeros\n\n    // Calculate index to remove based on padding\n    padLength := data[len(data)-1]             // \u003c-- PANIC: index out of range [-1]\n    lastGoodIndex := len(data) - int(padLength)\n    return data[:lastGoodIndex], nil\n```\n\n## Attack Details\n\n| Property | Value |\n|---|---|\n| **Attack Vector** | Network (unauthenticated HTTP POST to ACS endpoint) |\n| **Authentication Required** | None |\n| **Attacker Knowledge** | SP\u0027s public RSA certificate (published in SAML metadata) |\n| **Signature Required** | No \u2014 decryption happens before assertion signature validation |\n| **Payload Size** | Single HTTP POST (~2 KB) |\n| **Repeatability** | Unlimited \u2014 attacker can send the payload repeatedly |\n| **Affected Configurations** | Any SP with `SPKeyStore` configured (encrypted assertion support) |\n| **Trigger Condition** | AES-CBC plaintext that is all `0x00` bytes after decryption |\n\n## Impact\n\n- **Process crash**: In gRPC servers, custom frameworks, CLI tools, and background workers, the unrecovered panic kills the entire OS process immediately.\n- **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.\n- **Denial of service**: The attack is unauthenticated and repeatable. A single crafted HTTP request is sufficient. Automated retries can keep the service down indefinitely.\n- **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.\n\n## Reproduction\n\n### Prerequisites\n\n- Docker (for the vulnerable server)\n- Python 3.8+ with `cryptography` and `requests` packages\n\n### Files\n\n| File | Description |\n|---|---|\n| `server.go` | Minimal SAML SP using gosaml2 \u2014 the victim |\n| `poc.py` | Attacker script \u2014 builds and sends the crafted payload |\n| `Dockerfile` | Multi-stage build for the vulnerable server |\n| `run.sh` | Build and orchestration script |\n\n### Steps\n\n```bash\n# 1. Build the vulnerable server\n./run.sh build\n\n# 2. Start the server\n./run.sh start\n\n# 3. Run the attacker script\npip install cryptography requests\n./run.sh attack\n\n# Or do everything in one command:\n./run.sh all\n```\n\n### Expected Output\n\n**Attacker terminal (`poc.py`):**\n\n```\n ========================================================\n  CVE: CBC Padding Panic \u2014 Unauthenticated Process Crash\n  Target: gosaml2 (github.com/russellhaering/gosaml2)\n  File:   types/encrypted_assertion.go:77\n  Impact: Remote DoS \u2014 single HTTP request kills process\n ========================================================\n\n[*] Target: http://localhost:9999\n[*] Checking server health...\n[+] Server is alive\n\n========================================================\n  Phase 1: Obtain SP public certificate from metadata\n========================================================\n[*] GET http://localhost:9999/metadata\n[+] Retrieved SP certificate (xxx bytes)\n\n========================================================\n  Phase 2: Build crafted EncryptedAssertion payload\n========================================================\n[+] Extracted RSA public key (size=2048 bits)\n[*] Generated AES-128 key: \u003chex\u003e\n[+] RSA-OAEP encrypted AES key (256 bytes)\n[+] AES-128-CBC ciphertext: IV(\u003chex\u003e) + 16 bytes\n[*] Plaintext is all zeros \u2014 will trigger empty-slice panic after TrimRight\n[+] Built SAML Response (xxx bytes XML, xxx bytes b64)\n\n========================================================\n  Phase 3: Send payload to /acs\n========================================================\n[*] POST http://localhost:9999/acs\n[*] The server will decrypt our ciphertext, hit the all-zero\n    plaintext edge case, and panic in DecryptBytes()...\n\n[*] Got HTTP 500 \u2014 goroutine panicked but net/http recovered it\n\n========================================================\n  Phase 4: Verify server status\n========================================================\n[*] Server is still responding (net/http recovered the goroutine panic)\n[*] But the panic stack trace in server logs confirms the vulnerability.\n[*] In non-HTTP servers, the process would be dead.\n\n========================================================\n  VULNERABILITY CONFIRMED\n  types/encrypted_assertion.go:77 \u2014 index out of range [-1]\n\n  Stack trace:\n    types/encrypted_assertion.go:77  (padLength := data[len(data)-1])\n    decode_response.go:176           (decryptAssertions)\n    decode_response.go:346           (ValidateEncodedResponse)\n========================================================\n```\n\n**Server logs (panic stack trace):**\n\n```\nhttp: panic serving 127.0.0.1:xxxxx: runtime error: index out of range [-1]\ngoroutine XX [running]:\nnet/http.(*conn).serve.func1()\n    /usr/local/go/src/net/http/server.go:1898 +0xbe\ngithub.com/russellhaering/gosaml2/types.(*EncryptedAssertion).DecryptBytes(...)\n    types/encrypted_assertion.go:77 +0x...\ngithub.com/russellhaering/gosaml2.(*SAMLServiceProvider).decryptAssertions.func1(...)\n    decode_response.go:176 +0x...\ngithub.com/russellhaering/gosaml2.(*SAMLServiceProvider).decryptAssertions(...)\n    decode_response.go:196 +0x...\ngithub.com/russellhaering/gosaml2.(*SAMLServiceProvider).ValidateEncodedResponse(...)\n    decode_response.go:346 +0x...\n```\n\n## Suggested Fix\n\nReplace the unsafe zero-byte trimming and unchecked index with proper PKCS#7 unpadding and bounds checks:\n\n```go\ncase MethodAES128CBC, MethodAES256CBC, MethodTripleDESCBC:\n    if len(data)%k.BlockSize() != 0 {\n        return nil, fmt.Errorf(\"ciphertext not multiple of block size\")\n    }\n    nonce, data := data[:k.BlockSize()], data[k.BlockSize():]\n    c := cipher.NewCBCDecrypter(k, nonce)\n    c.CryptBlocks(data, data)\n\n    // Validate decrypted data is non-empty\n    if len(data) == 0 {\n        return nil, fmt.Errorf(\"decrypted data is empty\")\n    }\n\n    // Proper PKCS#7 unpadding with bounds checks\n    padLength := int(data[len(data)-1])\n    if padLength \u003c 1 || padLength \u003e k.BlockSize() || padLength \u003e len(data) {\n        return nil, fmt.Errorf(\"invalid padding length: %d\", padLength)\n    }\n\n    // Verify all padding bytes are consistent\n    for i := len(data) - padLength; i \u003c len(data); i++ {\n        if data[i] != byte(padLength) {\n            return nil, fmt.Errorf(\"invalid PKCS#7 padding\")\n        }\n    }\n\n    return data[:len(data)-padLength], nil\n```\n\nKey changes:\n1. **Remove `bytes.TrimRight(data, \"\\x00\")`** entirely \u2014 it corrupts valid PKCS#7-padded data and creates the empty-slice condition.\n2. **Bounds-check `padLength`** before using it as a slice index.\n3. **Validate all padding bytes** match (proper PKCS#7 verification).\n4. **Return errors** instead of panicking on malformed input.",
  "id": "GHSA-hwqm-qvj9-4jr2",
  "modified": "2026-03-18T20:19:11Z",
  "published": "2026-03-18T20:19:11Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/russellhaering/gosaml2/security/advisories/GHSA-hwqm-qvj9-4jr2"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/russellhaering/gosaml2"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "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",
      "type": "CVSS_V4"
    }
  ],
  "summary": "gosaml2 CBC Padding Panic \u2014 Unauthenticated Process Crash"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…