Nginx CVE‑2026‑42945:隐藏18年高危漏洞被曝光(附解决方案)

自查

  • 影响范围:NGINX Open Source 0.6.27 ~ 1.30.0,官方修复边界是 1.30.1+1.31.0+
  • 先自查:nginx -v,再扫配置里有没有 rewrite + $1/$2 + ?,后面还接 rewrite/if/set
  • 要不要升级:只要版本低于修复边界,或者命中上面的危险配置,先升级。
  • 有没有中招:生产上如果已经出现 worker 异常退出、core dumped、无故重启,先按受影响处理;不确定就去隔离环境验证。
  • 脚本覆盖:Debian/Ubuntu、RHEL/CentOS/Rocky/Alma/Oracle、Amazon Linux、SLES、Alpine;Fedora、Arch、openSUSE 这类不在 nginx.org 官方二进制源支持列表里的系统,脚本会停下来让你手动确认。

自查脚本

bash 复制代码
nginx -v
sudo nginx -T > /tmp/nginx-all.conf 2>/tmp/nginx-test.log
rg -n 'rewrite\s+.*\$\d+.*\?' /tmp/nginx-all.conf

修复步骤

  1. 先备份 /etc/nginx
  2. 根据系统类型安装基础依赖。
  3. 添加 nginx 官方 mainline 源和签名 key。
  4. 用当前系统的包管理器安装 nginx。
  5. nginx -vnginx -t 通过后再重启。

一键修复脚本

bash 复制代码
#!/bin/sh
set -eu

NGINX_KEY_URL="https://nginx.org/keys/nginx_signing.key"
NGINX_APK_KEY_URL="https://nginx.org/keys/nginx_signing.rsa.pub"

if [ "$(id -u)" -ne 0 ]; then
  echo "请用 root 运行:sudo sh $0"
  exit 1
fi

if [ ! -r /etc/os-release ]; then
  echo "无法读取 /etc/os-release,先手动确认发行版"
  exit 1
fi

. /etc/os-release
BACKUP_DIR="/root/nginx-backup-$(date +%F-%H%M%S)"
mkdir -p "$BACKUP_DIR"
if [ -d /etc/nginx ]; then
  cp -a /etc/nginx "$BACKUP_DIR/"
fi

restart_nginx() {
  if command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ]; then
    systemctl restart nginx
    return
  fi
  if command -v rc-service >/dev/null 2>&1; then
    rc-service nginx restart || rc-service nginx start
    return
  fi
  if command -v service >/dev/null 2>&1; then
    service nginx restart || service nginx start
    return
  fi
  nginx -s reload 2>/dev/null || nginx
}

version_ge() {
  awk -v v="$1" -v min="$2" '
    BEGIN {
      split(v, a, "."); split(min, b, ".");
      for (i = 1; i <= 3; i++) {
        a[i] += 0; b[i] += 0;
        if (a[i] > b[i]) exit 0;
        if (a[i] < b[i]) exit 1;
      }
      exit 0;
    }'
}

verify_nginx() {
  nginx -t
  restart_nginx
  nginx -v
  INSTALLED="$(nginx -v 2>&1 | sed -n 's#.*nginx/##p' | sed 's/[^0-9.].*$//')"
  if version_ge "$INSTALLED" "1.30.1"; then
    echo "版本检查通过:nginx/$INSTALLED"
  else
    echo "版本仍低于 1.30.1,请检查包来源或发行版 backport 状态:nginx/$INSTALLED"
    exit 1
  fi
}

setup_apt() {
  OS="$1"
  KEYRING_PACKAGE="$2"
  CODENAME="${3:-${VERSION_CODENAME:-}}"
  if [ -z "$CODENAME" ] && command -v lsb_release >/dev/null 2>&1; then
    CODENAME="$(lsb_release -cs)"
  fi
  if [ -z "$CODENAME" ]; then
    echo "无法识别发行版代号"
    exit 1
  fi

  apt-get update
  apt-get install -y curl gnupg ca-certificates lsb-release "$KEYRING_PACKAGE"
  install -d -m 0755 /usr/share/keyrings
  curl -fsSL "$NGINX_KEY_URL" | gpg --dearmor >/usr/share/keyrings/nginx-archive-keyring.gpg
  chmod 0644 /usr/share/keyrings/nginx-archive-keyring.gpg

  cat >/etc/apt/sources.list.d/nginx.list <<EOF
deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] https://nginx.org/packages/mainline/${OS} ${CODENAME} nginx
EOF

  cat >/etc/apt/preferences.d/99nginx <<'EOF'
