fix: use constant-time comparison for all secret/token checks#687
fix: use constant-time comparison for all secret/token checks#687spidershield-contrib wants to merge 2 commits intosamanhappy:mainfrom
Conversation
Replace `===`/`!==` with `crypto.timingSafeEqual` wrapper for: - Bearer token lookups (BearerKeyDao, sseService, auth middleware) - OAuth access token validation (oauthBearer) - OAuth client secret verification (oauthServerService) - Refresh token comparison (oauthServerService) Prevents timing side-channel attacks that could brute-force tokens one character at a time. CWE-208: Observable Timing Discrepancy
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the security posture of the application by addressing a critical timing side-channel vulnerability. By replacing standard string comparison operators with a cryptographically secure, constant-time comparison function, the system is now protected against attackers who might attempt to deduce secrets by measuring response times. This change improves the robustness of authentication mechanisms without altering their core logic. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request is a great step towards hardening the application against timing side-channel attacks by replacing standard string comparisons with a constant-time comparison function. The changes are applied consistently across several sensitive areas. However, my review found a subtle but important flaw in the implementation of the new safeCompare utility that could still leak the length of secrets via a timing attack. I've provided a detailed comment and a code suggestion to address this vulnerability. Once that is fixed, this PR will be a solid security improvement.
src/utils/safeCompare.ts
Outdated
| if (a.length !== b.length) { | ||
| // Still do a comparison to avoid leaking length info via early return timing | ||
| timingSafeEqual(Buffer.from(a), Buffer.from(a)); | ||
| return false; | ||
| } | ||
| return timingSafeEqual(Buffer.from(a), Buffer.from(b)); |
There was a problem hiding this comment.
This implementation of safeCompare is still vulnerable to a timing attack that can leak the length of the secret string a.
The issue is that Buffer.from(b) is only executed in the code path where a.length === b.length. This creates a small but measurable difference in execution time between the two branches. An attacker can measure this timing difference to determine if their supplied token has the same length as the secret, thus revealing the secret's length.
To fix this, we should ensure that both branches of the comparison perform a similar amount of work. This can be achieved by creating buffers from both strings unconditionally at the start of the function, and ensuring the timingSafeEqual function is always called on buffers of the same length.
const bufA = Buffer.from(a);
const bufB = Buffer.from(b);
const areSameLength = bufA.length === bufB.length;
// If lengths are different, we compare `bufA` with itself. This ensures the
// `timingSafeEqual` function is always called and runs in a time
// proportional to the secret's length, which is constant from the attacker's
// perspective.
const bufToCompare = areSameLength ? bufB : bufA;
const result = timingSafeEqual(bufA, bufToCompare);
// The final result is true only if the lengths were the same and the content matched.
return areSameLength && result;Buffer both strings unconditionally and always call timingSafeEqual with same-length buffers. When lengths differ, compare bufA with itself so execution time stays proportional to the secret's length, closing the timing side-channel that could reveal secret length.
|
@gemini-code-assist Good catch on the length leakage — fixed in 097e408. Both buffers are now created unconditionally upfront, and |
|
Thanks for the update, @teehooai. I appreciate you addressing the length leakage concern. However, upon reviewing the The If To fully close this side-channel, the Severity: critical Here's a suggested correction for import { timingSafeEqual } from 'crypto';
/**
* Compare two strings in constant time to prevent timing attacks.
* Returns true if both strings are equal.
*/
export function safeCompare(a: string, b: string): boolean {
const bufA = Buffer.from(a);
const bufB = Buffer.from(b);
// crypto.timingSafeEqual inherently handles length differences securely.
// It returns false if lengths differ, and does so in constant time.
return timingSafeEqual(bufA, bufB);
} |
Summary
Replace
===/!==withcrypto.timingSafeEqualfor all authentication secret comparisons to prevent timing side-channel attacks.Fixes #686
Problem
Seven locations compare secrets using JavaScript's
===/!==, which short-circuits on the first mismatched character. An attacker can measure response times to brute-force tokens one character at a time (CWE-208).Solution
Add a
safeCompare()utility wrappingcrypto.timingSafeEqualand apply to all 7 secret comparison sites:Changes
src/utils/safeCompare.tssrc/dao/BearerKeyDao.tskey.token === token→safeCompare(key.token, token)src/services/sseService.tssrc/middlewares/auth.tssrc/utils/oauthBearer.tsaccessToken !== token→!safeCompare(...)src/services/oauthServerService.tsTesting
safeComparehandles unequal-length strings without leaking length info