diff --git a/docs/internals.md b/docs/internals.md index 4b0153a454..38c7714808 100644 --- a/docs/internals.md +++ b/docs/internals.md @@ -228,7 +228,7 @@ FrankenPHP sandboxes environment variables per-thread: 1. At startup, the main thread snapshots `os.Environ()` into `main_thread_env` (a PHP `HashTable`). 2. `$_SERVER` is built from a copy of `main_thread_env` plus request-specific variables (in `frankenphp_register_server_vars`). It is rebuilt for every request, including each iteration of a worker script. 3. `$_ENV` is populated from the same snapshot through PHP's `php_import_environment_variables` hook. In regular mode this happens once per script execution; in worker mode it happens once when the worker script starts and is **not** rebuilt between worker requests, which is why writes to `$_ENV` leak across requests (see [Worker Mode](worker.md)). -4. `frankenphp_getenv()` looks a variable up in thread-local layers, the first match wins: `sandboxed_env` (`putenv()` writes and unsets), `prepared_env` (the `env` of the current request, released after each worker request), `worker_env` (the `env` of the worker script, kept for all its requests), then `main_thread_env`. `putenv()` writes land only in `sandboxed_env` and **never** mutate the process-global OS environment, so a value set by one request is not visible to the Go/Caddy side, to child processes (`proc_open`, `exec`, ...), or to other requests. This also avoids races on the global C environment. +4. `frankenphp_getenv()` looks a variable up in thread-local layers, the first match wins: `sandboxed_env` (`putenv()` writes and unsets), `prepared_env` (the `env` of the current request, released after each worker request), `worker_env` (the `env` of the worker script, kept for all its requests), then `main_thread_env`. `putenv()` writes land only in `sandboxed_env` and **never** mutate the process-global OS environment, so a value set by one request is not visible to the Go/Caddy side, to child processes (`proc_open`, `exec`, ...), or to other threads. This also avoids races on the global C environment. 5. `reset_sandboxed_environment()` releases `sandboxed_env` after each PHP script execution. In regular mode that's per request; in worker mode it only runs when the worker script itself exits, so `putenv()` writes are visible to subsequent worker requests on the same thread until the script restarts. ## Request flow (regular mode) diff --git a/docs/security.md b/docs/security.md index cbb4c06d19..97dbc9ce59 100644 --- a/docs/security.md +++ b/docs/security.md @@ -47,7 +47,7 @@ These are the surfaces FrankenPHP owns. A vulnerability here is a FrankenPHP vul - **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, explicitly clears `$_SESSION` (which would otherwise leak between requests) and the errors returned by `error_get_last()` and `libxml_get_errors()`, and the headers returned by `http_get_last_response_headers()`, the `strtok()` state, and the variables added by `output_add_rewrite_var()`, but **`$_ENV` is not reset**, and `putenv()` writes, `static` variables, class static properties, globals, and runtime engine settings (`ini_set()`, the default stream context, the default timezone, the working directory, ...) 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`. `putenv()` writes stay inside that sandbox and never reach the process-global OS environment, so a secret set by one request cannot leak to child processes (`proc_open`, `exec`, ...), to the Go/Caddy side, or to other requests. A child process that must see such a variable has to be given it explicitly, through the environment argument of `proc_open()` or `exec()` (see [Internals](internals.md#per-thread-environment-sandboxing)). +- **Per-thread environment sandboxing**: `frankenphp_putenv()` / `frankenphp_getenv()` operate on a thread-local `sandboxed_env`. `putenv()` writes stay inside that sandbox and never reach the process-global OS environment, so a secret set by one request cannot leak to child processes (`proc_open`, `exec`, ...), to the Go/Caddy side, or to other threads. In worker mode, it stays visible to the following requests on the same thread until the worker script restarts. A child process that must see such a variable has to be given it explicitly, through the environment argument of `proc_open()` or `exec()` (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. diff --git a/frankenphp.c b/frankenphp.c index 4066845349..27ffb92174 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -771,7 +771,7 @@ PHP_FUNCTION(frankenphp_finish_request) { /* {{{ */ /* {{{ Sandboxed putenv() that never mutates the process-global OS environment. * Writes land only in the thread-local sandboxed_env, so a secret passed to * putenv() by one request cannot leak to the Go/Caddy side, to child processes - * (proc_open, exec, ...), or to other requests. */ + * (proc_open, exec, ...), or to other threads. */ PHP_FUNCTION(frankenphp_putenv) { char *setting; size_t setting_len;