Package: *
Pin: origin nginx.org
Pin: release o=nginx
Pin-Priority: 900
EOF

  apt-get update
  apt-cache policy nginx
  apt-get install -y nginx
}

setup_rhel_family() {
  PM="yum"
  command -v dnf >/dev/null 2>&1 && PM="dnf"
  "$PM" install -y yum-utils ca-certificates curl

  cat >/etc/yum.repos.d/nginx.repo <<'EOF'
[nginx-stable]
name=nginx stable repo
baseurl=https://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=1
enabled=0
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true

[nginx-mainline]
name=nginx mainline repo
baseurl=https://nginx.org/packages/mainline/centos/$releasever/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
EOF

  "$PM" module disable -y nginx >/dev/null 2>&1 || true
  "$PM" install -y nginx
}

setup_amazon() {
  PM="yum"
  command -v dnf >/dev/null 2>&1 && PM="dnf"
  "$PM" install -y yum-utils ca-certificates curl

  if [ "${VERSION_ID:-}" = "2023" ]; then
    MAINLINE_URL='https://nginx.org/packages/mainline/amzn/2023/$basearch/'
    STABLE_URL='https://nginx.org/packages/amzn/2023/$basearch/'
  else
    MAINLINE_URL='https://nginx.org/packages/mainline/amzn2/$releasever/$basearch/'
    STABLE_URL='https://nginx.org/packages/amzn2/$releasever/$basearch/'
  fi

  cat >/etc/yum.repos.d/nginx.repo <<EOF
[nginx-stable]
name=nginx stable repo
baseurl=${STABLE_URL}
gpgcheck=1
enabled=0
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
priority=9

[nginx-mainline]
name=nginx mainline repo
baseurl=${MAINLINE_URL}
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
priority=9
EOF

  "$PM" install -y nginx
}

setup_sles() {
  zypper --non-interactive install curl ca-certificates gpg2
  curl -fsSL -o /tmp/nginx_signing.key "$NGINX_KEY_URL"
  rpmkeys --import /tmp/nginx_signing.key
  zypper --non-interactive removerepo nginx-mainline >/dev/null 2>&1 || true
  zypper --non-interactive addrepo --gpgcheck --type yum --refresh --check \
    'https://nginx.org/packages/mainline/sles/$releasever_major' nginx-mainline
  zypper --non-interactive refresh
  zypper --non-interactive install nginx
}

setup_alpine() {
  apk add --no-cache openssl curl ca-certificates
  ALPINE_VER="$(grep -o '^[0-9]\+\.[0-9]\+' /etc/alpine-release)"
  REPO="@nginx https://nginx.org/packages/mainline/alpine/v${ALPINE_VER}/main"
  grep -qxF "$REPO" /etc/apk/repositories || echo "$REPO" >>/etc/apk/repositories
  curl -fsSL -o /etc/apk/keys/nginx_signing.rsa.pub "$NGINX_APK_KEY_URL"
  apk update
  apk add nginx@nginx
}

case "${ID:-}" in
  debian)
    setup_apt "debian" "debian-archive-keyring" "${1:-}"
    ;;
  ubuntu)
    setup_apt "ubuntu" "ubuntu-keyring" "${1:-}"
    ;;
  rhel|centos|rocky|almalinux|ol|oracle)
    setup_rhel_family
    ;;
  amzn)
    setup_amazon
    ;;
  sles|sled|sles_sap)
    setup_sles
    ;;
  alpine)
    setup_alpine
    ;;
  fedora|arch|manjaro|opensuse-leap|opensuse-tumbleweed|opensuse)
    echo "当前系统 ${ID:-unknown} 不在 nginx.org 官方二进制源支持列表里。"
    echo "请用系统仓库升级后手动确认 nginx -v 是否 >= 1.30.1,或改用官方 Docker 镜像。"
    exit 1
    ;;
  *)
    echo "暂未识别系统 ID=${ID:-unknown},请按 nginx 官方文档手动配置 mainline 源。"
    exit 1
    ;;
esac

verify_nginx

if command -v apt-cache >/dev/null 2>&1; then
  apt-cache policy nginx || true
elif command -v rpm >/dev/null 2>&1; then
  rpm -qi nginx || true
elif command -v apk >/dev/null 2>&1; then
  apk info -v nginx || true
fi

echo "完成,备份目录:$BACKUP_DIR"

Nginx 漏洞编号是 CVE-2026-42945,也有人叫它 NGINX Rift

