Caddy v2.11.8:修复PHP/HTTP/2漏洞,弃用无效Host与超大请求体
v2.11.8
本次更新包含多项破坏性变更(如 Host 校验收紧、max_size 行为改变)及关键的安全修复。依赖 Caddy 处理 PHP 或特定 HTTP/2 场景的读者必须评估配置兼容性,建议在生效前完成迁移测试。
This patch release is a big batch of bug fixes and security hardening, plus a handful of small additions. This release is cut from our 2.11 maintenance branch: fixes are backported from master. We recommend upgrading, especially if you serve PHP or use {host} in file paths.
此补丁版本包含大量错误修复和安全加固,以及少量新增功能。本版本基于我们的 2.11 维护分支构建:修复内容从 master 分支向后移植。我们建议升级,特别是如果您提供 PHP 服务或在文件路径中使用 {host}。
Huge thank you to our sponsors for keeping the project alive with resources, and for our maintainers who triage and assist tirelessly in this relentless new age of AI.
衷心感谢我们的赞助商以资源支持让项目得以延续,也感谢我们的维护者在 AI 这个无情新时代中不知疲倦地进行分类和协助。
⚠️ Please read the breaking changes below before upgrading. Most of them reject configs or requests that never did what they looked like they did, so most setups won't notice.
⚠️ 请在升级前阅读下方的破坏性变更说明。其中大多数是拒绝那些从未真正按预期工作的配置或请求,因此大多数部署不会受到影响。
Highlights
亮点
- Idle timeouts, round two. More fixes for the idle read/write timeouts introduced in 2.11.6:
- Per-route timeouts now take precedence over the server-wide read_body_idle / write_idle, and nested timeouts use the innermost one. (#8121)
- Over HTTP/2, a handler that paused reading the request body (like reverse_proxy uploading to a slow upstream) no longer loses the body and fails with 502. (#8125)
- Deadlines set by handlers now act as a ceiling instead of being overridden, so things like Mercure's dispatch_timeout and FrankenPHP's request_body_timeout work again. Thanks @dunglas! (#8145, #8284)
- HTTP/1.1 uploads that time out with a little body left no longer hang instead of sending the error response. Thanks @SiyXo511! (#8178)
- Binding privileged ports with fewer capabilities (Linux): If CAP_NET_BIND_SERVICE is permitted but not effective, Caddy raises it just for binding a privileged port. Packagers can use setcap cap_net_bind_service=+p instead of +ep, so Caddy still starts under --cap-drop ALL, OpenShift's restricted-v2, or systemd units with an empty CapabilityBoundingSet=. It also follows the kernel's ip_unprivileged_port_start instead of assuming 1024. (#8102, #8253)
- Small additions:
- new log_level directive to set the access log level for matching requests, e.g. log_level @healthchecks DEBUG (#8201)
- new {time.now.rfc3339} placeholder. Thanks @steadytao! (#8140)
- new not matcher for TLS connection policies. Thanks @simonether! (#8138)
- the tls matcher can be used in CEL expression matchers: tls(), tls('complete'), tls('early_data'). Thanks @stackedbyaradhya! (#8088)
- Reverse proxy:
- headers you set yourself (like Strict-Transport-Security via header) are no longer lost when the upstream sends a 1xx response first, like 103 Early Hints (#8169)
- request trailer values are forwarded, not just their names. Thanks @krishsinghhura! (#8246)
- request-only expressions in lb_retry_match no longer retry successful responses (a regression since 2.11.3) (#8168)
- dial errors through a forward proxy and to FastCGI backends are retried like other dial errors. Thanks @adarshxsh and @zemamba! (#8264, #8172)
- weighted_round_robin no longer panics with fewer weights than upstreams, and no longer picks an unavailable upstream when there's only one weight; random_choose picks the least loaded host when all are busy (#8159, #8320, #8160)
- with request_buffers / response_buffers unlimited, a body that fails partway is no longer treated as complete (#8207)
- connections and sockets are no longer leaked when a protocol upgrade (e.g. WebSocket) fails during setup, or when a handle_response status isn't a number. Thanks @Version2beta and @BenMacha! (#8269, #8318)
- HTTP/3 client cancellations are logged at debug level instead of as warnings. Thanks @larrasket! (#8135)
- encode: If-None-Match lists and weak tags get 304s properly (#8128), HEAD responses no longer get a bogus Content-Length (#8150), and Accept-Encoding / Cache-Control split across several header lines are all taken into account, with faster header handling in the proxy too. Thanks @Indra55, @jayhemnani9910 and @dunglas! (#8025)
- PHP / FastCGI: a Location response without a Status is now a 302 redirect, as CGI specifies (#8209), and an app that writes more than its declared Content-Length (like a PHP warning after the page) no longer corrupts HTTP/1.1 responses or kills HTTP/2 connections. Thanks @Sulaiman-Dauda! (#8212)
- TLS & ECH:
- when Caddy rejects Encrypted ClientHello, it now sends retry configs, so clients with stale ECH configs can recover. Thanks @videah! (#8142)
- an ECH public name that also has its own site block keeps that site's TLS settings (#8156)
- client_auth modes request and verify_if_given no longer reject clients without a certificate when verifiers are configured (#8251)
- a site block spanning several listener addresses no longer lets one listener's TLS connection policy (including client authentication) overwrite another's. Thanks @steadytao! (#8192)
- repeated matcher lines in a TLS policy match block are all applied, not just the first. Thanks @simonether! (#8137)
- merged automation policies keep a specific domain's policy ahead of a wildcard's (#8287), and an ACME issuer's network_proxy survives re-provisioning (#8288)
- Caddyfile:
- caddy run prints the error again when startup fails, instead of silently exiting with code 1. Thanks @IndraGunawan! (#8180)
- routes in a group that already matched no longer run their matchers, which fixes e.g. try_files {path} {path}.html?{query} =404 returning 404 for everything (#8189)
- multiple handle_errors blocks and vars now follow Caddyfile order (#8195, #8197)
- each ?Field default in a header block is checked on its own, and respects match (#8217)
- import shows the real OS error for files it can't read (#8163), negative {args[...]} indices no longer panic (#8316), and very long heredoc markers no longer make parsing crawl (#8203, #8249)
- file_server: a path below a regular file (/index.php/foo) is a 404 again and works with pass_thru (#8131); browse handles symlinks in directories with escaped names (#8152) and offsets past the end (#8213)
- templates: template errors no longer keep stale Content-Length / Etag headers (#8149), bodyless responses no longer get Content-Length: 0 (which broke iOS PWAs) (#8153), plus fixes for humanize time layouts with colons and front matter closed with ... (#8215, #8259)
- Logging: include of a parent namespace includes its child modules (#8221), dir_mode from_file no longer creates unusable directories (#8225), and log routing to a site's logger ignores Host case (#8176)
- Admin API: with remote admin access_control, a request is allowed if any permission entry permits it, not just the first (#8317), and Cache-Control: must-revalidate is recognized alongside other directives (#8330)
- Other fixes: caddy hash-password handles Windows line endings and input without a final newline (#8227), a data race in the basic_auth cache is gone (#8278), the Windows service no longer gets stuck in START_PENDING (#8274), and uri strip_prefix / strip_suffix handle percent-escapes and literal % correctly (#8005, #8345)
- Release builds use Go 1.27 (Go 1.26 is still the minimum to build Caddy). (#8210)
- 空闲超时,第二轮。针对 2.11.6 中引入的空闲读/写超时的更多修复:
- 每路由超时现在优先于服务器范围的 read_body_idle / write_idle,嵌套超时使用最内层的超时值。(#8121)
- 在 HTTP/2 下,暂停读取请求体的处理程序(例如 reverse_proxy 向缓慢的上游上传时)不再丢失请求体并导致 502 错误。(#8125)
- 由处理程序设置的截止时间现在作为上限起作用,而不会被覆盖,因此 Mercure 的 dispatch_timeout 和 FrankenPHP 的 request_body_timeout 等功能恢复正常。感谢 @dunglas!(#8145, #8284)
- HTTP/1.1 上传在剩余少量数据时超期后,不再挂起而是发送错误响应。感谢 @SiyXo511!(#8178)
- 使用较少权限绑定特权端口(Linux):如果允许 CAP_NET_BIND_SERVICE 但未生效,Caddy 仅在为特权端口绑定时提升该权限。打包者可以使用 setcap cap_net_bind_service=+p 代替 +ep,这样 Caddy 仍可在 --cap-drop ALL、OpenShift 的 restricted-v2 或 CapabilityBoundingSet= 为空的 systemd 单元下启动。它还遵循内核的 ip_unprivileged_port_start 设置,而非假设值为 1024。(#8102, #8253)
- 小型新增功能:
- 新增 log_level 指令,用于设置匹配请求的访问日志级别,例如 log_level @healthchecks DEBUG(#8201)
- 新增 {time.now.rfc3339} 占位符。感谢 @steadytao!(#8140)
- TLS 连接策略新增 not 匹配器。感谢 @simonether!(#8138)
- tls 匹配器可用于 CEL 表达式匹配器:tls()、tls('complete')、tls('early_data')。感谢 @stackedbyaradhya!(#8088)
- 反向代理:
- 当您自行设置的标头(如通过 header 设置的 Strict-Transport-Security)在上游首先发送 1xx 响应(如 103 Early Hints)时,不再丢失。(#8169)
- 请求尾部值会被转发,而不仅仅是其名称。感谢 @krishsinghhura!(#8246)
- lb_retry_match 中的仅请求表达式不再重试成功响应(自 2.11.3 以来的回归问题)(#8168)
- 通过正向代理和 FastCGI 后端的拨号错误像其他拨号错误一样被重试。感谢 @adarshxsh 和 @zemamba!(#8264, #8172)
- weighted_round_robin 在权重少于上游服务器时不再发生 panic,且当只有一个权重时不再选择不可用的上游;random_choose 在所有主机繁忙时选择负载最少的主机 (#8159, #8320, #8160)
- 当 request_buffers / response_buffers 设置为无限制时,中途失败的主体不再被视为完整 (#8207)
- 当协议升级(例如 WebSocket)在设置期间失败,或 handle_response 状态不是数字时,不再泄漏连接和套接字。感谢 @Version2beta 和 @BenMacha!(#8269, #8318)
- HTTP/3 客户端取消操作以调试级别记录而非警告级别。感谢 @larrasket!(#8135)
- encode:If-None-Match 列表和弱标签正确返回 304 (#8128),HEAD 响应不再包含虚假的 Content-Length (#8150),并且跨越多行标头的 Accept-Encoding / Cache-Control 均被考虑在内,代理中的标头处理也更快。感谢 @Indra55、@jayhemnani9910 和 @dunglas!(#8025)
- PHP / FastCGI:没有 Status 的 Location 响应现在作为 302 重定向,符合 CGI 规范 (#8209),且写入内容超过其声明的 Content-Length 的应用程序(如页面后的 PHP 警告)不再损坏 HTTP/1.1 响应或中断 HTTP/2 连接。感谢 @Sulaiman-Dauda!(#8212)
- TLS & ECH:
- 当 Caddy 拒绝 Encrypted ClientHello 时,它现在发送重试配置,使拥有过时 ECH 配置的客户端能够恢复。感谢 @videah!(#8142)
- 具有自己站点块的 ECH 公共名称保留该站点的 TLS 设置 (#8156)
- client_auth 模式 request 和 verify_if_given 在配置了验证器时不再拒绝没有证书的客户端 (#8251)
- 跨越多个监听地址的站点块不再允许一个监听器的 TLS 连接策略(包括客户端身份验证)覆盖另一个的策略。感谢 @steadytao!(#8192)
- TLS 策略匹配块中重复的匹配器行全部应用,而不仅仅是第一行。感谢 @simonether!(#8137)
- 合并自动化策略时,特定域名的策略保持在通配符之前 (#8287),且 ACME 发行者的 network_proxy 在重新配置后得以保留 (#8288)
- Caddyfile:
- caddy run 在启动失败时再次打印错误,而不是静默退出并返回代码 1。感谢 @IndraGunawan!(#8180)
- 已匹配的组内的路由不再运行其匹配器,这修复了例如 try_files {path} {path}.html?{query} =404 对一切返回 404 的问题 (#8189)
- 多个 handle_errors 块和变量现在遵循 Caddyfile 顺序 (#8195, #8197)
- header 块中的每个 ?Field 默认值都单独检查,并尊重匹配条件 (#8217)
- import 显示它无法读取的文件的真实 OS 错误 (#8163),负数 {args[...]} 索引不再导致 panic (#8316),且非常长的 heredoc 标记不再使解析过程变慢 (#8203, #8249)
- file_server:常规文件下的路径(如 /index.php/foo)再次变为 404,并与 pass_thru 配合工作 (#8131);browse 处理目录中名称含转义字符的符号链接 (#8152) 以及超出末尾的偏移量 (#8213)
- templates:模板错误不再保留过时的 Content-Length / Etag 头 (#8149),无 body 的响应不再获得 Content-Length: 0(这会破坏 iOS PWA)(#8153),此外还修复了带冒号的时间 humanize 布局以及以 ... 关闭的前置元数据的问题 (#8215, #8259)
- 日志记录:包含父命名空间时会包含其子模块 (#8221),来自 from_file 的 dir_mode 不再创建不可用的目录 (#8225),且将日志路由到站点日志记录器时忽略 Host 的大小写 (#8176)
- Admin API:使用远程管理访问控制时,如果任何权限条目允许该请求,则允许请求,而不仅仅是第一个 (#8317),并且 Cache-Control: must-revalidate 与其他指令一起被识别 (#8330)
- 其他修复:caddy hash-password 处理 Windows 换行符以及没有最终换行符的输入 (#8227),basic_auth 缓存中的数据竞争已消除 (#8278),Windows 服务不再卡在 START_PENDING 状态 (#8274),uri strip_prefix / strip_suffix 正确处理百分号转义和字面量 % (#8005, #8345)
- 发布构建使用 Go 1.27(Go 1.26 仍是构建 Caddy 的最低版本)。(#8210)
⚠️ Breaking changes
⚠️ 破坏性变更
- Requests with an invalid or dots-only host (like ..) are rejected with 400 on every protocol, before any route runs. Before, HTTP/2 and HTTP/3 also accepted hosts containing / or \. (#8328)
- request_body max_size now rejects a request with 413 as soon as its Content-Length is over the limit, even if nothing would have read the body. (#8337)
- php_fastcgi sites without an index.php: requests for paths that don't exist are now sent to PHP as index.php, as the documented first_exist_fallback try policy always promised, instead of falling through to file_server and returning 404. Also, PATH_INFO and DOCUMENT_URI are now derived from the normalized path (repeated slashes merged, . segments removed); REQUEST_URI is unchanged.
- A try_files / file candidate ending in / only matches a directory, so /foo.txt/ no longer matches the file /foo.txt. (#8326)
- 具有无效或仅含点的主机(如 ..)的请求在每个协议上、在任何路由运行之前都会以 400 拒绝。此前,HTTP/2 和 HTTP/3 也接受包含 / 或 \ 的主机。(#8328)
- request_body max_size 现在一旦请求的 Content-Length 超过限制就立即以 413 拒绝请求,即使没有任何内容会读取 body。(#8337)
- 没有 index.php 的 php_fastcgi 站点:对于不存在的路径的请求现在会作为 index.php 发送给 PHP,正如文档中 first_exist_fallback try 策略所承诺的那样,而不是继续传递到 file_server 并返回 404。此外,PATH_INFO 和 DOCUMENT_URI 现在从规范化路径派生(重复的斜杠被合并,. 段被移除);REQUEST_URI 保持不变。
- 以 / 结尾的 try_files / file 候选项仅匹配目录,因此 /foo.txt/ 不再匹配文件 /foo.txt。(#8326)
更进一步:量化金融体系
看懂新闻只是起点——沿量化金融路径,把它变成能交付的工程能力