From 738b358a4e1793a18508a0cfeb42487962b7057a Mon Sep 17 00:00:00 2001 From: Lee Cheneler Date: Mon, 1 Dec 2025 21:39:39 +0000 Subject: [PATCH 1/2] fix(cookies): use constant-time comparison for signature verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace naive string comparison (===) with @std/crypto timingSafeEqual() to prevent timing attacks. The previous implementation returned early on the first mismatched character, allowing attackers to measure response times and incrementally guess correct signatures byte-by-byte. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- cookies/cookie-utils.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/cookies/cookie-utils.ts b/cookies/cookie-utils.ts index fa1d421..ba489e6 100644 --- a/cookies/cookie-utils.ts +++ b/cookies/cookie-utils.ts @@ -1,3 +1,4 @@ +import { timingSafeEqual } from "@std/crypto"; import type { TabiContext } from "../app/mod.ts"; /** @@ -28,7 +29,13 @@ const createSignature = async ( /** * Verify HMAC-SHA256 signature for a value. - * Uses constant-time comparison to prevent timing attacks. + * + * Uses constant-time comparison via @std/crypto timingSafeEqual() to prevent + * timing attacks. A naive string comparison (===) returns early on the first + * mismatched character, allowing attackers to measure response times and + * incrementally guess the correct signature byte-by-byte. Constant-time + * comparison always takes the same amount of time regardless of where the + * mismatch occurs, eliminating this side-channel. */ const verifySignature = async ( value: string, @@ -36,7 +43,18 @@ const verifySignature = async ( secret: string, ): Promise => { const expectedSignature = await createSignature(value, secret); - return signature === expectedSignature; + + const encoder = new TextEncoder(); + const a = encoder.encode(signature); + const b = encoder.encode(expectedSignature); + + // Length check is safe to leak via timing - attackers already know the + // expected signature length (64 hex chars for SHA-256) + if (a.byteLength !== b.byteLength) { + return false; + } + + return timingSafeEqual(a, b); }; /** From e0d5ff35fce917beec54f155278bef9fa7dae9ff Mon Sep 17 00:00:00 2001 From: Lee Cheneler Date: Mon, 1 Dec 2025 21:41:19 +0000 Subject: [PATCH 2/2] chore: bump version to 0.1.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deno.json b/deno.json index 2025ac2..8898fe6 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,6 @@ { "name": "@tabirun/app", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", "nodeModulesDir": "auto", "publish": {