它的问题不在控制台,也不在某个后台接口,而是在请求处理路径里的 ngx_http_rewrite_module。公网请求只要能打到命中的 rewrite 配置,就有机会触发 worker 进程里的堆缓冲区溢出。NVD 里 F5 CNA 给的是 CVSS v4.0 9.2,v3.1 是 8.1;nginx.org 的安全公告页把它列成 medium,但受影响版本范围写得很直接:0.6.271.30.0,修复边界是 1.30.1+1.31.0+

这篇就不写成新闻稿了。我把我真正关心的几个点捋一下:到底什么配置会中、怎么在自己的环境里复现风险、Debian 上为什么看版本号容易误判、最后我是怎么升级到 nginx 官方 mainline 1.31.x 的。

触发点不是所有 rewrite,而是这个组合

这次漏洞卡在一个很具体的配置模式上。

按 NVD 的描述,满足下面几件事才危险:

  • 使用 ngx_http_rewrite_module 里的 rewrite 指令。
  • 正则里用了未命名 PCRE 捕获,比如 $1$2
  • 替换字符串里带了 ?
  • 这个 rewrite 后面又跟着 rewriteifset 指令。

例子大概长这样:

nginx 复制代码
server {
    listen 8080;

    location / {
        rewrite ^/users/([0-9]+)/profile/(.*)$ /profile.php?id=$1&tab=$2 last;
        set $rewrite_marker 1;

        return 200 "ok\n";
    }
}

这里最刺眼的是 $1$2 和替换目标里的 ?。正常看它只是把路径改写到 PHP 参数里,这类配置在老项目、网关、CMS 迁移规则里并不少见。

问题就在这:Nginx 脚本引擎会先算目标 buffer 需要多长,再把内容 copy 进去。DepthFirst 的分析里提到,当替换字符串里出现 ? 时,主执行引擎会进入 args escape 相关状态;但长度计算那一轮用的是一个新的子引擎,它看到的状态不一致。于是长度按原始 capture 算,真正 copy 时又按 query args 规则转义,某些字符会膨胀,最后写出分配好的 heap buffer。

也就是说,这不是"正则写得丑"那么简单。

它是长度计算和实际写入的 escaping 假设不一致。

我会先扫配置,而不是先跑 PoC

公开 PoC 已经有了,甚至有 RCE 复现仓库。这里我不贴直接拿去打 shell 的命令,没必要。对大多数维护自己服务器的人来说,第一步应该是确认配置是否命中触发模式,而不是拿 payload 去怼生产。

先看版本:

bash 复制代码
nginx -v
nginx -V 2>&1 | head

如果是 Open Source 1.30.1+1.31.0+,这条 CVE 已经在官方修复边界内。否则继续看配置。

先粗扫所有 rewrite

bash 复制代码
sudo nginx -T > /tmp/nginx-all.conf 2>/tmp/nginx-test.log
rg -n 'rewrite\s+.*\$\d+.*\?' /tmp/nginx-all.conf

这个命令不是完美检测器,但很适合第一轮排查。它找的是 rewrite 里同时出现未命名捕获引用和问号的地方。

然后再看这些命中的 rewrite 后面,同一层级里有没有继续跟 rewriteifset。这个判断靠纯正则不太稳,最好打开配置人工看一遍。尤其是很多人会把公共片段拆到 /etc/nginx/snippets//etc/nginx/conf.d/nginx -T 展开后看会更准。

我当时重点看这几类地方:

  • 老站点的 URL 迁移规则。
  • 把路径改写成 query string 的规则。
  • 网关层为了兼容旧接口写的 $1$2
  • WordPress、PHP、历史项目留下来的 rewrite 片段。
  • Ingress 或面板产品生成的 nginx 配置。

这一步我当时也差点在漏看一次,因为 rewrite 本身太常见了,很多配置看起来只是"正常历史包袱"。

复现步骤:只做隔离环境里的风险确认

如果只是确认自己是否受影响,我建议复现拆成两层。

第一层是配置命中复现:证明你的配置存在危险模式。第二层才是漏洞触发复现:在隔离容器里用公开 PoC 验证旧版本会崩 worker,升级后不再触发。

不要在生产机器上直接跑 PoC。

1. 准备一个旧版本测试环境

用容器最省事,目标是准备一个 1.30.0 或更低版本的 Nginx。示例配置放到测试目录里,不要拿线上配置原封不动去跑。

bash 复制代码
mkdir -p /tmp/nginx-rift-lab/conf.d

写一个只用于实验的配置,保存到 /tmp/nginx-rift-lab/conf.d/default.conf

