Nginx UI 六个 CVE 同一根因:AuthRequired 只验 JWT,不验会话从哪来
Nginx UI v2.5.0(2026-10-09)修掉六个高危 CVE,全部影响 2.0.0–2.4.x。这六个洞表面上看分散在登录、CSRF、备份、集群、升级几个模块,根因是一条:AuthRequired 只检查 JWT 与 OTP,不调用 RequireSecureSession——而只有后者才验证「这个会话是在有第二因子的前提下建立的」。
| CVE | 缺陷 | 类型 |
|---|---|---|
| CVE-2026-107808 | 只配 passkey 的账号,仅凭密码就发会话 | 认证绕过 |
| CVE-2026-107809 | 带 JWT 的 cookie 被 AuthRequired 接受,不校验 CSRF token | CSRF |
| CVE-2026-107810 | 备份恢复时解压出的符号链接指向线上配置 | 配置注入 |
| CVE-2026-107811 | /api/nodes 发的 node token 被 AuthRequired 接受 | 跨节点认证绕过 |
| CVE-2026-107813 | cluster 相关端点走 AuthRequired 但没有 RequireSecureSession | 陈旧 JWT 提权 |
| CVE-2026-107812 | 升级包 digest 只对同源镜像校验 | 镜像被篡改 → RCE |
根因
AuthRequired 是管理端点的通用门禁,但它只做「JWT 有效 + OTP 通过」的判断。会话是从哪条路径签发的、签发时有没有第二因子,它不问。于是四类问题同时成立:
- 加入 passkey 之前偷到的 JWT 仍长期有效(CVE-2026-107813);
- passkey-only 账号仅凭密码就能拿到会话(CVE-2026-107808);
- JWT cookie 跨源可用且无 CSRF 保护(CVE-2026-107809);
/api/nodes的 node token 通过AuthRequired被映射成initUser(CVE-2026-107811)。
CVE-2026-107808:passkey-only 账号少了第二因子
nginx-ui 支持 TOTP 与 WebAuthn passkey 两种第二因子,任一配置即报告 2FA 已启用。但 POST /api/login 只在存在 TOTP secret 时强制第二因子。
model/user.go:
func (u *User) EnabledOTP() bool { return len(u.OTPSecret) != 0 }
func (u *User) EnabledPasskey() bool { /* true if a passkey row exists */ }
func (u *User) Enabled2FA() bool { return u.EnabledOTP() || u.EnabledPasskey() }
api/user/auth.go 的 Login 里只写了 if u.EnabledOTP() { ... 要求 OTP ... },passkey-only 账号直接落到 GenerateJWT。RequireSecureSession() 同样只用 EnabledOTP() 判断,所以 passkey-only 用户在敏感操作上也被免掉 step-up。
PoC 三行:
GET /api/2fa_status
→ {"enabled":true,"otp_status":false,"passkey_status":true}
POST /api/login # 只提交密码
→ HTTP 200 {"message":"ok","code":200,"token":"eyJ..."}
# 有 TOTP 的账号同样请求返回 code 199(需要第二因子)
影响面比「登录绕过」大:nginx-ui 里所有已认证用户实际都是管理员,terminal 功能能给宿主机 shell,账号接管等于控制受管 nginx 与主机。
其余几条的攻击面
CVE-2026-107809(CSRF):带 JWT 的 cookie 被 AuthRequired 直接接受,不校验 CSRF token,且 cookie 跨源可用。攻击者页面诱导已登录的管理员浏览器发请求,即可借其会话执行管理操作。
CVE-2026-107810(配置注入):备份恢复时,解压出的符号链接可以指向线上配置目录。恢复一个构造过的备份,写入目标就落到解压目录之外,等于改配置。
CVE-2026-107811(跨节点认证绕过):/api/nodes 签发的 node token 被 AuthRequired 接受并映射成 initUser,拿到 node token 的一方可以直接以该身份调用管理端点。
CVE-2026-107813(陈旧 JWT 提权):cluster 相关端点走 AuthRequired 但没有 RequireSecureSession,在启用第二因子之前签发的旧 JWT 依然能进入这些端点。
CVE-2026-107812(镜像篡改 → RCE):升级包 digest 只对同源镜像校验,非同源镜像提供的包不受 digest 约束,篡改后的包会被装上去。
同一条线上更早的两个洞
CVE-2026-84314(CVSS 8.1):POST /api/users/:id 在修改他人密码时没有 RequireSecureSession,而自助改密路径有。任何已认证的低权限账号可改掉初始管理员的密码,使既有管理员 token 失效,再用新密码登录。修复在 2.4.1(commit 23d1934,把 RequireSecureSession 加到用户管理、备份、证书、DNS 凭据路由)。
CVE-2026-33032:影响 2.3.5 及以下。MCP 集成暴露 /mcp 与 /mcp_message,/mcp 有 IP 白名单 + AuthRequired,/mcp_message 只有 IP 白名单——而默认白名单为空被当成「允许全部」。任何网络攻击者都能调用全部 MCP 工具(重启 nginx、增删改配置、触发 reload),CVSS 9.8。
2.5.0 的修复
AuthRequired在管理端点上调用RequireSecureSession;- 备份恢复校验符号链接目标在解压目录内;
- 升级 digest 用签名 manifest,而非仅同源;
- node token 限定到来源节点。
升级与加固清单
- 升级到 2.5.0+;只跑过 2.4.x 且未装 2.4.1 的,先到 2.4.1 再往上。
docker inspect --format '{{index .Config.Labels "org.opencontainers.image.version"}}'
- 轮换密码与 API token,假设旧 token 已泄露:管理员密码、自助改密后的密码、
/api/nodes相关 node token 全部重发。 - 使所有活跃会话失效——旧 JWT 在修复前的窗口内可能已被取走,
RequireSecureSession不会追溯作废它们。 - 审计 users 表与管理员操作:看有没有非预期的密码变更、新增节点、备份恢复记录。
- 把管理界面限制在可信网络或 VPN 后,不要把 8080/443 直接暴露在公网。
- 在反向代理上对
/api/users/限流,并对 POST 告警:
limit_req_zone $binary_remote_addr zone=nginxui_users:10m rate=5r/m;
location /api/users/ {
limit_req zone=nginxui_users burst=3 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:9000;
}
# 记录 POST 以便告警
log_format nginxui_post '$remote_addr "$request" $status';
access_log /var/log/nginx/nginxui_post.log nginxui_post;
如果暂时无法升级,最短路径是把管理端口的访问控制收到反代层,并对 /api/users/、/api/nodes、cluster 端点做同样的来源限制——它们都是这六个洞的落点。