From 171836d2accaa5ecaa42fe93c48d70db8a30cc04 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:35:32 +0000 Subject: [PATCH] fix: restrict dashboard file access to allowed directories This patch addresses a security vulnerability where the dashboard server could serve any file from the package root. Changes: - Implemented an allowlist in `src/dashboard.mjs` to restrict file access to: - `dashboard/` - `assets/` - `src/docs/` - `torch-config.json` - Added `test/dashboard-security.test.mjs` to verify the fix and prevent regressions. The fix ensures that sensitive files like `package.json` or source code cannot be accessed via the dashboard server. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- src/dashboard.mjs | 19 +++++++++ test/dashboard-security.test.mjs | 70 ++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 test/dashboard-security.test.mjs diff --git a/src/dashboard.mjs b/src/dashboard.mjs index 24e3d5f..3853d04 100644 --- a/src/dashboard.mjs +++ b/src/dashboard.mjs @@ -90,6 +90,25 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0. const safePath = path.normalize(pathname).replace(new RegExp('^(\\.\\.[\\/\\\\])+'), ''); let filePath = path.join(packageRoot, safePath); + // Security check: restrict access to allowed paths + const allowedPaths = [ + path.join(packageRoot, 'dashboard'), + path.join(packageRoot, 'assets'), + path.join(packageRoot, 'src', 'docs'), + path.join(packageRoot, 'torch-config.json') + ]; + + const isAllowed = allowedPaths.some(allowedPath => { + const rel = path.relative(allowedPath, filePath); + return !rel.startsWith('..') && !path.isAbsolute(rel); + }); + + if (!isAllowed) { + res.writeHead(403); + res.end('Forbidden'); + return; + } + // If directory, try index.html let fileStat = await statSafe(filePath); if (fileStat && fileStat.isDirectory()) { diff --git a/test/dashboard-security.test.mjs b/test/dashboard-security.test.mjs new file mode 100644 index 0000000..f86e5a5 --- /dev/null +++ b/test/dashboard-security.test.mjs @@ -0,0 +1,70 @@ + +import test from 'node:test'; +import assert from 'node:assert/strict'; +import http from 'node:http'; +import { cmdDashboard } from '../src/dashboard.mjs'; + +test('Dashboard File Access Security', async (t) => { + const testPort = 4175; + let server; + + t.beforeEach(async () => { + server = await cmdDashboard(testPort); + }); + + t.afterEach(() => { + if (server) server.close(); + }); + + function get(path) { + return new Promise((resolve) => { + http.get({ + host: '127.0.0.1', + port: testPort, + path: path + }, (res) => { + // Consume data to avoid memory leaks + res.resume(); + resolve(res); + }); + }); + } + + await t.test('allows access to dashboard assets', async () => { + const res = await get('/dashboard/index.html'); + assert.strictEqual(res.statusCode, 200); + }); + + await t.test('allows access to global assets', async () => { + // assuming favicon.svg exists + const res = await get('/assets/favicon.svg'); + assert.strictEqual(res.statusCode, 200); + }); + + await t.test('blocks access to package.json', async () => { + const res = await get('/package.json'); + assert.strictEqual(res.statusCode, 403); + }); + + await t.test('blocks access to src/dashboard.mjs', async () => { + const res = await get('/src/dashboard.mjs'); + assert.strictEqual(res.statusCode, 403); + }); + + await t.test('blocks access to random root files', async () => { + const res = await get('/README.md'); + assert.strictEqual(res.statusCode, 403); + }); + + await t.test('allows access to torch-config.json via special handler', async () => { + const res = await get('/torch-config.json'); + // It exists in root, so it should return 200 + assert.strictEqual(res.statusCode, 200); + }); + + await t.test('blocks directory traversal attempts', async () => { + const res = await get('/dashboard/../../package.json'); + // path.normalize collapses this to /package.json, which is then blocked + assert.strictEqual(res.statusCode, 403); + }); +});