From 0bb2b5e1a62f5899f271a45b7fb4af000936b726 Mon Sep 17 00:00:00 2001 From: aiirvizionz Date: Tue, 28 Jul 2026 10:49:52 -0600 Subject: [PATCH] fix(pwa): require decimal signature timestamps --- apps/pwa/src/lib/signature.mjs | 8 +++++--- apps/pwa/test/signature.test.mjs | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 apps/pwa/test/signature.test.mjs diff --git a/apps/pwa/src/lib/signature.mjs b/apps/pwa/src/lib/signature.mjs index 0fb1f71..3e493da 100644 --- a/apps/pwa/src/lib/signature.mjs +++ b/apps/pwa/src/lib/signature.mjs @@ -4,11 +4,13 @@ import crypto from "node:crypto"; export function verifySignature(header, rawBody, secret, toleranceSec = 300) { if (!secret || !header) return false; const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=").map((s) => s.trim()))); - const ts = Number(parts.t); + const rawTs = parts.t; + if (!/^\d+$/.test(rawTs || "")) return false; + const ts = Number(rawTs); const v1 = parts.v1; - if (!ts || !v1) return false; + if (!Number.isSafeInteger(ts) || !ts || !v1) return false; if (Math.abs(Math.floor(Date.now() / 1000) - ts) > toleranceSec) return false; - const expected = crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex"); + const expected = crypto.createHmac("sha256", secret).update(`${rawTs}.${rawBody}`).digest("hex"); const a = Buffer.from(v1); const b = Buffer.from(expected); return a.length === b.length && crypto.timingSafeEqual(a, b); diff --git a/apps/pwa/test/signature.test.mjs b/apps/pwa/test/signature.test.mjs new file mode 100644 index 0000000..b9247ad --- /dev/null +++ b/apps/pwa/test/signature.test.mjs @@ -0,0 +1,28 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import test from "node:test"; +import { verifySignature } from "../src/lib/signature.mjs"; + +const secret = "test-secret"; +const rawBody = JSON.stringify({ ok: true }); + +function sign(ts) { + return crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex"); +} + +test("verifySignature accepts a valid decimal timestamp signature", () => { + const ts = String(Math.floor(Date.now() / 1000)); + assert.equal(verifySignature(`t=${ts},v1=${sign(ts)}`, rawBody, secret), true); +}); + +test("verifySignature rejects scientific notation timestamps", () => { + const ts = "1e9"; + const normalized = String(Number(ts)); + const sig = crypto.createHmac("sha256", secret).update(`${normalized}.${rawBody}`).digest("hex"); + assert.equal(verifySignature(`t=${ts},v1=${sig}`, rawBody, secret, 9_999_999_999), false); +}); + +test("verifySignature rejects fractional timestamps", () => { + const ts = `${Math.floor(Date.now() / 1000)}.5`; + assert.equal(verifySignature(`t=${ts},v1=${sign(ts)}`, rawBody, secret), false); +});