From fe13242c3ee9b0e6a2a0943e02b432343b3ff2ec Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Fri, 29 May 2026 09:35:16 +0200 Subject: [PATCH] docs: add security model documentation --- SECURITY.md | 6 +++++ docs/security.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++ llms.txt | 1 + 3 files changed, 69 insertions(+) create mode 100644 docs/security.md diff --git a/SECURITY.md b/SECURITY.md index ba241ac655..4be17fda3e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,6 +7,12 @@ Please ensure that you're always using the latest release. Binaries and Docker images are rebuilt nightly using the latest versions of dependencies. +## Security Model + +FrankenPHP embeds the PHP interpreter into a Go and Caddy server, so its trust boundaries span Go, C, and PHP. +Before auditing the project or reporting an issue, read the [security model documentation](docs/security.md), +which describes what is trusted, what is not, and which attack surfaces belong to FrankenPHP itself. + ## Reporting a Vulnerability If you believe you have discovered a security issue directly affecting FrankenPHP, diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000000..da732e4068 --- /dev/null +++ b/docs/security.md @@ -0,0 +1,62 @@ +--- +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." +--- + +# Security model + +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](internals.md). +For state persistence in long-running processes, see [Worker Mode](worker.md). + +## Trust boundaries + +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. | + +## Code provenance vs. data taint + +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. + +## What FrankenPHP is responsible for + +The trusted computing base has three jobs. Security defects in FrankenPHP live in one of them: + +1. **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). +2. **Isolation**: keep request-scoped state from crossing between requests, between PHP threads, and between worker iterations. +3. **Memory safety**: manage the CGO boundary (Go ↔ C/PHP) without corrupting memory. + +## In scope: FrankenPHP's own attack surface + +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` (`.php` by default) into `SCRIPT_NAME` / `PATH_INFO`, then joined to the document root with `sanitizedPathJoin` (`filepath.Join(root, filepath.Clean("/"+reqPath))`), which keeps `SCRIPT_FILENAME` from escaping the document root (path traversal). The `php_server` directive additionally sets a default `try_files` rewrite 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 `$_REQUEST` between requests, and explicitly clears `$_SESSION` (which would otherwise leak between requests), but **`$_ENV` is not reset**, and `putenv()` writes, `static` variables, 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](worker.md#state-persistence)). +- **Per-thread environment sandboxing**: `frankenphp_putenv()` / `frankenphp_getenv()` operate on a thread-local `sandboxed_env` so concurrent threads don't race on the global C environment (see [Internals](internals.md#per-thread-environment-sandboxing)). +- **CGO memory boundary**: Go string pinning and `C.CString()` / `free()` lifetimes across the Go ↔ C boundary. +- **Caddy admin API**: the `/frankenphp/workers/restart` and `/frankenphp/threads` endpoints, exposed through Caddy's admin API (which listens on `localhost:2019` by 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 when [`trusted_proxies`](production.md#running-behind-a-reverse-proxy) is configured. + +## Out of scope + +- **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. + +## Reporting a vulnerability + +See [`SECURITY.md`](../SECURITY.md) for how to report a security issue affecting FrankenPHP. diff --git a/llms.txt b/llms.txt index c0a3fde949..adef1048ee 100644 --- a/llms.txt +++ b/llms.txt @@ -10,6 +10,7 @@ This index points AI agents and crawlers at the canonical English documentation. - [FrankenPHP Worker Mode](https://frankenphp.dev/docs/worker/): keep your PHP application bootstrapped in memory between requests for lower latency. - [Configuring FrankenPHP](https://frankenphp.dev/docs/config/): Caddyfile, JSON, environment variables, and `php.ini` configuration. - [FrankenPHP Internals](https://frankenphp.dev/docs/internals/): thread types, the state machine, the Go/C/PHP CGO boundary, auto-scaling, and per-thread environment sandboxing. +- [FrankenPHP Security Model](https://frankenphp.dev/docs/security/): trust boundaries between Go, C, and PHP (which inputs are untrusted, what the runtime is responsible for, and what is out of scope). ## Setup and build