FrankenPHP 安全模型:Go 与 PHP 之间的信任边界解析
【免费下载链接】frankenphp🧟 The modern PHP app server项目地址: https://gitcode.com/GitHub_Trending/fr/frankenphp
本篇技术指南系统梳理 FrankenPHP 的信任模型(trust model):哪些输入是可信任的、哪些是不可信任的,信任边界究竟落在哪里。FrankenPHP 以 CGO 方式把 PHP 解释器嵌入 Go/Caddy 服务器(见 README.md 与 docs/internals.md),因此其攻击面横跨 Go、C、PHP 三层。读完本文,你将能够为安全审计、自动化扫描器与渗透测试正确划定 FrankenPHP 自身的责任范围,并理解请求映射、脚本路径解析、Worker 状态隔离、环境变量沙箱、慢速请求防御等关键防线背后的源码实现。本文聚焦于FrankenPHP 本身的安全边界,而非它所承载的 PHP 应用程序。
信任边界:四个参与者
FrankenPHP 运行的整个技术栈由四个截然不同的参与者构成,它们各自的信任级别决定了安全审计时需要检视的对象:
| 参与者 | 信任级别 | 说明 |
|---|---|---|
| 远程客户端(Remote client) | 不可信任 | HTTP 请求(方法、URI、请求头、Cookie、请求体、上传文件)是污染(tainted)输入的主要来源 |
| 运维人员(Operator) | 可信任 | 提供部署配置:Caddyfile、环境变量、php.ini、已安装的 PHP 扩展与 Caddy 模块,以及应用代码本身 |
| PHP 应用代码 | 按来源可信任(trustedby provenance) | 由运维人员部署,因此 FrankenPHP 永远不会执行攻击者提供的代码;但这段代码会消费不可信任的请求数据 |
| FrankenPHP(Go + C) | 可信计算基(TCB) | 内嵌 PHP,负责数据进出传输,并隔离请求与线程。它自身的缺陷才是本文档所界定的审计范围 |
这一分层明确回答了安全审计中最常见的问题:"我们到底该信任 PHP 吗?"——答案取决于你问的是"代码"还是"数据"。
代码来源可信 vs. 数据污染:最关键的一对概念
这是整个信任模型中最重要、也是最能化解"我们到底信不信任 PHP"这一困惑的区分:
- 代码来源(Code provenance)是可信任的。FrankenPHP 只执行由运维人员部署的 PHP 文件:文档根目录下解析出的脚本,或配置好的 Worker 脚本。它从不评估请求本身携带的代码(请求体、查询字符串、请求头):SAPI 边界传递的是数据,永远不会是不可信任的代码。请求路径的分割逻辑位于 cgi.go 的
splitCgiPath,最终脚本路径由sanitizedPathJoin(cgi.go)确定——这部分在后文"脚本路径解析"小节展开。 - 请求数据是污染的。PHP 代码从请求中读取的一切(
$_GET、$_POST、$_COOKIE、$_FILES、$_SERVER、php://input)都是不可信任的,这与任何 PHP SAPI 完全一致。清洗这些数据是应用层的责任。
因此,"我们信任来自 PHP 的东西"对代码成立,而"SAPI 承载不可信任的输入"对数据成立,二者并不矛盾。FrankenPHP 的职责是忠实地搬运这些污染数据,并确保一个请求的数据不会泄漏到另一个请求中去。
FrankenPHP 的三大职责
可信计算基(TCB)承担三项任务,FrankenPHP 自身的安全缺陷必居其一:
- 忠实传输(Faithful transport):将请求映射为 PHP 超全局变量与
php://input,并把 PHP 的输出与响应头带回客户端,过程中不得引入注入(请求头/CRLF 注入、请求走私、执行错误文件)。 - 隔离(Isolation):防止请求级状态在请求之间、PHP 线程之间以及 Worker 迭代之间发生串扰。
- 内存安全(Memory safety):管理 Go ↔ C/PHP 之间的 CGO 边界,不产生内存破坏。
范围内:FrankenPHP 自身的攻击面
以下表面归 FrankenPHP 所有,这里的漏洞即 FrankenPHP 漏洞:
1. 请求到超全局变量的映射
$_SERVER、REMOTE_ADDR、SCRIPT_NAME、PATH_INFO等 CGI 变量的构造,由 cgi.go 的addKnownVariablesToServer与 frankenphp.c 中的frankenphp_register_server_vars协同完成。值得注意的实现细节包括:
- Go 侧通过 go_register_server_variables 这个 cgo 导出回调,将已知 CGI 变量、请求头以及
PreparedEnv(fc.env)合并进 PHP 的track_vars_array,其中环境变量最后注册并允许覆盖前面的值。 - 对于非常见请求头,addHeadersToServer 走
frankenphp_register_variable_safe路径,交由 PHP 侧做额外消毒,而非使用缓存的常见请求头快路径。 - splitRemoteAddr 对畸形地址(例如单独的
[)做了防御性处理:net.SplitHostPort失败后降级为手工解析,避免在 cgo 回调中 panic 展开而拖垮整个进程。 - HTTPS 场景下会按 Apache mod_ssl 兼容格式填充
SSL_PROTOCOL、SSL_CIPHER等变量(tlsProtocol)。
2. PHP 脚本路径解析(防目录穿越与错误文件执行)
请求路径首先经split_path(默认.php)拆分为SCRIPT_NAME/PATH_INFO,再与文档根目录通过sanitizedPathJoin拼接:
// cgi.go path := filepath.Join(root, filepath.Clean("/"+reqPath))这个实现借鉴了 Go 标准库http.Dir的防护逻辑:root被视为可信路径,而reqPath不可信,拼接结果永远不会逃出 root,从而阻断路径穿越(path traversal)。同时 splitPos 采用严格 ASCII 大小写不敏感匹配,字节值>= utf8.RuneSelf的路径段永远不会命中任何 split 项——这是针对曾经出现过的 Unicode 等价字符绕过(如全角或数学字母折叠为 ASCII,导致攻击者上传的文件被当作 PHP 执行)的修复,源码注释中明确引用了 GHSA-3g8v-8r37-cgjm 与 GHSA-v4h7-cj44-8fc8 两个安全公告。
此外,php_server指令还会设置默认的try_files重写规则,将请求路由到已存在的文件或前端控制器,从而缓解经典 PHP-FPM 陷阱——"执行了错误的文件"(详见 docs/config.md 与 docs/performance.md 中的等价 route 配置)。
3. Worker 模式的请求级状态隔离
Worker 模式让 PHP 进程常驻内存,因此请求之间的状态隔离是安全关键。C 层的 frankenphp_reset_super_globals 在每个请求之间:
- 显式刷新
$_FILES;$_GET、$_POST、$_COOKIE、$_SERVER在重新导入时被刷新; - 但
$_ENV不被刷新; $_SESSION必须显式从符号表中删除——因为它存储在EG(symbol_table)中且带有对PS(http_session_vars)的引用,而会话的 RSHUTDOWN 只减少引用计数却不将其从符号表移除,否则会导致数据在请求间泄漏(frankenphp.c)。此外frankenphp_reset_session_state(frankenphp.c)会刷新活动会话、关闭会话模块、释放会话 ID 并保留用户自定义处理器。
需要强调的是,putenv()写入、static变量、类静态属性以及全局变量在同一线程的多个请求之间会持续存在。请求相关或用户特有的数据一旦残留在这些状态里,就可能泄漏给后续请求。关于状态持久化的完整说明与示例,见 docs/worker.md。
4. 每线程环境变量沙箱
由于 PHP 的 ZTS(Zend 线程安全)模型要求每个 PHP 执行运行在真实 POSIX 线程上,若多个线程直接操作全局 C 环境(setenv/getenv),将产生数据竞争。因此 frankenphp.c 实现了线程局部的环境沙箱:
- 主线程启动时把
os.Environ()快照进main_thread_env; frankenphp_putenv()(frankenphp.c)与frankenphp_getenv()(frankenphp.c)都作用于懒初始化自main_thread_env的线程局部sandboxed_env(声明于 frankenphp.c);reset_sandboxed_environment()(frankenphp.c)在每次 PHP 脚本执行后释放sandboxed_env:普通模式即每次请求,Worker 模式则只在 Worker 脚本自身退出时执行——因此putenv()的写入在同一线程的后续 Worker 请求中可见,直到脚本重启。
环境变量的填充时序与$_ENV的差异详见 docs/internals.md。
5. CGO 内存边界
Go 与 C/PHP 之间的字符串生命周期管理是典型的内存安全风险点:
- Go → C 字符串经
C.CString()以malloc()分配,由 C 侧负责释放(如frankenphp_free_request_context()释放 Cookie 数据); - 为减少拷贝,phpthread.go 中的
phpThread内嵌 Go 的runtime.Pinner,通过thread.Pin()/thread.Unpin()钉住 Go 内存供 C 引用,每次脚本执行后解除钉住; - PHP 侧内存由 Zend 内存管理器(
emalloc/efree)管理,在请求关闭时自动释放。
6. Caddy Admin API
/frankenphp/workers/restart与/frankenphp/threads两个端点通过 Caddy 的 Admin API 暴露(caddy/admin.go),而 Caddy Admin API 默认监听localhost:2019。是否将该端点暴露到 localhost 之外,属于运维人员的决策。Worker 的优雅重启调用方式参见 docs/worker.md:
curl -X POST http://localhost:2019/frankenphp/workers/restart7. 可信代理处理(Trusted Proxies)
传入的X-Forwarded-*请求头始终以污染的$_SERVER['HTTP_X_FORWARDED_*']值到达 PHP;只有当配置了trusted_proxies时,它们才会被信任用于推导真实客户端 IP 与协议。若不配置,X-Forwarded-For、X-Forwarded-Proto等头将被忽略,可能导致 HTTPS 误判或客户端 IP 错误——此时 PHP 框架侧通常还需要同步配置受信代理(如 Symfony 的TRUSTED_PROXIES或 Laravel 的trustedproxies中间件)。
8. 慢速请求体(Slow-POST DoS)
一个声明了请求体却慢慢"滴水式"发送或干脆停滞的客户端,会长时间占用处理线程。在线程池有上限的情况下,足够多的此类连接即可耗尽线程池(典型的 slow-POST DoS)。FrankenPHP 的默认防线是对请求体读取施加60 秒空闲超时(request_body_timeout):
php_server { request_body_timeout 60s # 默认值;设置为 0 可禁用 }其原理是在每次读取前重置 deadline(deadline 重置机制见 requestbodytimeout_test.go,测试覆盖 HTTP/1 与 HTTP/2 两个版本),因此:
- 持续稳定上传的请求体(无论多大)可以成功完成;
- 停滞的请求会被切断,线程被释放。
该默认值定义于 caddy/caddy.go(defaultRequestBodyTimeout = 60 * time.Second),并在 caddy/module.go 中作为RequestBodyTimeout字段暴露,0表示禁用。
范围外:不属于 FrankenPHP 的缺陷
- 应用 PHP 代码中的漏洞(SQL 注入、XSS、不安全反序列化等)。FrankenPHP 将不可信任的请求数据原样送达应用,防御它们是应用的责任,与任何其他 SAPI 无异。
- 上游组件的缺陷:FrankenPHP 依赖的 PHP、Caddy、Go 本身的缺陷,以及构建在其之上的项目(Laravel Octane、Symfony Runtime)的问题,应上报给相应项目。
这一点在 SECURITY.md 中有明确呼应:只有直接影响 FrankenPHP 本身的漏洞才应向本项目报告;影响其依赖或被其使用组件的缺陷应上报给相关项目。
上报安全漏洞
如果你认为发现了直接影响 FrankenPHP 的安全问题,请不要公开披露。完整的上报流程、受支持版本策略(仅最新版本受支持,二进制与 Docker 镜像每晚基于最新依赖重建)见 SECURITY.md。
延伸阅读
- docs/internals.md:线程模型、状态机与 CGO 边界的内部机制(环境沙箱、线程状态机、请求流程)
- docs/worker.md:长驻进程的状态持久化与超全局变量行为
- docs/config.md:
request_body_timeout、split_path、trusted_proxies等配置项的完整说明 - docs/production.md:反向代理场景下的可信代理配置
- SECURITY.md:安全政策与漏洞上报渠道
【免费下载链接】frankenphp🧟 The modern PHP app server项目地址: https://gitcode.com/GitHub_Trending/fr/frankenphp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考