HTTP/3 基于 QUIC 协议,引入 0-RTT 机制,允许客户端在完成首次握手后缓存会话票据,后续连接时直接发送加密的应用数据,省去一个往返。对于内容分发网络或反向代理缓存来说,这可以把首字节时间压到接近理论下限,但代价是服务器无法通过正常握手来确认请求的新鲜度。重放攻击者可以录制早期数据,在多个连接中重复发送,如果代理缓存没有识别机制,就可能把重复的写操作转发给源站,或者让缓存条目被错误更新。

Apache HTTP Server 通过 mod_http3 模块提供 HTTP/3 服务,配合 mod_proxy 和缓存模块可以构建支持 QUIC 的反向代理缓存。本文不讨论某个特定版本的实验性实现,而是基于代理层可操作的机制,说明如何识别 0-RTT 流量、限制其动作,并让缓存与后端保持一致。
理解 0-RTT 重放风险为什么对代理缓存更棘手
0-RTT 数据是由客户端在握手完成前发出的,通常包含 HTTP 请求。服务器无法区分这是原始请求还是被重放的请求,因为 QUIC 的加密上下文基于会话票据,攻击者一旦拿到票据就可以在票据有效期内重放早期数据。对于直接面向浏览器的源站,重放的主要风险是非幂等操作,比如 POST 表单、支付确认或数据删除。代理缓存处在客户端与源站之间,还额外带来两个问题:缓存污染和跨用户泄漏。
缓存污染是指一个被重放的 0-RTT 请求携带着攻击者控制的 URL 和参数,代理缓存将其视为正常 GET 请求并缓存响应,之后正常用户访问同一 URL 时拿到被篡改过的内容。由于 0-RTT 请求没有经过完整的服务器握手协商,某些针对连接级别的防护策略无法生效,这让攻击者有机会诱导缓存写入恶意数据。另外,如果代理缓存的键只包含 URL 而忽略了 early-data 状态,那么同一个缓存条目可能被早期数据与正常数据共用,进一步放大风险。
还需要注意,HTTP/3 的 0-RTT 只适用于安全、可重放的请求。规范中建议客户端只对幂等请求使用 0-RTT,但这一约束无法强制实施,因为客户端脚本或恶意工具可以构造任意请求。因此服务器和代理必须独立执行防护,不能依赖客户端的自觉。
在 Apache 代理层识别并限制 0-RTT 请求
Apache 作为反向代理,可以通过请求头来识别来自 0-RTT 的请求。在支持 TLS 1.3 早期数据的场景中,代理可以在收到包含 Early-Data 头或由 QUIC 层标记的请求时进行拦截。典型做法是仅允许 GET、HEAD、OPTIONS 等安全方法通过,对于其他方法直接返回 403 或 425 Too Early,避免任何可能产生副作用的操作被重放。
以下配置片段演示了使用 mod_rewrite 检查请求头 Early-Data 的逻辑。该头由前端 TLS 终止层或 mod_http3 注入,取值为 1 时表示当前请求来自 0-RTT 数据。
<VirtualHost *:443>
ServerName proxy.ippipp.com
RewriteEngine On
# 仅允许幂等方法携带早期数据
RewriteCond %{HTTP:Early-Data} =1
RewriteCond %{REQUEST_METHOD} !^(GET|HEAD|OPTIONS)$
RewriteRule .* - [F,L]
</VirtualHost>
上面的 RewriteCond 第一条匹配 Early-Data 头等于 1,第二条排除安全方法。对不满足条件的请求,RewriteRule 使用 F 标志返回 403 Forbidden。如果想要更友好的响应,可以使用 R=425 返回 425 Too Early 状态码,提示客户端稍后重试。需要注意的是,Apache 配置中的请求头名称会转换为大写并可能把短横线转换为下划线,因此 HTTP:Early-Data 的写法在多数版本中有效。
如果代理前面还有负载均衡或 WAF 解密 TLS,需要确保 Early-Data 头由真正处理 QUIC 连接的组件注入,而不是信任客户端伪造。攻击者可以在正常 HTTPS 请求中自行添加 Early-Data: 1 头,如果没有层层剥离,这个头会到达后端,造成误判。Apache 可以使用 RequestHeader unset Early-Data 在转发到源站前移除该头,避免源站被伪造信息干扰。
缓存键隔离与后端幂等性保障
识别 0-RTT 只是第一步,缓存系统还必须避免早期数据与普通数据共享缓存条目。Apache 的缓存键可以由 mod_cache 或 mod_proxy 的 CacheKey 指令控制。建议在缓存键中加入请求是否来自 0-RTT 的标记,例如使用请求头 Early-Data 的值作为缓存键的一部分。这样,即使同一 URL 的早期 GET 请求和正常 GET 请求返回不同内容,也不会互相覆盖。
CacheQuickHandler off
CacheLock on
CacheKey "%{REQUEST_SCHEME}://%{HTTP_HOST}%{REQUEST_URI}?early=%{HTTP:Early-Data}"
上述配置将 Early-Data 头值拼入缓存键,0-RTT 请求会命中独立的缓存条目。如果代理完全拒绝非幂等的 0-RTT 请求,那么缓存中的早期数据条目只可能是 GET 等安全方法,即使发生重放,也不会造成后端状态变化,最多导致缓存命中重复内容。对于动态内容或需要登录态的请求,应使用 no-cache 或 private 响应头,避免被共享缓存保存。
后端幂等性是最后一道防线。即使代理漏掉某个 0-RTT 请求,只要源站接口本身是幂等的,重放也不会产生额外副作用。反向代理应当配合后端约定:所有可能产生数据变更的接口必须使用 POST、PUT、DELETE 等方法,并且这些方法禁止通过 0-RTT 路径到达;而 GET 接口应设计为纯读取,不依赖请求头中的单次令牌或状态变更。对于必须允许 0-RTT 的读接口,可以引入一次性 nonce 或时间窗限制,但会牺牲 0-RTT 的无往返优势,需要权衡。
此外,Apache 的缓存失效策略也要考虑 0-RTT。当源站内容更新时,代理应主动清除相关缓存条目,而不是等待过期,否则早期缓存的响应可能长期保留。可以结合 CachePurge 或外部脚本实现精确失效。安全团队还应监控访问日志中 Early-Data 请求的比例,如果某个源 IP 在短时间内发送大量相同的早期数据,极可能是重放尝试,需要触发限流或封禁。
Apache代理缓存HTTP/30-RTT重放防护修改时间:2026-08-20 11:45:45