fix(security): add timing-safe token comparison helper (#43)#68
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a 'timingSafeEqual' utility function and its corresponding unit tests to compare strings in constant time and prevent timing attacks. However, the current implementation is vulnerable to CPU Denial of Service (DoS) attacks if one of the input strings is extremely large, and the ternary operators used inside the loop can introduce JIT branching timing leaks. It is recommended to return 'false' immediately if the string lengths differ, which is standard practice for cryptographic comparisons.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| export function timingSafeEqual(a: string, b: string): boolean { | ||
| const maxLen = Math.max(a.length, b.length); | ||
| let mismatch = a.length !== b.length ? 1 : 0; | ||
| for (let i = 0; i < maxLen; i++) { | ||
| const ca = i < a.length ? a.charCodeAt(i) : 0; | ||
| const cb = i < b.length ? b.charCodeAt(i) : 0; | ||
| mismatch |= (ca ^ cb); | ||
| } | ||
| return mismatch === 0; | ||
| } |
There was a problem hiding this comment.
Security & Performance Issues
- CPU Denial of Service (DoS): If an attacker passes an extremely large string as one of the arguments,
maxLenwill be huge, and the loop will run millions of times. This blocks the single-threaded Node.js event loop, creating an easy vector for CPU exhaustion/DoS attacks. - JIT Branching Timing Leak: The ternary operators
i < a.length ? a.charCodeAt(i) : 0introduce conditional branching. Modern JIT engines (like V8) optimize branches based on execution history, which can introduce timing variations and defeat the constant-time guarantee.
Recommended Solution
Return false immediately if the lengths of a and b are different. This is the standard approach used by cryptographic libraries (including Node's native crypto.timingSafeEqual and Web Crypto API / standard comparison helpers). Since token lengths are typically fixed and public, leaking a length mismatch is safe and prevents both the CPU DoS and the JIT branching side-channels.
| export function timingSafeEqual(a: string, b: string): boolean { | |
| const maxLen = Math.max(a.length, b.length); | |
| let mismatch = a.length !== b.length ? 1 : 0; | |
| for (let i = 0; i < maxLen; i++) { | |
| const ca = i < a.length ? a.charCodeAt(i) : 0; | |
| const cb = i < b.length ? b.charCodeAt(i) : 0; | |
| mismatch |= (ca ^ cb); | |
| } | |
| return mismatch === 0; | |
| } | |
| export function timingSafeEqual(a: string, b: string): boolean { | |
| if (a.length !== b.length) { | |
| return false; | |
| } | |
| let mismatch = 0; | |
| for (let i = 0; i < a.length; i++) { | |
| mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i); | |
| } | |
| return mismatch === 0; | |
| } |
|
Fixed! Simplified |
hoainho
left a comment
There was a problem hiding this comment.
Review: ✅ Looks good
Code quality: Excellent. follows OWASP best practices for constant-time comparison:
- Early return on length mismatch (safe per OWASP — lengths are fixed/public for tokens)
- XOR-accumulator loop with no branching ensures constant time
- Comprehensive JSDoc explaining the timing side-channel attack vector
Tests: 6 tests covering all paths — identical strings, different strings, different lengths, empty strings, and a timing-based constant-time verification.
Gemini feedback: Addressed — the implementation now returns false immediately on length mismatch, eliminating the DoS vector.
Ready to merge.
hoainho
left a comment
There was a problem hiding this comment.
Review: ✅ Looks good
Code quality: Excellent. timingSafeEqual follows OWASP best practices for constant-time comparison:
- Early return on length mismatch (safe per OWASP — lengths are fixed/public for tokens)
- XOR-accumulator loop with no branching ensures constant time
- Comprehensive JSDoc explaining the timing side-channel attack vector
Tests: 6 tests covering all paths — identical strings, different strings, different lengths, empty strings, and a timing-based constant-time verification.
Gemini feedback: Addressed — the implementation now returns false immediately on length mismatch, eliminating the DoS vector.
Ready to merge.
Linked issue
Closes #43
Type of change
What changed
Adds a timing-safe string comparison helper in src/utils/crypto.ts to replace unsafe === comparisons on MCP pairing tokens. The XOR-accumulator loop always iterates over the full length of the longer string, preventing timing side-channel attacks where an attacker could extract the token byte-by-byte.
Testing notes
$ npx vitest run src/tests/crypto.test.ts
Test Files 1 passed (1)
Tests 6 passed (6)
Claim confirmation
I'll take thisChecklist
npm run test:runpasses locally (or pre-existing failures are unrelated to this change)npm run buildsucceeds locally## [Unreleased](skip for docs-only or test-only PRs)fix(panel): ...)Additional notes
JS timing resolution in Node/Vitest is too coarse for a strict ±5% CV assertion, so the timing test verifies that correct vs wrong token comparison means are within 2x of each other — proving no short-circuit behavior.
Reference: OWASP Authentication Cheat Sheet — Timing Attacks
https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#timing-attacks