Express req.subdomains 把点分 IPv6 拆成了子域名
hostname保留IPv6方括号,net.isIP因此未识别地址,点分尾段被当域名。只在应用读取边界规范化识别,保留offset与可信代理规则。
请求的 Host 是 [::ffff:127.0.0.1],Express 的 req.subdomains 却返回了看似子域名的 "0" 和 "[::ffff:127"。这不是一个实际域名,而是包含 IPv4 点分段的 IPv6 字面地址,被按点拆开了。
Express 5.3.0 的 hostname 保留 IPv6 外层方括号,subdomains 随后把这个值直接交给 net.isIP()。后者识别的是不带 URL 方括号的 IP 文本;带括号时返回 0,于是代码落入域名的 split 分支。只检查 [::1] 很容易漏掉问题,因为它没有点,切分结果恰好看不出来。
先看 Express 实际采用的 hostname
对同一条受控请求比较 hostname、subdomains 和 subdomain offset,不要只看地址栏。默认 offset 为 2,点分字符串反转后再截取,所以结果甚至不像原地址。
Fetch 或某些 URL 实现会将 IPv4-mapped IPv6 规范化为十六进制形式,例如把点分尾段改成 7f00:1;这样的请求不一定重现同一现象。应保留真正到达应用的 Host 形态,以及代理有没有重写它。
启用 trust proxy 后,hostname 还可能来自受信任入口的 X-Forwarded-Host。不要为了复现或修显示把 trust proxy 改成无条件 true;来自不可信直连请求的头,不能因此变成租户或权限依据。
只在自己的读取边界识别 IP,不改原请求头
若应用当前需要消除这次错误切分,可以在自己的子域读取函数中,仅去掉成对外层方括号后识别 IP。识别成功时保留 Express 对 offset 的截取语义;普通域名继续交给原框架处理:
const { isIP } = require('node:net');
function applicationSubdomains(req) {
const hostname = req.hostname;
if (!hostname) return [];
const address = hostname.startsWith('[') && hostname.endsWith(']')
? hostname.slice(1, -1)
: hostname;
if (isIP(address)) {
return [hostname].slice(req.app.get('subdomain offset'));
}
return req.subdomains;
}
这个函数用于已有 Express 请求处理代码,调用处应改读它的返回值。它没有替换 Express 的 getter,也没有重写 Host 或 X-Forwarded-Host。默认 offset 下 IP 得到空数组;offset 为 0 时保留原 hostname 作为单项,不能一律对 IP 返回空数组后悄悄改变既有行为。
函数也不是完整的主机名合法性验证器。业务若按域名选择租户,仍须验证它属于允许的域名范围,并按既有认证与租户映射处理;不能把任何能解析的 Host 都视作授权入口。域名还涉及注册后缀等规则,不能把 subdomain offset 当成通用公共后缀解析器。
用带点与不带点的地址分别验收
保留 [::1]、[::ffff:127.0.0.1]、普通 IPv4 和正常域名,分别核对默认 offset 与业务实际采用的其他值。代理部署还要分别检查可信代理转发和不可信直连,确认没有为了修字符串拆分扩大头字段的信任范围。
URL 中 IPv6 的方括号有结构用途,不能把发送请求的完整 URL 随手删括号。这里仅在 IP 识别参数上取内部地址;它不同于查询参数方括号触发 curl globbing,也不需要把 IPv6 地址括号编码成查询参数形式。
截至 2026 年 10 月 24 日,正式 Express 5.3.0 仍有这条带括号的识别路径。采用后续依赖修正时,应继续核对实际 Host、offset 和代理条件,不能仅凭一次规范化后的浏览器请求没有错误,就认定点分 IPv6 已覆盖。


