From 258c36899cf56ab7f6cb23d07fbdf6d1781f5203 Mon Sep 17 00:00:00 2001 From: Ralksta <16941050+ralksta@users.noreply.github.com> Date: Wed, 1 Apr 2026 12:46:04 +0200 Subject: [PATCH] fix(auth): secure plaintext password comparison against timing attacks (#68) Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- .jules/sentinel.md | 6 +++++- lib/auth.ts | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 077613b..9d32122 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -4,4 +4,8 @@ **Learning:** Next.js dynamic endpoints that do not hit external upstream APIs or databases (like image generation with `next/og`) are often overlooked for rate limiting. Rate limiting is just as critical for protecting local compute resources as it is for protecting downstream API quotas or databases. Any route performing on-the-fly heavy processing must implement throttling. -**Prevention:** Apply the shared `checkRateLimit` utility (from `@/lib/rate-limit`) to all computationally expensive endpoints (such as `next/og` usage), even if they do not explicitly query external services. Ensure `getConfig().rateLimitRpm` is passed as the threshold to maintain configurable global protection. \ No newline at end of file +**Prevention:** Apply the shared `checkRateLimit` utility (from `@/lib/rate-limit`) to all computationally expensive endpoints (such as `next/og` usage), even if they do not explicitly query external services. Ensure `getConfig().rateLimitRpm` is passed as the threshold to maintain configurable global protection. +## 2024-05-18 - [Preventing Timing Attacks on Variable-Length Secrets] +**Vulnerability:** Comparing variable-length plaintext passwords using strict equality (`===`) or `crypto.timingSafeEqual` with unequal lengths, which leaks length information and enables timing attacks. +**Learning:** When comparing variable-length secrets (like plaintext fallback passwords), both values must be hashed to a fixed-length algorithm (like SHA-256) first to ensure identical input lengths for the secure comparison, preventing side-channel leaks. +**Prevention:** Always hash variable-length secrets to a fixed length before passing them to `crypto.timingSafeEqual`. diff --git a/lib/auth.ts b/lib/auth.ts index 3f02903..9ef194a 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -96,7 +96,11 @@ export function authenticate(slug: string, password: string): string | null { isValid = verifyScrypt(password, sp.password); } else { // Plaintext fallback (deprecated) - isValid = password === sp.password; + // Hash both to a fixed length before constant-time comparison to prevent timing and length attacks + const attemptHash = crypto.createHash('sha256').update(password).digest(); + const storedHash = crypto.createHash('sha256').update(sp.password).digest(); + isValid = crypto.timingSafeEqual(attemptHash, storedHash); + if (isValid) { const recommendedHash = generateScryptHash(sp.password); console.warn(