Node.js AES-GCM 身份验证标签验证缺失与不当废弃处理漏洞详解

本文详细披露了Node.js加密模块中一个严重的安全漏洞。该漏洞源于`createDecipheriv`函数未正确强制执行AES-GCM身份验证标签的默认长度(16字节),允许攻击者通过截断标签并进行暴力破解,最终可能导致密钥材料泄露和任意密文伪造。

报告摘要

报告ID: #3463949

标题: AES-GCM 身份验证标签验证缺失与不当废弃处理

报告人: sideni 报告给: Node.js 报告时间: 2025年12月13日,UTC 下午4:49 状态: 已公开 (2025年12月19日) 严重性: 高 (8.2) 弱点: 缺少必要的加密步骤

漏洞详情

问题概述

在 Node.js 的 crypto 模块中,createDecipheriv 函数的文档指出:"authTagLength 选项默认为 16 字节,如果使用不同的长度,则必须设置为不同的值。"

然而,身份验证标签(authentication tag)的长度并未根据该默认值进行验证,可以被截断至 4、8、12、13、14 或 15 字节。这使得攻击者可以在修改密文后暴力破解身份验证标签,并恢复密钥材料,从而在之后离线计算更多身份验证标签。

影响与风险

这些经过身份验证的加密方案通常用于保护会话或令牌。攻击者可以利用此缺陷,通过更改有效密文并在服务器上暴力破解新的身份验证标签(最多需要约 12,884,902,656 次请求:(2**32) * 3 + 2 * 256)来获得有效的会话/令牌。

这还导致了 AES-GCM 的非重用(nonce reuse),从而泄露用于计算任何后续身份验证标签的密钥。请注意,不一定需要 120 亿次请求,因为攻击者可以利用截断的标签来恢复所需的密钥。

获得计算任意身份验证标签所需的密钥后,攻击者可以操纵密文进行解密并伪造新的密文。

废弃处理问题

我理解到,隐式使用较短标签长度的行为已通过某个 PR 被废弃。但是,废弃警告仅在标签实际短于 16 字节时才会发出。对于按照文档使用该函数(即期望 authTagLength 选项默认为 16 字节)的用户来说,除非攻击正在进行,否则永远不会看到警告。

复现步骤

以下脚本展示了默认的 16 字节 authTagLength 如何未被强制执行,从而允许隐式使用较短标签。同时,它也展示了废弃警告仅当攻击者截断身份验证标签(或明确截断标签,但这不是用户在未知危险中的场景)时才会显示。

复现代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const {
    createCipheriv,
    createDecipheriv
} = require('crypto');

const key = 'key0123456789key';
const nonce = '123456789012';

var algo = 'aes-128-gcm';
const cipher = createCipheriv(algo, key, nonce);
const plaintext = 'This is some plaintext';

const ciphertext = cipher.update(plaintext, 'utf8');
cipher.final();
const tag = cipher.getAuthTag();

var decipher = createDecipheriv(algo, key, nonce);
decipher.setAuthTag(tag);
var decryptedPlaintext = decipher.update(ciphertext, null, 'utf8') + decipher.final();

console.log('Decrypted with full tag: ' + decryptedPlaintext);

console.log('---------------------------------');

decipher = createDecipheriv(algo, key, nonce);
// 截断身份验证标签
decipher.setAuthTag(tag.subarray(0, 4));
decryptedPlaintext = decipher.update(ciphertext, null, 'utf8') + decipher.final();
console.log('Decrypted with truncated tag: ' + decryptedPlaintext);

观察废弃警告消息如何仅对后一种情况显示,以及被截断的身份验证标签如何成功允许解密(尽管“authTagLength 选项默认为 16 字节,如果使用不同的长度,则必须设置为不同的值。”)。

影响总结

  • 密文可以被修改以解密为任意值。
  • 通过观察解析差异可以解密密文(例如,如果解密后的数据预期是 JSON,则可以像填充预言攻击那样观察有效的/无效的 JSON 解析来解密密文)。
  • 可以恢复 GCM 内部的 GHASH 密钥(用于计算更多任意身份验证标签,而无需暴力破解)。这需要轮换加密密钥。 考虑到 createDecipheriv 函数的记录方式,这完全破坏了人们期望从此 API 获得的加密方案的完整性和机密性。

官方回应与处理过程

Node.js 团队成员 tniessen 回应 (2025年12月14日): 感谢报告。虽然同意当前行为不理想且文档可以改进,但这不幸是一个已知问题,并已在公开场合进行了广泛讨论。当前行为是由于 node:crypto 模块在许多方面紧密模仿 OpenSSL,包括这一点。虽然我们意识到这个问题已有一段时间,但由于某些应用程序依赖当前行为(即用户应用程序自行验证身份验证标签长度),我们无法在不经过废弃周期的情况下更改行为。

鉴于该行为已公开记录至少八年,认为无需将此已知问题视为漏洞。提议在 Node.js 的下一个主要版本中将其移至生命周期结束状态。

报告人 sideni 补充 (15天前):

不介意不将此问题视为漏洞。关切的是人们按照文档使用此功能却不知道自己面临风险。按照当前的废弃方式,只有依赖当前行为的用户知道它即将停止工作。其他人甚至不知道自己很脆弱。

即使不作为漏洞处理,也希望看到文档更新(添加警告等),或者废弃警告可以发给任何未在 createDecipheriv 上指定 authTagLength 选项的用户(而不仅仅是那些明确截断标签或正在被主动利用的用户)。感觉因为少数人使用有问题的行为而让大众保持脆弱很奇怪。

最终处理方案 (tniessen, 14天前):

  1. 将此报告作为信息性报告关闭。报告人可以请求公开披露此报告以提高透明度和可见性。
  2. 创建一个文档拉取请求,以更醒目地警告当前行为。这将在 Node.js 的下一个常规版本中可见。
  3. 创建一个拉取请求,将当前的废弃状态移至生命周期结束状态。这将在下一个主要版本中生效。

报告人 sideni 同意该计划。

报告状态更新:

附加信息:

  • CVE ID: 无
  • 赏金: 无
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计