FrankenPHP: dot-header trust spoof in PHP (CVE-2026-128023)
An unauthenticated caller can spoof the trusted identity headers a PHP application reads from $_SERVER by sending dot-form header names that FrankenPHP rewrites to underscores. A request carrying X.Authenticated.User: admin is indistinguishable from a gateway-injected identity header, so an exposed deployment grants the attacker the identity, client IP, or scheme the application trusts.
Public CVE record · Vendor or researcher advisory · Module source · Source and API bundle
Patch title. FrankenPHP Dot-header Trust Spoof in PHP Guard (frankenphp-dot-header-trust-spoof-in-php-guard in the registry). The original source snapshot preserves the source available for the historical report.
| Field | Value |
|---|---|
| Affected component and versions | FrankenPHP through 1.12.7 |
| Fixed release | FrankenPHP 1.13.0 |
| Severity | Critical (CVSS 9.1, CNA) |
| Evidence status | Recorded fixture comparison |
| Test environment | Synthetic HTTP fixture behind a single-node Eden 1.103.0 interlay |
| Eden release and module version | 1.103.0 · gabriel_frankenheader_v1 1.0.0 |
| Tested at | 2026-11-13 UTC |
| First publicly published | 2026-11-13T17:18:33Z |
Result. The values after each status are the observed increases in the fixture's identity-grant counter. The native rule refused the marked fixture request with HTTP 403 while the benign probe stayed HTTP 200, verified against an independent backend counter on 2026-11-13 (UTC) through Eden 1.103.0.
The vulnerability
FrankenPHP before 1.13.0 rewrites dot-form header names to underscores when populating $_SERVER, so a client-supplied X.Authenticated.User becomes the X_AUTHENTICATED_USER key an application treats as gateway-injected identity. Eden terminates the client connection in front of the PHP worker, so every spoofable header transits an interlay where a policy or module can refuse it before the request is released upstream.
The Eden patch
Tier 1 — declarative policy. The recorded policy denies the two spoofable trust headers by name (x.authenticated.user, x.forwarded.for) on every route. This is surgical for the recorded probes: the benign request carried neither header and passed.
{
"version": 1,
"profile": "http-header-presence-v1",
"policy": {
"version": 1,
"denied_headers": [
"x.authenticated.user",
"x.forwarded.for"
]
}
}
Tier 2 — native Rust module. gabriel_frankenheader_v1 waits for a complete request, then walks the parsed header names on the profile route. A header whose name case-insensitively equals x.authenticated.user or x.forwarded.for is refused; every other request continues. The shown lines are excerpts from the linked source.
const ROUTE: &[u8] = b"/api/profile";
static DOTS: [&[u8]; 2] = [b"x.authenticated.user", b"x.forwarded.for"];
Implement the remediation
The commands are operator examples for the recorded scope. Compare fixture routes and software selectors with the intended deployment before use. See deployment prerequisites and the API example.
- Prepare the target. Verify the loaded FrankenPHP version against the advisory. Use a compatible Eden HTTP interlay and an authenticated operator with Configure permission. Use the recorded declaration and the linked source.
- Apply the declarative policy. Set
INTERLAYto the target interlay UUID in the authenticated Eden CLI. Save the interlay's complete current settings, place the policy from this report insettings.remediation, and PATCH the full settings object with a freshIdempotency-Key. Read the interlay back and confirmremediation_status.stateisready. - Import and activate the native module. Build the schema-2 archive from the linked source with the compatible SDK, verify its digest, and PUT it to the interlay's patch-packages endpoint. Import requires explicit activation: enroll the publisher key, attest the host inventory, inspect the returned configuration, obtain independent host approval, sign the typed statement, admit, and activate. Read the receipt until
serving_nowis true and the expected shards are acknowledged. Full command sequences are in the API example. - Verify, revoke, and restore. Replay the benign and advisory-shaped probes and compare the backend counter. Revoke the operation and confirm the advisory-shaped request no longer succeeds; re-approve, re-admit, and reactivate a replacement operation to restore protection. Keep the route isolated during the change window.
Vendor update. Apply the fix for the installed release line. The advisory documents affected versions and update guidance.
Fixture and probes
The benign probe fetched /api/profile with no dot-form headers. The advisory-shaped probe sent X.Authenticated.User: admin. The fixture incremented its marker only when the spoofed identity header was present, measuring the identity-grant effect independently of the HTTP status.
Measured comparison (2026-11-13 UTC)
The values after each status are the observed increases in the fixture's identity-grant counter. The declarative tier blocked the spoofed header by name and passed the benign request, so no false positive was recorded for this case.
| Probe | Baseline | Declarative policy | Native module |
|---|---|---|---|
| Benign profile fetch | 200, 0 | 200, 0 | 200, 0 |
| Advisory-shaped spoof header | 200, 1 | 403, 0 | 403, 0 |
The rule matches exact header names on one recorded route. Header values are not inspected, so a same-named header on another route is not refused by the module; the declarative deny covers all routes for the two recorded names. Other dot-form spellings of trust headers (for example X.Remote.User) were not exercised.
Signed module authority
Import returned HTTP 201 with explicit activation required and signature verification deferred. Read-only inspection was refused (403) for the reader principal, admission of a tampered statement was rejected (400), and admission of an invalid signature was refused (403). The activation receipt reported the module serving on all 32 expected shards. After revocation the advisory-shaped probe received HTTP 503 (fail-closed); re-approval and reactivation restored the refusal.
- Source SHA-256
e245c6a836da844c0e6d74f566dfbd9c475b1962110cc9672b00d4aac79c09ea - Tested binary SHA-256
15aefd2b2896f8ff3f06b9a94edaf5a64ec70cb80ee4e6822d27aebc2426418d - Activation operation
a325d69c-1ed2-46db-8c43-464dda310c85
Performance
Instrument: client wall-clock through the interlay (includes the network hop), warmup discarded, mean/p50/p95 in milliseconds.
| Arm | n | mean | p50 | p95 |
|---|---|---|---|---|
| Tier 1 — declarative policy, blocked (403) | 15 | 0.4 ms | 0.4 ms | 0.6 ms |
| Tier 2 — module, benign passes (200) | 20 | 0.7 ms | 0.7 ms | 0.9 ms |
| Tier 2 — module, attack refused | 15 | 0.5 ms | 0.5 ms | 0.6 ms |
Test scope
The fixture covers two specific dot-form trust header names. The native module is scoped to the recorded profile route. Other trusted header names, routes, and production identity configurations remain unverified.