Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## 2026-06-03 - Hardening equality checks against timing-based length leakage

**Vulnerability:** Equality checks using `timingSafeEqual` or simple loops were leaking the length of the secret because they either performed a length check first or only iterated up to the length of the provided input.

**Learning:** `timingSafeEqual` requires both buffers to have the exact same length. If an application checks `left.length === right.length` before calling it, it leaks the secret's length. Even a constant-time loop that only iterates up to the length of one of the inputs can leak the length of the other.

**Prevention:** To prevent length leakage, always hash both the expected secret and the user-provided input with a fixed-length hashing algorithm (like SHA-256) before performing a constant-time comparison on the resulting hashes. This ensures the comparison is always performed on buffers of the same, fixed length.
6 changes: 3 additions & 3 deletions frontend/src/lib/middleware/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ function hashSecret(secret) {
}

export function safeEqual(a = '', b = '') {
const left = Buffer.from(a);
const right = Buffer.from(b);
return left.length === right.length && timingSafeEqual(left, right);
const leftHash = createHash('sha256').update(String(a)).digest();
const rightHash = createHash('sha256').update(String(b)).digest();
return timingSafeEqual(leftHash, rightHash);
}
Comment on lines 23 to 27

export function createSessionCookieValue() {
Expand Down
18 changes: 13 additions & 5 deletions frontend/src/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export async function proxy(request) {
const sessionCookie = request.cookies.get(SESSION_COOKIE)?.value || '';
if (secret && sessionCookie) {
const expected = await hashSecret(secret);
if (safeEqualText(sessionCookie, expected)) return NextResponse.next();
if (await safeEqualText(sessionCookie, expected)) return NextResponse.next();
}

const loginUrl = request.nextUrl.clone();
Expand All @@ -34,11 +34,19 @@ async function hashSecret(secret) {
return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('');
}

function safeEqualText(a = '', b = '') {
if (a.length !== b.length) return false;
async function safeEqualText(a = '', b = '') {
const aBytes = new TextEncoder().encode(String(a));
const bBytes = new TextEncoder().encode(String(b));
const [aHash, bHash] = await Promise.all([
crypto.subtle.digest('SHA-256', aBytes),
crypto.subtle.digest('SHA-256', bBytes),
]);
const aArr = new Uint8Array(aHash);
const bArr = new Uint8Array(bHash);
if (aArr.length !== bArr.length) return false;
let mismatch = 0;
for (let i = 0; i < a.length; i += 1) {
mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
for (let i = 0; i < aArr.length; i++) {
mismatch |= aArr[i] ^ bArr[i];
}
return mismatch === 0;
}