diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 1597b82..64a5233 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -17,3 +17,7 @@ **Vulnerability:** The in-memory rate limiter implemented a hard ceiling (`MAX_STORE_ENTRIES`) and rejected all new IPs when reached, creating a trivial Global Denial of Service (DoS) vulnerability where an attacker could spam spoofed IPs to fill the bounded Map and permanently block all legitimate traffic. **Learning:** "Failing securely" by denying unknown requests during capacity limits is incorrect for rate limiters. Bounded Maps must actively evict older entries rather than rejecting new ones, otherwise the security mechanism itself becomes the vector for DoS. **Prevention:** In-memory rate limiters using bounded Maps must implement a FIFO (First-In-First-Out) or LRU (Least Recently Used) eviction policy (e.g., deleting `store.keys().next().value`) to make room for new IPs when maximum capacity is reached. +## 2024-05-18 - Incorrect Scrypt Hash Generation in Plaintext Fallback +**Vulnerability:** The plaintext password fallback logic incorrectly generated a recommended `scrypt` hash using the SHA256 hashed password rather than the raw `storedPassword`. This leads to generating an invalid `scrypt` hash that would fail authentication if the user updated their configuration with it. Furthermore, hashing variable-length secrets with SHA256 before `crypto.timingSafeEqual` prevents timing attacks, but using a non-keyed hash like SHA256 may leave it vulnerable. HMAC-SHA256 keyed with a secret provides better security. +**Learning:** When generating a secure hash recommendation for users migrating from plaintext, ensure the raw input is used, not an intermediate hash intended for timing-safe comparison. +**Prevention:** Generate the `scrypt` hash directly from the raw secret string (`storedPassword`). Always use HMAC keyed with a secret for hashing variable-length passwords prior to constant-time comparison. diff --git a/lib/auth.ts b/lib/auth.ts index b6026ec..4f3500b 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -111,12 +111,18 @@ export function authenticate( } else { // Plaintext fallback (deprecated) // 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(storedPassword).digest(); + const attemptHash = crypto + .createHmac('sha256', getConfig().authSecret) + .update(password) + .digest(); + const storedHash = crypto + .createHmac('sha256', getConfig().authSecret) + .update(storedPassword) + .digest(); isValid = crypto.timingSafeEqual(attemptHash, storedHash); if (isValid) { - const recommendedHash = generateScryptHash(storedHash.toString('hex')); + const recommendedHash = generateScryptHash(storedPassword); console.warn( `\n⚠️ SECURITY WARNING: ${type === 'subpage' ? 'Subpage' : 'Album'} "${key}" is using a plaintext password in gallery.yaml.\n` + ` Please replace it with this native secure hash:\n\n ${recommendedHash}\n`,