7. 安全考虑 (Security Considerations)
本节描述 QPACK 中可能存在安全关注的区域:
-
使用压缩作为基于长度的预言机 (oracle), 以验证关于秘密值的猜测, 这些秘密值被压缩进共享压缩上下文.
-
解码器的处理能力或内存容量被耗尽而导致拒绝服务.
7.1 探测动态表状态 (Probing Dynamic Table State)
QPACK 通过利用 HTTP 等协议中固有的冗余来减少字段段 (field section) 的编码大小. 这样做的最终目标是减少发送 HTTP 请求或响应所需的数据量.
如果攻击者既能定义要编码和传输的字段, 又能在字段编码后观察其长度, 则可探测用于编码头部字段和尾部字段的压缩上下文. 当攻击者同时具备这两种能力时, 就可以自适应地修改请求, 以确认关于动态表状态的猜测. 如果某个猜测被压缩成更短长度, 攻击者就能观察编码后的长度并推断该猜测正确.
即使在传输层安全协议 ([TLS]) 和 QUIC 传输协议 ([QUIC-TRANSPORT]) 之上, 这仍然可能发生, 因为 TLS 和 QUIC 虽然为内容提供机密性保护, 但只对内容长度提供有限保护.
注意: 填充方案只能针对具备这些能力的攻击者提供有限保护, 可能只是迫使攻击者增加猜测次数才能获知与某个猜测相关的长度. 填充方案还会直接抵消压缩效果, 因为它增加了需要传输的比特数.
CRIME ([CRIME]) 等攻击证明了这类通用攻击者能力的存在. 该特定攻击利用了 DEFLATE ([RFC1951]) 基于前缀匹配消除冗余这一事实. 这允许攻击者逐字符确认猜测, 将指数时间攻击降低为线性时间攻击.
7.1.1 对 QPACK 和 HTTP 的适用性 (Applicability to QPACK and HTTP)
QPACK 通过强制猜测必须匹配整条字段行而不是单个字符, 缓解了以 CRIME ([CRIME]) 为模型的攻击, 但并不能完全阻止这类攻击. 攻击者只能得知某个猜测是否正确, 因此攻击者会退化为对给定字段名称相关字段值的暴力猜测.
因此, 恢复特定字段值是否可行取决于值的熵. 因而, 高熵值不太可能被成功恢复. 但是, 低熵值仍然容易受攻击.
只要两个互不信任的实体控制的请求或响应被放到同一个 HTTP/3 连接上, 这种攻击就可能发生. 如果共享的 QPACK 压缩器允许一个实体向动态表添加条目, 又允许另一个实体在编码选定字段行时引用这些条目, 则攻击者 (第二个实体) 可以通过观察编码输出长度来获知表状态.
例如, 当中间节点执行以下任一操作时, 可能出现来自互不信任实体的请求或响应:
-
在面向源服务器的单个连接上发送来自多个客户端的请求, 或
-
从多个源服务器取得响应, 并将它们放到面向客户端的共享连接上.
Web 浏览器还需要假定不同 Web 源 ([RFC6454]) 在同一连接上发出的请求来自互不信任的实体. 也可能存在涉及互不信任实体的其他场景.
7.1.2 缓解措施 (Mitigation)
如果 HTTP 使用者要求头部字段或尾部字段具有机密性, 可以使用熵足够高、使猜测不可行的值. 但是, 这作为通用解决方案并不现实, 因为它要求所有 HTTP 使用者都采取步骤来缓解攻击. 这会给 HTTP 的使用方式施加新的约束.
与其对 HTTP 使用者施加约束, QPACK 实现可以改为限制压缩的应用方式, 以限制动态表探测的潜在影响.
理想方案是根据构造消息的实体来隔离对动态表的访问. 加入表中的字段值归属于某个实体, 并且只有创建特定值的实体才能提取该值.
为了提高此选项的压缩性能, 某些条目可以标记为公开. 例如, Web 浏览器可以让 Accept-Encoding 头部字段的值在所有请求中可用.
如果编码器并不清楚字段值的来源, 也可以对具有相同字段名但不同值的大量字段行引入惩罚. 这种惩罚可以使大量猜测某个字段值的尝试导致后续消息中该字段不再与动态表条目比较, 从而有效阻止进一步猜测.
这种响应可以与字段值长度成反比. 对较短值, 针对给定字段名禁用动态表访问可以更快发生, 或以更高概率发生; 对较长值则相反.
这种缓解措施在两个端点之间最有效. 如果消息由中间节点重新编码, 而该中间节点不知道给定消息由哪个实体构造, 则中间节点可能无意中合并原始编码器特意保持分离的压缩上下文.
注意: 如果攻击者有可靠方法使值被重新安装, 简单地从动态表中删除与该字段对应的条目可能无效. 例如, Web 浏览器中加载图片的请求通常包含 Cookie 头部字段 (这类攻击可能高度重视的目标), 网站也很容易强制加载图片, 从而刷新动态表中的条目.
7.1.3 永不索引的字面量 (Never-Indexed Literals)
实现也可以选择通过不压缩敏感字段, 而是把其值编码为字面量来保护这些字段.
拒绝把字段行插入动态表只有在所有跳都避免插入时才有效. 永不索引字面量位 (见第 4.5.4 节) 可以用来向中间节点表明某个特定值是有意作为字面量发送的.
中间节点禁止把设置了 N 位的字面量表示重新编码为会对其建立索引的其他表示. 如果使用 QPACK 重新编码, 则必须使用设置了 N 位的字面量表示. 如果使用 HPACK 重新编码, 则必须使用永不索引字面量表示 (见 [RFC7541] 第 6.2.3 节).
是否标记某个字段值永不应被索引取决于多个因素. 由于 QPACK 不能防止对整个字段值的猜测, 短值或低熵值更容易被对手恢复. 因此, 编码器可以选择不索引低熵值.
编码器也可以选择不索引那些被认为一旦恢复会很有价值或很敏感的字段值, 例如 Cookie 或 Authorization 头部字段.
相反, 对于即使暴露也几乎没有价值的字段, 编码器可能更倾向于索引其值. 例如, User-Agent 头部字段通常在请求之间不会变化, 并且会发送给任何服务器. 在这种情况下, 确认使用了某个特定 User-Agent 值并没有多少价值.
注意, 随着新攻击被发现, 用于决定是否使用永不索引字面量表示的这些准则会随时间演进.
7.2 静态 Huffman 编码 (Static Huffman Encoding)
目前没有已知针对静态 Huffman 编码的攻击. 一项研究显示, 使用静态 Huffman 编码表会造成信息泄漏; 不过, 同一项研究也得出结论, 攻击者无法利用这种信息泄漏恢复任何有意义数量的信息 (见 [PETAL]).
7.3 内存消耗 (Memory Consumption)
攻击者可以尝试使端点耗尽内存. QPACK 的设计目标是限制端点分配内存的峰值量和稳定量.
QPACK 使用动态表最大大小和最大阻塞流数量的定义, 限制编码器能使解码器消耗的内存量. 在 HTTP/3 中, 这些值分别由解码器通过设置参数 SETTINGS_QPACK_MAX_TABLE_CAPACITY 和 SETTINGS_QPACK_BLOCKED_STREAMS 控制 (见第 3.2.3 节和第 2.1.2 节). 动态表大小限制会考虑动态表中存储数据的大小, 外加少量开销余量. 阻塞流数量限制只是解码器所需最大内存量的近似指标. 实际最大内存量取决于解码器跟踪每个阻塞流所使用的内存.
解码器可以通过为动态表最大大小设置合适值来限制动态表使用的状态内存量. 在 HTTP/3 中, 这是通过为 SETTINGS_QPACK_MAX_TABLE_CAPACITY 参数设置合适值来实现的. 编码器可以选择小于解码器允许值的动态表大小, 并向解码器发出信号 (见第 4.3.1 节), 从而限制自身使用的状态内存量.
解码器可以通过为最大阻塞流数量设置合适值来限制阻塞流使用的状态内存量. 在 HTTP/3 中, 这是通过为 SETTINGS_QPACK_BLOCKED_STREAMS 参数设置合适值来实现的. 有可能变为阻塞的流不会在编码器上消耗额外状态内存.
编码器会分配内存, 以跟踪未确认字段段中对动态表的所有引用. 实现可以通过只使用自己愿意跟踪数量的动态表引用来直接限制状态内存量; 不需要向解码器发出信号. 但是, 限制对动态表的引用会降低压缩效果.
编码器或解码器消耗的临时内存量可以通过顺序处理字段行来限制. 解码器实现解码字段段时不需要保留完整字段行列表. 如果编码器实现使用单遍算法, 则编码字段段时不需要保留完整字段行列表. 注意, 应用程序可能由于其他原因需要保留完整字段行列表; 即使 QPACK 不强制这样做, 应用约束也可能使其成为必要.
虽然协商得到的动态表大小限制覆盖了 QPACK 实现可能消耗的大部分内存, 但由于流量控制而无法立即发送的数据不受该限制影响. 实现应当限制未发送数据的大小, 尤其是在解码器流上, 因为那里可选择发送内容的灵活性有限. 对未发送数据过多的可能响应包括限制对等方打开新流的能力、只从编码器流读取, 或关闭连接.
7.4 实现限制 (Implementation Limits)
QPACK 实现需要确保整数的大值、整数的长编码或长字符串字面量不会造成安全弱点.
实现必须为其接受的整数值以及编码长度设置限制; 见第 4.1.1 节. 同样, 它必须为其接受的字符串字面量长度设置限制; 见第 4.1.2 节. 这些限制应当足够大, 以便处理 HTTP 实现可配置接受的最大单个字段.
如果实现遇到大于其能够解码的值, 则当该值位于请求流上时, 必须将其视为类型为 QPACK_DECOMPRESSION_FAILED 的流错误; 当该值位于编码器流或解码器流上时, 必须将其视为适当类型的连接错误.