-
-
Notifications
You must be signed in to change notification settings - Fork 69.1k
[Bug]: Timing Attack Vulnerability in Token Comparisons #6021
Description
CVSS Assessment
| Metric | Value |
|---|---|
| Score | 7.4 / 10.0 |
| Severity | High |
| Vector | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N |
Summary
Multiple token validation endpoints use standard string comparison (!== or ===) instead of constant-time comparison. This allows attackers to determine tokens character-by-character via timing side-channel attacks.
Affected Code
Instance 1: Hook Token Validation
File: src/gateway/server-http.ts:82
const { token, fromQuery } = extractHookToken(req, url);
if (!token || token !== hooksConfig.token) { // VULNERABLE: timing leak
res.statusCode = 401;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Unauthorized");
return true;
}Instance 2: Node Pairing Token Verification
File: src/infra/node-pairing.ts:277
export async function verifyNodeToken(
nodeId: string,
token: string,
baseDir?: string,
): Promise<{ ok: boolean; node?: NodePairingPairedNode }> {
const state = await loadState(baseDir);
const normalized = normalizeNodeId(nodeId);
const node = state.pairedByNodeId[normalized];
if (!node) {
return { ok: false };
}
return node.token === token ? { ok: true, node } : { ok: false }; // VULNERABLE: timing leak
}Instance 3: Device Token Verification
File: src/infra/device-pairing.ts:434
export async function verifyDeviceToken(params: {
deviceId: string;
token: string;
role: string;
scopes: string[];
baseDir?: string;
}): Promise<{ ok: boolean; reason?: string }> {
return await withLock(async () => {
const state = await loadState(params.baseDir);
const device = state.pairedByDeviceId[normalizeDeviceId(params.deviceId)];
// ... validation checks ...
if (entry.token !== params.token) { // VULNERABLE: timing leak
return { ok: false, reason: "token-mismatch" };
}
// ...
});
}Contrast with secure implementations in the codebase:
src/gateway/auth.ts:35-40:
import { timingSafeEqual } from "crypto";
function safeEqual(a: string, b: string): boolean {
if (a.length !== b.length) {
return false;
}
return timingSafeEqual(Buffer.from(a), Buffer.from(b));
}src/line/signature.ts:12-17:
// Use constant-time comparison to prevent timing attacks.
if (hashBuffer.length !== signatureBuffer.length) {
return false;
}
return crypto.timingSafeEqual(hashBuffer, signatureBuffer);Note: The safeEqual function in auth.ts is currently private (not exported) and is only used internally for gateway token/password validation.
Attack Surface
How is this reached?
- Network (HTTP/WebSocket endpoint, API call)
Authentication required?
- None (unauthenticated/public access)
Entry points:
- Hook endpoint at the gateway HTTP server (server-http.ts)
- Node pairing verification during device connection (node-pairing.ts)
- Device token verification during device authentication (device-pairing.ts)
Attacker makes many requests with different token guesses, measuring response times.
Exploit Conditions
Complexity:
- High (requires race condition, specific config, or timing)
User interaction:
- None (automatic, no victim action needed)
Prerequisites:
- Network access to the vulnerable endpoint
- Ability to make many requests with precise timing measurements
- Low network jitter (local network or controlled environment)
Impact Assessment
Scope:
- Unchanged (impact limited to vulnerable component)
What can an attacker do?
| Impact Type | Level | Description |
|---|---|---|
| Confidentiality | High | Extract hook, node pairing, or device tokens via timing oracle |
| Integrity | High | With hook token, attacker can invoke hooks and modify gateway state; with node/device token, attacker can impersonate paired devices |
| Availability | None | No direct availability impact |
Steps to Reproduce
- Target the hook endpoint:
POST /hooks - Use a timing attack tool (e.g., time-based blind techniques):
import requests import time def measure_response_time(token_guess): start = time.perf_counter_ns() requests.post("http://target/hooks", headers={"X-Hook-Token": token_guess}) return time.perf_counter_ns() - start # Compare timing for different first characters for c in "abcdefghijklmnopqrstuvwxyz0123456789": avg_time = sum(measure_response_time(c + "X" * 31) for _ in range(1000)) / 1000 print(f"{c}: {avg_time}ns")
- Character with longer average response time is correct (comparison went further before failing)
- Repeat for each character position to extract full token
Recommended Fix
Export the safeEqual() function from auth.ts and use it in all three vulnerable locations:
Step 1: Export the function in src/gateway/auth.ts:
export function safeEqual(a: string, b: string): boolean {
if (a.length !== b.length) {
return false;
}
return timingSafeEqual(Buffer.from(a), Buffer.from(b));
}Step 2: Fix src/gateway/server-http.ts:
import { safeEqual } from "./auth.js";
if (!token || !safeEqual(token, hooksConfig.token)) {
res.statusCode = 401;
// ...
}Step 3: Fix src/infra/node-pairing.ts:
import { timingSafeEqual } from "node:crypto";
function safeEqual(a: string, b: string): boolean {
if (a.length !== b.length) {
return false;
}
return timingSafeEqual(Buffer.from(a), Buffer.from(b));
}
// In verifyNodeToken():
return safeEqual(node.token, token) ? { ok: true, node } : { ok: false };Step 4: Fix src/infra/device-pairing.ts:
import { timingSafeEqual } from "node:crypto";
function safeEqual(a: string, b: string): boolean {
if (a.length !== b.length) {
return false;
}
return timingSafeEqual(Buffer.from(a), Buffer.from(b));
}
// In verifyDeviceToken():
if (!safeEqual(entry.token, params.token)) {
return { ok: false, reason: "token-mismatch" };
}Alternatively, create a shared utility module for safeEqual to avoid duplication across the three files.
References
- CWE: CWE-208 - Observable Timing Discrepancy