跳过内容 [alt-c]Andrew Ayer板块[博客](/blog/)[项目](/projects/)[照片](/photos/)博客[最新文章](/blog/)[热门文章](/blog/popular)[文章存档](/blog/index)[RSS 订阅](/blog/feed)2026 年 4 月 29 日FastCGI30 岁了仍是反向代理的更优协议HTTP 反向代理可谓是雷区重重。就在前几周一位研究人员披露了 Discord 媒体代理中的同步漏洞攻击者可借此窥探私人附件。这种情况并不罕见此类漏洞层出不穷。问题在于尽管 HTTP 并不适合反向代理与后端之间的通信但它仍被广泛用作这两者间的协议。不过我们并非只能使用 HTTP。有一种已有 30 年历史的协议可用于代理与后端的通信它能避开 HTTP 的诸多陷阱这就是 FastCGI其规范于 30 年前的今天发布。FastCGI 是一种网络协议而非进程模型的确有些 Web 服务器能自动生成 FastCGI 进程来处理扩展名为 .fcgi 的文件请求就像处理 .cgi 文件一样。但你不必非得这么使用 FastCGI你也可以像使用 HTTP 那样使用 FastCGI 协议通过 TCP 或 UNIX 套接字将请求发送给一个长期运行的守护进程该进程会像处理 HTTP 请求一样处理这些请求。例如在 Go 语言中你只需导入 net/http/fcgi 标准库包然后将 http.Serve 替换为 fcgi.Serve 即可。Go 语言中的 HTTP 示例l, _ : net.Listen(tcp, 127.0.0.1:8080)http.Serve(l, handler)Go 语言中的 FastCGI 示例l, _ : net.Listen(tcp, 127.0.0.1:8080)fcgi.Serve(l, handler)应用的其他部分保持不变甚至你的处理程序仍可继续使用标准的 http.ResponseWriter 和 http.Request 类型。像 Apache、Caddy、nginx 和 HAProxy 等流行的代理都支持 FastCGI 后端而且配置也很简单。nginx 的 HTTP 配置proxy_pass http://localhost:8080;nginx 的 FastCGI 配置fastcgi_pass localhost:8080;include fastcgi_params;查看更多配置示例Apache 的 HTTP 配置ProxyPass / http://localhost:8080/Apache 的 FastCGI 配置ProxyPass / fcgi://localhost:8080/Caddy 的 HTTP 配置reverse_proxy localhost:8080 {transport http { }}Caddy 的 FastCGI 配置reverse_proxy localhost:8080 {transport fastcgi { }}HAProxy 的 HTTP 配置backend app_backendserver s1 localhost:8080HAProxy 的 FastCGI 配置fcgi-app fcgi_app docroot /backend app_backenduse-fcgi-app fcgi_appserver s1 localhost:8080 proto fcgi为何 HTTP 不适合反向代理同步攻击/请求走私HTTP/1.1 表面上看起来简单不过是文本而已但实际上要想进行稳健解析却十分困难。同一 HTTP 消息有太多不同的格式而且有太多边缘情况和模糊之处不同的实现难以做到一致处理。结果就是没有两个 HTTP/1.1 实现是完全相同的同一个消息可能会被不同的解析器以不同的方式解析。最严重的问题是HTTP 消息没有明确的框架——消息本身描述了其结束位置而且有多种方式来描述每种方式都有其边缘情况。不同的实现可能会对消息的结束位置产生分歧进而对下一个消息的起始位置也产生分歧。这就是 HTTP 同步攻击也称为请求走私的根源即反向代理和后端对 HTTP 消息的边界存在分歧从而引发各种严重的安全问题比如上面提到的 Discord 漏洞。很多人似乎认为可以通过修补解析器的差异来解决问题但这是一种徒劳的策略。James Kettle 总能发现新的问题。在去年又发现了一批问题后他宣称 HTTP/1.1 必须淘汰。如果代理和后端之间始终使用 HTTP/2它可以通过明确消息边界来解决同步问题但 FastCGI 自 1996 年起就以一种更简单的协议实现了这一点。值得一提的是nginx 从首次发布就支持 FastCGI 后端但直到 2025 年末才开始支持 HTTP/2 后端。Apache 对 HTTP/2 后端的支持目前仍处于 实验阶段。为何 HTTP 不适合反向代理不可信的头部如果同步攻击是唯一的问题那使用 HTTP/2 就可以解决。但不幸的是还有另一个问题HTTP 没有一种可靠的方式让代理传达关于请求的可信信息比如真实客户端 IP 地址、经过身份验证的用户名如果代理处理身份验证或客户端证书详细信息如果使用了双向 TLS。唯一的办法是将这些信息放在 HTTP 头部中与从客户端代理过来的头部放在一起但却没有明确的结构来区分代理的可信头部和潜在攻击者的不可信头部。例如X-Real-IP 头部通常用于传达客户端的真实 IP 地址。理论上如果你的代理在添加自己的 X-Real-IP 头部之前正确删除了所有的 X-Real-IP 头部实例不仅仅是第一个还包括大小写变体如 x-REaL-ip那你就安全了。但实际上这是一个雷区后端很可能最终会信任攻击者控制的数据。你的代理实际上不仅要删除 X-Real-IP还要删除任何用于此类目的的头部以防你的系统的某些部分在你不知情的情况下依赖这些头部。例如Chi 中间件通过首先查看 True-Client-IP 头部来确定客户端的真实 IP 地址。只有当 True-Client-IP 不存在时它才会使用 X-Real-IP。所以即使你的代理正确处理了 X-Real-IP攻击者发送 True-Client-IP 头部仍可能让你遭受攻击。FastCGI 通过在客户端头部和代理添加的信息之间提供域分离完全避免了这类问题。尽管代理的可信数据和 HTTP 请求头部在同一个键值参数列表中传输到后端但 HTTP 头部名称以字符串 HTTP_ 为前缀这使得客户端在结构上不可能发送一个会被解释为可信数据的头部。FastCGI 定义了一些标准参数如 REMOTE_ADDR 来传达真实客户端 IP 地址。Go 语言的 net/http/fcgi 包会自动使用这个参数来填充 http.Request 的 RemoteAddr 字段从而无需中间件。一切都能正常工作。代理还可以使用非标准参数来报告是否使用了 HTTPS、协商的 TLS 密码套件以及是否呈现了客户端证书如果有的话。如果请求使用了 HTTPSGo 会自动将 Request 的 TLS 字段设置为非空但为空值这对于强制使用 HTTPS 非常方便。可以使用 fcgi.ProcessEnv 函数来访问代理发送的完整可信参数集。结语如果 FastCGI 是更优的协议为什么它没有更受欢迎呢也许是因为它的名字——虽然在 1996 年借助 CGI 的流行来命名有一定道理但到了 2026 年CGI 显得有些过时了。此外人们对 HTTP 反向代理的安全问题仍然缺乏足够的认识。Watchfire 在 2005 年就描述了同步攻击并对其难以解决的特性发出了有先见之明的警告但这些攻击在十多年里却被莫名其妙地忽视了。在另一个时间线里如果 Watchfire 的研究得到重视人们可能会去寻找其他用于反向代理的协议。FastCGI 如今仍然非常实用SSLMate 已经使用它进行生产超过 10 年了。不过使用这种古老的技术也有一些缺点。它从未更新以支持 WebSocket相关工具也不够完善。例如curl 无法向 FastCGI 服务器发送请求。它支持 FTP、Gopher甚至 SMTP不管它是如何工作的但不支持 FastCGI。当对 Go 语言的 FastCGI 服务器在各种反向代理后的性能进行基准测试时与 HTTP/1.1 或 HTTP/2 相比某些工作负载的吞吐量更差。认为这并非协议本身的固有问题而是反映出 FastCGI 的代码路径没有像 HTTP 那样得到充分优化。尽管存在这些缺点仍然认为 FastCGI 值得使用。不使用 WebSocket而且它对于使用场景来说速度足够快也许对你也是如此。如果它真的成为了瓶颈宁愿购买更多硬件也不愿面对 HTTP 反向代理带来的噩梦。祝 FastCGI 30 岁生日快乐不要错过我的下一篇文章输入你的电子邮件地址通过邮件接收我的最新文章订阅你也可以通过 RSS 订阅或者在联邦宇宙Mastodon或 Bluesky 上关注我。旧文章[查看存档](/blog/index)为什么 IP 地址证书很危险且通常不必要评论目前还没有评论。发表评论你的评论将公开。若想私下联系我请给我发邮件。请保持评论礼貌、切题且易于理解。你的评论可能会在发布前经过审核。你的姓名可选将公开你的电子邮件地址可选不会公开你的网站可选将公开空行分隔段落。以 开头的行将作为块引用缩进。以两个空格开头的行将原样输出适合代码。用 *星号* 包围的文本将变为 _斜体_。用 反引号 包围的文本将变为 等宽字体。URL 将自动转换为链接。使用预览按钮检查你的格式。