nginx 复制代码
server {
    listen 8080;
    server_name _;

    access_log /var/log/nginx/access.log;
    error_log  /var/log/nginx/error.log notice;

    location / {
        rewrite ^/u/([0-9]+)/(.+)$ /profile.php?id=$1&tab=$2 last;
        set $rewrite_marker 1;

        return 200 "lab\n";
    }
}

启动旧版本:

bash 复制代码
docker run --rm --name nginx-rift-lab \
  -p 8080:8080 \
  -v /tmp/nginx-rift-lab/conf.d:/etc/nginx/conf.d:ro \
  nginx:1.30.0

再开一个终端确认版本:

bash 复制代码
docker exec nginx-rift-lab nginx -v
docker exec nginx-rift-lab nginx -T | sed -n '/server_name _/,/}/p'

到这里,只能说明配置形态命中了触发条件,还不能说明你已经完成了漏洞利用。

2. 触发验证只看 worker 是否异常重启

公开 PoC 仓库里已经有完整 exploit。我的建议是,只在隔离网络里做 DoS 级别的验证,观察 error log 里是否出现 worker 异常退出、core dumped 或进程重启,不要追求 RCE shell。

日志窗口先开着:

bash 复制代码
docker logs -f nginx-rift-lab

然后对测试实例发送能命中 rewrite 的请求。真正的触发 payload 需要包含会在 args escape 下膨胀的 URI 字节,公开 PoC 已经覆盖这部分;文章里不贴可直接武器化的请求。

你只需要记录两个结果:

  • 旧版本加危险配置:worker 可能异常退出或重启。
  • 升级到 1.30.1+1.31.0+ 后:同样测试不再触发这个崩溃。

安全验证做到这里就够了。

临时缓解:把未命名捕获换成命名捕获

如果暂时没法马上升级,先把危险 rewrite 改掉。

危险写法:

nginx 复制代码
rewrite ^/users/([0-9]+)/profile/(.*)$ /profile.php?id=$1&tab=$2 last;
set $rewrite_marker 1;

缓解写法:

nginx 复制代码
rewrite ^/users/(?<user_id>[0-9]+)/profile/(?<section>.*)$ /profile.php?id=$user_id&tab=$section last;
set $rewrite_marker 1;

这里的重点不是"命名捕获更优雅",而是绕开 $1$2 这种未命名 capture 的触发条件。

改完一定要测试配置:

bash 复制代码
sudo nginx -t
sudo systemctl reload nginx

但这个只能算临时处理。只要二进制还在受影响版本里,我还是建议升级。

Debian 上最容易误判的是版本号

我这次修的时候也碰到了这个坑。

Debian 仓库里的 nginx 版本可能还是 1.26.3,看起来不像最新。这里要分清两件事:

  • Debian 的稳定仓库经常是旧上游版本加安全 backport,不一定把版本号直接抬到最新。
  • nginx 官方这次的明确修复边界是 Open Source 1.30.1+1.31.0+

截至我写这篇时,Debian Security Tracker 里 trixie1.26.3-3+deb13u2 仍列为 vulnerable,sid1.30.0-3 已标 fixed。这个状态后面可能变,因为 Debian 可以把补丁 backport 到旧版本号里。

所以判断时别只盯着 nginx -v 的主版本号。要么看 Debian Security Tracker 里对应包状态,要么直接切到 nginx 官方源,用官方已经包含修复的 mainline 包。

我最后选的是第二种:升级到 nginx 官方 mainline 1.31.x

手动修复流程:以 Debian 切到 nginx 官方 mainline 1.31.x 为例

上面的脚本已经覆盖主流 Linux 发行版。下面这套是 Debian trixie 的手动写法,方便你看清每一步到底动了什么;不是 trixie 的机器,把源里的 trixie 换成你的发行版代号,或者用 lsb_release -cs 动态取。

升级前先备份配置:

bash 复制代码
sudo cp -a /etc/nginx /etc/nginx.bak.$(date +%F-%H%M)
sudo nginx -t

1. 安装 gpg 和基础依赖

bash 复制代码
sudo apt update
sudo apt install curl gnupg ca-certificates lsb-release debian-archive-keyring -y

2. 下载 nginx 官方签名 key

bash 复制代码
curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \
| sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null

建议顺手核一下 fingerprint。nginx 官方文档给的是 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62

bash 复制代码
gpg --dry-run --quiet --no-keyring --import --import-options import-show \
  /usr/share/keyrings/nginx-archive-keyring.gpg

