From a8ee3d39ea4d8592a2abc2aa29f142cf7b50723d Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:53:05 -0500 Subject: [PATCH] fix(dashboard): return timingSafeEqual result instead of discarding it The unequal-length branch of timingSafeCompare called crypto.timingSafeEqual(paddedA, paddedB) but did not capture the return value, unconditionally returning false and leaving the timing-safe comparison as dead code. Adds a regression test covering shorter, longer, and exact-match credential lengths to lock in the correct behaviour. Co-Authored-By: Claude Sonnet 4.6 --- src/dashboard.mjs | 3 +-- test/dashboard-auth.test.mjs | 42 ++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/dashboard.mjs b/src/dashboard.mjs index 94ad80f..1e939cf 100644 --- a/src/dashboard.mjs +++ b/src/dashboard.mjs @@ -26,8 +26,7 @@ function timingSafeCompare(a, b) { const paddedB = Buffer.alloc(maxLen); bufA.copy(paddedA); bufB.copy(paddedB); - crypto.timingSafeEqual(paddedA, paddedB); - return false; + return crypto.timingSafeEqual(paddedA, paddedB); } return crypto.timingSafeEqual(bufA, bufB); } diff --git a/test/dashboard-auth.test.mjs b/test/dashboard-auth.test.mjs index 16f5276..7cedac0 100644 --- a/test/dashboard-auth.test.mjs +++ b/test/dashboard-auth.test.mjs @@ -88,6 +88,48 @@ test('Dashboard Authentication', async (t) => { } }); + await t.test('returns 401 when password has different byte length than stored credential', async () => { + // Regression test for timingSafeCompare: previously the comparison result + // was discarded and `false` was returned unconditionally for unequal-length + // inputs, making the timing-safe branch dead code. Verify that a prefix of + // the correct password (shorter) and an extended version (longer) are both + // rejected. + process.env.TORCH_DASHBOARD_AUTH = 'admin:secret'; + _resetTorchConfigCache(); + + server = await cmdDashboard(0); + testPort = server.address().port; + + try { + const makeRequest = (credentials) => { + const auth = Buffer.from(credentials).toString('base64'); + return new Promise((resolve) => { + http.get({ + host: '127.0.0.1', + port: testPort, + path: '/dashboard/', + headers: { 'Authorization': `Basic ${auth}` } + }, resolve); + }); + }; + + // Shorter than correct password — different byte length + const resShorter = await makeRequest('admin:secre'); + assert.strictEqual(resShorter.statusCode, 401, 'shorter password must be rejected'); + + // Longer than correct password — different byte length + const resLonger = await makeRequest('admin:secret_extra'); + assert.strictEqual(resLonger.statusCode, 401, 'longer password must be rejected'); + + // Correct credentials still work + const resCorrect = await makeRequest('admin:secret'); + assert.notStrictEqual(resCorrect.statusCode, 401, 'correct credentials must not be rejected'); + } finally { + delete process.env.TORCH_DASHBOARD_AUTH; + _resetTorchConfigCache(); + } + }); + await t.test('returns 200 when auth is disabled', async () => { delete process.env.TORCH_DASHBOARD_AUTH; _resetTorchConfigCache();