From 10277e7af0987b8cf931461b42da42a3060e845c Mon Sep 17 00:00:00 2001 From: s6pa1rta3n-lab Date: Tue, 1 Sep 2026 13:09:40 -0400 Subject: [PATCH] fix(web): reject empty hex input in the auditor decoder --- apps/web/package.json | 2 +- apps/web/src/lib/hex.test.ts | 137 +++++++++++++++++++++++++++++++++++ apps/web/src/lib/hex.ts | 2 +- 3 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/lib/hex.test.ts diff --git a/apps/web/package.json b/apps/web/package.json index 51f4a7c..9679412 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -8,7 +8,7 @@ "dev": "vite", "build": "vite build", "preview": "vite preview", - "test": "node --import tsx --test src/demo/trace-health-check.test.ts src/lib/config.test.ts src/lib/chain.test.ts src/dashboard/fixture-health-check.test.ts src/lib/round-status.test.ts src/components/dashboard/RoundStatusCard.test.tsx src/hooks/useDashboardData.test.ts", + "test": "node --import tsx --test src/demo/trace-health-check.test.ts src/lib/config.test.ts src/lib/chain.test.ts src/dashboard/fixture-health-check.test.ts src/lib/round-status.test.ts src/lib/hex.test.ts src/components/dashboard/RoundStatusCard.test.tsx src/hooks/useDashboardData.test.ts", "typecheck": "tsc --noEmit -p tsconfig.json" }, "dependencies": { diff --git a/apps/web/src/lib/hex.test.ts b/apps/web/src/lib/hex.test.ts new file mode 100644 index 0000000..5cb07b9 --- /dev/null +++ b/apps/web/src/lib/hex.test.ts @@ -0,0 +1,137 @@ +// Copyright (c) 2026 Sub Rosa contributors +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { bytesToHex, hexToBytes } from "./hex"; + +test("hexToBytes rejects empty input", () => { + assert.throws(() => hexToBytes(""), { + name: "Error", + message: "invalid hex string", + }); +}); + +test("hexToBytes rejects whitespace-only input", () => { + assert.throws(() => hexToBytes(" "), { + name: "Error", + message: "invalid hex string", + }); + assert.throws(() => hexToBytes("\t\n"), { + name: "Error", + message: "invalid hex string", + }); +}); + +test("hexToBytes rejects prefix-only inputs", () => { + assert.throws(() => hexToBytes("0x"), { + name: "Error", + message: "invalid hex string", + }); + assert.throws(() => hexToBytes("0X"), { + name: "Error", + message: "invalid hex string", + }); + assert.throws(() => hexToBytes(" 0x "), { + name: "Error", + message: "invalid hex string", + }); + assert.throws(() => hexToBytes(" 0X "), { + name: "Error", + message: "invalid hex string", + }); +}); + +test("hexToBytes rejects odd-length inputs", () => { + assert.throws(() => hexToBytes("1"), { + name: "Error", + message: "invalid hex string", + }); + assert.throws(() => hexToBytes("0x1"), { + name: "Error", + message: "invalid hex string", + }); + assert.throws(() => hexToBytes("123"), { + name: "Error", + message: "invalid hex string", + }); + assert.throws(() => hexToBytes("0x123"), { + name: "Error", + message: "invalid hex string", + }); +}); + +test("hexToBytes rejects non-hex characters", () => { + assert.throws(() => hexToBytes("0xzz"), { + name: "Error", + message: "invalid hex string", + }); + assert.throws(() => hexToBytes("0x12g4"), { + name: "Error", + message: "invalid hex string", + }); + assert.throws(() => hexToBytes("0x12 34"), { + name: "Error", + message: "invalid hex string", + }); + assert.throws(() => hexToBytes("invalidhex"), { + name: "Error", + message: "invalid hex string", + }); +}); + +test("hexToBytes decodes valid unprefixed hex", () => { + assert.deepEqual(hexToBytes("00"), new Uint8Array([0x00])); + assert.deepEqual(hexToBytes("ff"), new Uint8Array([0xff])); + assert.deepEqual( + hexToBytes("deadbeef"), + new Uint8Array([0xde, 0xad, 0xbe, 0xef]), + ); + assert.deepEqual( + hexToBytes("0123456789abcdef"), + new Uint8Array([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]), + ); +}); + +test("hexToBytes decodes valid prefixed hex", () => { + assert.deepEqual(hexToBytes("0x00"), new Uint8Array([0x00])); + assert.deepEqual(hexToBytes("0xff"), new Uint8Array([0xff])); + assert.deepEqual( + hexToBytes("0xdeadbeef"), + new Uint8Array([0xde, 0xad, 0xbe, 0xef]), + ); + assert.deepEqual( + hexToBytes("0XDEADBEEF"), + new Uint8Array([0xde, 0xad, 0xbe, 0xef]), + ); + assert.deepEqual( + hexToBytes("0x0123456789abcdef"), + new Uint8Array([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]), + ); +}); + +test("hexToBytes trims surrounding whitespace around valid hex", () => { + assert.deepEqual( + hexToBytes(" 0xdeadbeef "), + new Uint8Array([0xde, 0xad, 0xbe, 0xef]), + ); + assert.deepEqual( + hexToBytes(" deadbeef "), + new Uint8Array([0xde, 0xad, 0xbe, 0xef]), + ); +}); + +test("bytesToHex encodes byte arrays correctly", () => { + assert.equal(bytesToHex(new Uint8Array([])), ""); + assert.equal(bytesToHex(new Uint8Array([0x00])), "00"); + assert.equal(bytesToHex(new Uint8Array([0xff])), "ff"); + assert.equal( + bytesToHex(new Uint8Array([0xde, 0xad, 0xbe, 0xef])), + "deadbeef", + ); +}); + +test("hexToBytes and bytesToHex round-trip", () => { + const original = "deadbeef0123456789abcdef"; + assert.equal(bytesToHex(hexToBytes(original)), original); + assert.equal(bytesToHex(hexToBytes(`0x${original}`)), original); +}); diff --git a/apps/web/src/lib/hex.ts b/apps/web/src/lib/hex.ts index e693e92..b9ad7ae 100644 --- a/apps/web/src/lib/hex.ts +++ b/apps/web/src/lib/hex.ts @@ -1,7 +1,7 @@ // Copyright (c) 2026 Sub Rosa contributors export function hexToBytes(hex: string): Uint8Array { const clean = hex.trim().replace(/^0x/i, ""); - if (clean.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(clean)) { + if (clean.length === 0 || clean.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(clean)) { throw new Error("invalid hex string"); } const out = new Uint8Array(clean.length / 2);