3. 添加 nginx 官方 mainline 源

我这里是 Debian 13 trixie

bash 复制代码
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
http://nginx.org/packages/mainline/debian/ trixie nginx" \
| sudo tee /etc/apt/sources.list.d/nginx.list

也可以按官方文档用 HTTPS 和系统代号:

bash 复制代码
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
https://nginx.org/packages/mainline/debian $(lsb_release -cs) nginx" \
| sudo tee /etc/apt/sources.list.d/nginx.list

4. 让 apt 优先使用 nginx.org 的包

这一步官方文档也有。加 pinning 之后,apt install nginx 会更明确地选 nginx.org 的包,而不是继续被发行版仓库抢走。

bash 复制代码
echo -e "Package: *\nPin: origin nginx.org\nPin: release o=nginx\nPin-Priority: 900\n" \
| sudo tee /etc/apt/preferences.d/99nginx

5. 更新 apt 并安装 nginx

bash 复制代码
sudo apt update
apt-cache policy nginx
sudo apt install nginx

如果之前装的是 Debian 自带拆分包,比如 nginx-commonnginx-core,安装过程中可能会提示配置文件保留或覆盖。我的习惯是先保留现有配置,升级后再手动 diff。

6. 检查版本和配置

bash 复制代码
nginx -v
sudo nginx -t
sudo systemctl restart nginx
sudo systemctl status nginx --no-pager

修完之后,nginx -v 应该能看到 1.31.x。我这次最后看到的修复结果是这样:

再确认包来源:

bash 复制代码
apt-cache policy nginx

如果 candidate 或 installed 来源是 nginx.org/packages/mainline/debian,说明已经切到官方 mainline 源。

修完之后还要回头做两件事

第一,重新扫一遍危险 rewrite。

bash 复制代码
sudo nginx -T > /tmp/nginx-all-after.conf 2>/tmp/nginx-test-after.log
rg -n 'rewrite\s+.*\$\d+.*\?' /tmp/nginx-all-after.conf

升级能修漏洞,但不代表这些历史 rewrite 就值得继续留着。能改成命名捕获就改掉,能删就删。

第二,确认所有 worker 都吃到了新二进制。

bash 复制代码
ps -eo pid,ppid,cmd | rg 'nginx: (master|worker)'
sudo systemctl restart nginx
nginx -v

只 reload 有时会让你误以为修完了,但老 worker 生命周期没处理干净。安全补丁升级后,我更倾向直接 restart。

最后

这次 CVE-2026-42945 给我的感觉是,Nginx 这种老牌基础设施也不是"稳定到不用看"的东西。

它真正危险的地方不只是"18 年老洞",而是触发点藏在非常日常的 rewrite 配置里。你可能不是特意用了什么高级功能,只是多年前为了兼容旧 URL 写了几行 $1$2,然后这个入口一直挂在公网。

我的处理建议很简单:

  • 先用 nginx -T 扫配置,找 $1/$2? 的 rewrite。
  • 有危险配置就先改成命名捕获。
  • Debian 用户不要只看 1.26.3 这个版本号,要看安全跟踪状态或包来源。
  • 能升级就直接上 nginx 官方修复版本,稳定线至少 1.30.1+,主线就是 1.31.x

这类洞不要等面板、发行版、云厂商全部替你兜底。边缘入口跑在自己机器上,最后还是要自己确认版本、配置和实际进程状态。

参考:

相关推荐
Csvn1 小时前
Vue 性能优化实战指南
前端·vue.js
BestHeaker1 小时前
CC Switch 全能使用教程
后端·职场和发展·跳槽·学习方法
折哥的程序人生 · 物流技术专研1 小时前
Java面试85题图解版 · 全系列总目录
java·开发语言·后端·面试·职场和发展
海棠Flower未眠1 小时前
Spring Boot 3 + JPA多模块系统对MySQL和DORIS进行多数据源集成实战(荣耀典藏版)
spring boot·后端·mysql
UXbot1 小时前
AI原型设计工具如何从PRD自动生成交互原型
前端·低代码·ui·交互·ai编程·原型模式
武子康1 小时前
Java-01 深入浅出 MyBatis 入门与核心原理:半自动 ORM 框架详解
java·后端·mybatis
Csvn1 小时前
Vue 最佳实践
前端·vue.js
木易 士心1 小时前
Java 跳出多层循环
java·开发语言·后端
神奇小汤圆1 小时前
背了那么久的慢 SQL 八股,不如动手跑一遍 EXPLAIN
后端