2023 年 8 月,Cloudflare、Google、AWS 几乎同时披露了一种新的 HTTP/2 攻击手法:Rapid Reset(CVE-2023-44487)。它不需要僵尸网络,少量机器、少量连接,就能把请求速率推到让 CDN 的防护和计费同时失效的量级。
对多数团队来说,这件事的教训不是“HTTP/2 不安全”,而是“协议层的语义也会被当成武器”。
背景:多路复用是把双刃剑#
HTTP/2 在一条 TCP 或 TLS 连接上跑多条流(stream),每个请求是一条流。为了支持“用户点了取消”“页面跳走了”这类场景,协议提供了 RST_STREAM 帧,客户端可以主动终止一条流。
设计本身没问题:取消意味着服务端可以早点停下。问题出在实现。很多服务端在收到 HEADERS 时就开始分配资源、调用后端,收到 RST_STREAM 后清理,但清理成本远低于创建成本。而且并发流的限制只约束“同时存在”的流,不约束“单位时间创建”的流。
HTTP/1.1 时代没有这个问题。一个请求占一条连接,取消就是断开,服务端能立刻感知。多路复用把“连接”和“请求”解耦之后,取消变成一个帧,成本几乎为零。协议没有错,错在实现里两边的成本不对等。
漏洞原理:取消比创建便宜#
一次循环是:打开流、发请求、立刻 RST_STREAM、再开下一条。服务端看到的是持续不断的合法请求,每条都触发路由、鉴权、日志、上游调用;客户端付出的只是一个帧。
于是一条连接就能产生每秒数百到数千次请求,绕过“最大并发流”的限制。放大倍数取决于服务端在每个请求上做多少事:如果每条请求都要查数据库或渲染页面,放大就很可观。
这也是它和传统 CC 攻击的区别。没有慢连接,没有畸形包,流量在协议层看起来完全正常,基于包特征和连接数的防护很难识别。
公开披露里提到的请求量级,远超当时任何一次 CC 攻击。原因不复杂:单机就能产生足够多的流,不需要控制大量肉鸡。对防守方来说,这意味着“按 IP 封禁”这种老办法从一开始就不成立。
影响范围#
| 组件 | 说明 |
|---|---|
| nginx / Envoy / HAProxy | 反向代理层,需按各自公告升级 |
| Go net/http、Netty、Tomcat | 应用服务器与运行时层面的补丁 |
| CDN / 云 WAF | 服务商侧修复,用户侧仍需配限速 |
| 自建 HTTP/2 入口 | 最容易漏,尤其是老版本 nginx |
具体的修复版本请以各项目安全公告为准。可以借鉴的思路是:nginx 在 1.25.3 中加入了按连接限制请求数的能力,把“单连接能发多少请求”变成可配置项。
内部服务也会中招。gRPC、服务网格的 sidecar、API 网关都跑 HTTP/2,如果它们对上游不设限,一个内部的小服务就能把整条调用链打满。别以为只有公网入口需要关心。
检测:看比值,不看总量#
三个信号最有用。
- 每连接请求数。正常浏览器一条连接几十个请求,攻击时可能是几千。
RST_STREAM密度。抓包统计最直接。- CPU 与 QPS 背离。QPS 很高、响应体很小、上游几乎没被打满,说明工作耗在协议层和中间件上。
# 从抓包看 RST_STREAM 的密度(先采一小段)
sudo tcpdump -i eth0 -w h2.pcap 'tcp port 443' -c 20000
tshark -r h2.pcap -Y 'http2.type == 3' 2>/dev/null | wc -l
# 按来源 IP 统计请求量(字段号按你的 log_format 调整)
awk '{print $1}' /var/log/nginx/access.log \
| sort | uniq -c | sort -rn | head
第一段命令的判读标准不是绝对值,而是比例:RST_STREAM 帧数接近甚至超过 HEADERS 帧数,就很不正常。正常用户几乎不发取消帧。
日志侧还要看一个指标:同一 IP 在一分钟内的连接数与请求数。Rapid Reset 的特征是连接数不多、请求数极高,和“大量短连接”的 CC 攻击正好相反。
如果用 Prometheus,直接看 nginx_http_requests_total 与连接数的比值,或者 Envoy 的 downstream_rq_total 与 downstream_cx_total。比值突然抬高,基本就是有人在刷流。
修复与缓解:升级、限流、后端舱壁#
顺序上,先升级,再限流,最后才考虑降级协议。
- 升级:nginx、Envoy、Go、Netty 等按公告打补丁。这是唯一从根上降低单连接放大倍数的办法。
- 限流:对单 IP、单连接设请求速率上限。nginx 可以用
limit_req配合keepalive_requests。 - 边缘兜底:云 WAF 的 L7 速率规则、Bot 管理;必要时在入口临时关闭 HTTP/2,回退 HTTP/1.1,用性能换稳定。
- 后端自保:给上游加并发舱壁和超时,别让协议层的洪水直接压垮数据库。
责任划分要说清:CDN 侧修的是它自己的实现,源站如果还跑着旧版 nginx,攻击者绕过 CDN 直连源站 IP 一样能打。源站 IP 不要进 DNS,安全组只放行 CDN 回源段。
# 参考片段:限制单连接请求数与单 IP 速率
limit_req_zone $binary_remote_addr zone=perip:10m rate=20r/s;
server {
listen 443 ssl;
http2 on; # 1.25.1+ 的写法,老版本用 listen ... http2
keepalive_requests 200; # 单连接请求上限,1.25.3+ 支持
http2_max_concurrent_streams 64;
limit_req zone=perip burst=40 nodelay;
}
http2 on; 是新语法,老版本要写 listen 443 ssl http2;,两种写法混用会直接报错。改完 nginx -t,先在一台边缘节点灰度,观察 keep-alive 复用率是否被削弱。
keepalive_requests 调小会让长连接更早重建,TLS 握手次数上升。静态资源站点要权衡,动态 API 通常无所谓。
验证#
- 压测:用 h2load 做一次正常并发压测,确认吞吐没有明显下降。重点看 P99 与握手次数,而不是只看 QPS。
- 观察 keep-alive 命中率。若 TLS 握手数暴涨,说明连接被过早关闭,上限设得太小。
- 告警复盘:单 IP QPS 突增、
RST_STREAM比例、5xx 与超时率,三个都要有阈值。 - 与 CDN 侧确认其修复状态。源站没事不代表整体没事,边缘被打穿时源站可能连告警都收不到。
结论#
- Rapid Reset 利用的是协议语义,不是某一行代码的 bug;补丁修的是放大倍数。
- 并发流限制挡不住它,需要按连接、按 IP 的请求速率限制。
- 最实用的检测信号是“每连接请求数”和
RST_STREAM密度。 - 入口临时降级到 HTTP/1.1 是可接受的应急手段,但要先评估性能损失。
- 升级、限流、后端舱壁三件事要一起做,缺一件都还有缺口。
完