背景
这是我发现该问题的流程,只是想快速了解问题原因和应对方法的读者可忽略本节。
不久前的某天早上,我访问我的 Nextcloud 实例时突然报警告提示“通过不被信任的域名访问”(“Access through untrusted domains”)。
我的 Nextcloud 实例部署在家庭局域网中的一台运行 Debian 12 (Bookworm) 的 NAS 上,未使用 Docker,直接通过 nginx 下的 PHP-FPM 运行。访问路径是这样的:用户访问 nextcloud.example.com:11451,其中 nextcloud.example.com 解析至我的家庭宽带公网 IP,11451 这个高位端口则在路由器上设置为转发至 NAS 的 nginx 服务端口,因为国内家宽无法在标准 Web 端口上提供服务。
这个报错让我摸不着头脑,因为之前一直正常运行,从未遇到这样的问题。我最近并没有对Nextcloud 和 nginx 配置做任何修改,也没有更新过相关软件;唯一的更改是最近我部署了一个 Vaultwarden 实例(之后我打算为此写一篇博客)并添加了对应的 nginx 配置,但我检查了配置没有任何问题。
后来我注意到 nginx 进程在半夜被重启过一次,其他一些关键进程如 sshd 也是如此,一开始我还怀疑是 NAS 被入侵了,通过日志检查才发现是无人值守更新(unattended upgrade)机制自动执行了更新。这主要用于自动安装一些重要的安全更新,通过定时任务静默触发,无需用户介入。说实话我是第一次听说,不过毕竟这对于用户是无感的,也难怪。
其中,我注意到 nginx 版本被更新至 1.22.1-9+deb12u6。在 LLM 的帮助下我检查了一下这次更新的内容。更新日志中提到其中一个变化是将 $http_host 替换为了 $host。这有些可疑,我随后又注意到了 /etc/nginx/*_params 的注释中统一加上了一段说明(见附录)。由此我花时间分析了一下这次更新,发现这便是导致 Nextcloud 出现上述问题的元凶,于是有了本文。
影响范围与行为差异
这次更新修改了所有 /etc/nginx/*_params 配置,这些配置用于指定一些反向代理常用的参数与标头(headers),例如 fastcgi_params(PHP-FPM 会用到)、proxy_params(反向代理转发)等。具体而言,所有的 Host 标头都被从 $http_host 改为了 $host。$http_host 是用户发往 nginx 请求的 Host 标头中的原始值,而 $host 则是经过过滤另行生成的主机名。
它们两者有一个非常明显的区别是,$host 在任何情况下都是不含端口号的。比如说以我的情况为例,之前 Nextcloud 看到的 Host 标头是 nextcloud.example.com:11451,这也是预先写在 trusted_domains 配置中的值;而nginx 更新后,Nextcloud 看到的则是 nextcloud.example.com,从而无法匹配预设的名称,因此会拒绝访问。
因此,nginx 的这次更新并不是向下兼容的,在特定条件下会对后端应用原本的业务产生影响。受影响的环境需要满足以下所有条件:
首先,你得安装了这次更新:
- 软件源版本为 Debian 11 (Bullseye) 至 13 (Trixie),即 APT 源中的 nginx 为 1.30.0 以下的版本并且仍然在进行安全更新。
- 更新过 nginx,包括手动更新或开启了无人值守更新
其次,你需要用到被修改的参数:
- 配置文件中引用了
*_params,即包含include *_params;命令(例如include fastcgi_params;) - 后端应用的业务逻辑中使用了 nginx 传递的
Host标头
- 配置文件中引用了
最后,你的服务部署在非标准 Web 端口上,也就是说在浏览器输入的 URL 是带端口号的。
更新背后的原因
根据相关内容来看,这次更新主要是出于安全原因。在 nginx 中,形如 $http_* 的变量都代表客户端请求的原始值,因而 $http_host 使用的是来自客户端的原始标头。众所周知来自客户端的东西是不可信任的,任何一个恶意用户都可以在上面动手脚。这需要利用到一个比较少见的特性:在 HTTP/1.1 中,请求对象除了常见的绝对路径(如 /path/to/some.html)之外,也可以一个完整的 URI,包含目标主机在内,而不需要在 Host 中指定。不过,攻击者可以构造这样的请求:
GET https://example.com/ HTTP/1.1
Host: malformedhost
这个请求是有歧义的,因为 Host 标头和实际请求的主机名不一致。按照 RFC 9112 中的规定,在这种情况下反向代理应当忽略来自客户端的 Host 标头,而是根据实际请求对象构造一个新的 Host 标头转发至后端服务器。但 nginx 原先的做法是将该请求路由至 example.com,但保留来自攻击者的 malformedhost 作为 Host 标头,这实则并不符合 HTTP 标准。进一步地,如果后端程序信任该标头,在业务逻辑中使用,例如用其构造各种完整 URL,那么就给攻击者留下了投毒的空间。
不过,这个漏洞的利用条件比较苛刻,因为大部分应用都不会不加检查地直接使用用户输入的内容;更重要的是,即使是 Host 标头直接用于构造绝对路径的 URL,在大部分场景下攻击者构造的恶意请求只会影响他们自己,除非构造的 URL 持久化并应用于其他用户,如缓存投毒,此时攻击者可以利用该漏洞做 CSRF。
因此,这次更新只是为对齐 HTTP 标准的一次调整。但问题在于,$host 并不是最终解,它是没有端口号的,这与 Host 标头的语义并不一致。nginx 也提供了端口号的变量,可以用 $host$is_request_port$request_port 作为最终的替代。只不过,这两个变量是在 nginx 1.29.3 才被引入的,低版本用不了。Debian 上 nginx 的维护者在安全性和兼容性之前选择了前者,在修复安全漏洞的同时造成了 nginx 行为的上述改变。
此外讨论中还提到的一个原因是对 HTTP/3 的兼容,因为 HTTP/3 中并不使用 Host 标头,而是以 :authority 的伪标头的方式传递,此时 $http_host 是一个空值,后端应用不应当依赖于该变量,而是 $host。这里顺便提一下。
应对
有的朋友可能要问了,那把 Nextcloud 的 trusted_domains 配置对应更改一下不就可以了吗?这样确实可以通过验证了,不过这个值同时也是用于构造绝对路径 URL 的,用户随后会被重定向至不带端口的 https://nextcloud.example.com/,于是就无法访问了。
最完善的解决方式当然是升级到 nginx 1.30.0 及之后的版本,但我不想做。
一个方法是忽略这次更新,手动将 HTTP_HOST 重新改为 $http_host。直接覆写fastcgi_params 不合适,而且也会被之后的更新覆盖。因此,最好的方法是在include之后再次指定覆盖。Nextcloud 官方提供的 nginx 配置中,在处理 PHP 文件的部分引入了 fastcgi_params。因此,只需要在 include fastcgi_params; 之后覆盖参数即可,即:
location ~ \.php(?:$|/) {
# Required for legacy support
rewrite ^/(?!index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+|.+\/richdocumentscode(_arm64)?\/proxy) /index.php$request_uri;
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
set $path_info $fastcgi_path_info;
try_files $fastcgi_script_name =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $path_info;
fastcgi_param HTTPS on;
# 在 include 之后再声明:
fastcgi_param HTTP_HOST $http_host;
...
}
当然,这“不安全”,但考虑到我的 Nextcloud 实例不对外公开,除了 Nextcloud 自带的登录之外还部署了额外的访问鉴权机制,并且考虑到上面分析的漏洞利用之苛刻,因此我认为在我的场景下保留 $http_host 没有问题。
此外,也有不牺牲安全性的解决方案,例如将端口硬写在配置中(唯一的缺点是比较丑陋以及不便于维护),即:
set $host_with_port $host:11451;
fastcgi_param HTTP_HOST $host_with_port;
之前提到 nginx 1.29.3 引入了 $request_port 和 $is_request_port 两个变量,但 Debian 的软件源中并没有这个版本的 nginx,因而此处不表。
顺带提醒一下,最近 nginx 有一个影响范围很大的漏洞 CVE-2026-42945,可利用一些特殊的rewrite规则做远程代码执行攻击。如果你的 nginx 配置涉及复杂的rewrite规则,最好关注一下。
参考
- Debian -- News -- Updated Debian 13: 13.5 released(更新日志)
- release nginx 1.22.1-9+deb12u6, upload to bookworm pu (8e8749a5) · Commits · Nginx / nginx · GitLab(本次更新对应的 Git 提交)
- #1126960 - nginx: proxy_params should use \(host instead of \)http_host - Debian Bug report logs(本次更新相关的讨论)
- RFC 9112: HTTP/1.1
- HTTP/3: initialize Host header from :authority to enable $http_host by MarcoMarcoaldi · Pull Request #917 · nginx/nginx
- NGINX configuration — Nextcloud latest Administration Manual latest documentation
- nginx.org/en/CHANGES(nginx 1.29.3 的更新引入
$is_request_port等变量)