| title | FrankenPHP security model: trust boundaries between Go and PHP |
|---|---|
| description | Where FrankenPHP's trust boundary lives: which inputs are untrusted, what the Go/C runtime is responsible for, and what falls to the PHP application or to upstream projects. |
This document describes FrankenPHP's trust model: which inputs are trusted, which are not, and where the boundary between them lives. It is meant to help security audits and automated scanners reason about FrankenPHP itself, not the PHP applications it serves.
For the internal mechanics referenced here (threads, the CGO boundary, environment sandboxing), see Internals. For state persistence in long-running processes, see Worker Mode.
FrankenPHP runs in a stack with four distinct actors:
| Actor | Trust | Notes |
|---|---|---|
| Remote client | Untrusted | The HTTP request (method, URI, headers, cookies, body, uploads) is the primary source of tainted input. |
| Operator | Trusted | Supplies the deployment configuration: the Caddyfile, environment variables, php.ini, installed PHP extensions and Caddy modules, and the application code itself. |
| PHP application code | Trusted by provenance | Deployed by the operator, so FrankenPHP never executes attacker-supplied code, but this code consumes untrusted request data. |
| FrankenPHP (Go + C) | Trusted computing base | Embeds PHP, transports data in and out, and isolates requests and threads. Its own defects are what this document scopes. |
This is the single most important distinction, and the one that resolves most "do we trust PHP or not?" confusion:
- Code provenance is trusted. FrankenPHP only executes PHP files deployed by the operator: the script resolved under the document root, or the configured worker script. It never evaluates code carried in the request itself (body, query string, headers): the SAPI boundary carries data, never untrusted code.
- Request data is tainted. Everything the PHP code reads from the request (
$_GET,$_POST,$_COOKIE,$_FILES,$_SERVER,php://input) is untrusted, exactly as in any PHP SAPI. Sanitizing it is the application's job.
So "we trust what comes from PHP" is true for the code, while "the SAPI carries untrusted input" is true for the data: the two are not in conflict. FrankenPHP's job is to carry that tainted data faithfully and to keep one request's data from leaking into another.
The trusted computing base has three jobs. Security defects in FrankenPHP live in one of them:
- Faithful transport: map the request into PHP superglobals and
php://input, and carry PHP's output and headers back to the client, without introducing injection (header/CRLF injection, request smuggling, execution of the wrong file). - Isolation: keep request-scoped state from crossing between requests, between PHP threads, and between worker iterations.
- Memory safety: manage the CGO boundary (Go ↔ C/PHP) without corrupting memory.
These are the surfaces FrankenPHP owns. A vulnerability here is a FrankenPHP vulnerability:
- Request to superglobal mapping (
cgi.go,frankenphp_register_server_vars): building$_SERVER,REMOTE_ADDR,SCRIPT_NAME,PATH_INFO, and the other CGI variables from the request. - PHP script-path resolution: the request path is split on
split_path(.phpby default) intoSCRIPT_NAME/PATH_INFO, then joined to the document root withsanitizedPathJoin(filepath.Join(root, filepath.Clean("/"+reqPath))), which keepsSCRIPT_FILENAMEfrom escaping the document root (path traversal). Thephp_serverdirective additionally sets a defaulttry_filesrewrite that routes requests to existing files or the front controller, mitigating the classic PHP-FPM pitfall of executing the wrong file. - Worker-mode state isolation: FrankenPHP resets
$_GET,$_POST,$_COOKIE,$_FILES,$_SERVER, and$_REQUESTbetween requests, and explicitly clears$_SESSION(which would otherwise leak between requests), but$_ENVis not reset, andputenv()writes,staticvariables, class static properties, and globals persist across requests on the same thread. Request- or user-specific data left in that state can leak into a later request (see Worker Mode). - Per-thread environment sandboxing:
frankenphp_putenv()/frankenphp_getenv()operate on a thread-localsandboxed_envso concurrent threads don't race on the global C environment (see Internals). - CGO memory boundary: Go string pinning and
C.CString()/free()lifetimes across the Go ↔ C boundary. - Caddy admin API: the
/frankenphp/workers/restartand/frankenphp/threadsendpoints, exposed through Caddy's admin API (which listens onlocalhost:2019by default). Exposing that endpoint beyond localhost is an operator decision. - Trusted proxy handling: incoming
X-Forwarded-*headers always reach PHP as tainted$_SERVER['HTTP_X_FORWARDED_*']values; they are only trusted to derive the real client IP and scheme whentrusted_proxiesis configured.
- Vulnerabilities in the application's PHP code (SQL injection, XSS, insecure deserialization, etc.). FrankenPHP delivers untrusted request data to the application unchanged; defending against it is the application's responsibility, just as with any SAPI.
- Flaws in upstream components used by FrankenPHP (PHP, Caddy, Go) or in projects built on top of it (Laravel Octane, Symfony Runtime). Report those to the relevant project.
See SECURITY.md for how to report a security issue affecting FrankenPHP.