feat(security): add authentication and host binding to dashboard

This commit addresses the missing authentication vulnerability in the
dashboard server.

Changes:
- Added Basic Authentication support via TORCH_DASHBOARD_AUTH env var
  or dashboard.auth config field.
- Defaulted dashboard host binding to 127.0.0.1 for better security.
- Added --host CLI argument to allow overriding the bind address.
- Used crypto.timingSafeEqual for secure credential comparison.
- Fixed a potential crash when parsing malformed Authorization headers.
- Added comprehensive unit tests for dashboard authentication.
- Updated documentation in README.md and torch-config.example.json.

Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-02-17 17:24:33 +00:00
1 parent 5196f264cb
commit 456bc2df18
7 files changed
+174 -8

No files matched your search

+52 -4
View File
@@ -2,15 +2,57 @@ import fs from 'node:fs';
import fsp from 'node:fs/promises';
import path from 'node:path';
import http from 'node:http';
import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { DEFAULT_DASHBOARD_PORT } from './constants.mjs';
import { getDashboardAuth } from './torch-config.mjs';
export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT) {
function timingSafeCompare(a, b) {
const bufA = Buffer.from(a);
const bufB = Buffer.from(b);
if (bufA.length !== bufB.length) {
// Still do a comparison to avoid some timing leaks, though length leak is hard to avoid entirely
crypto.timingSafeEqual(bufA, bufA);
return false;
}
return crypto.timingSafeEqual(bufA, bufB);
}
export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.0.1') {
// Resolve package root relative to this file (src/dashboard.mjs)
// this file is in <root>/src/dashboard.mjs, so '..' goes to <root>
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const auth = getDashboardAuth();
const server = http.createServer(async (req, res) => {
// Basic Auth check
if (auth) {
const authHeader = req.headers.authorization;
if (!authHeader) {
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="TORCH Dashboard"' });
res.end('Authentication required');
return;
}
const parts = authHeader.split(' ');
const type = parts[0];
const credentials = parts[1];
let isValid = false;
if (type === 'Basic' && credentials) {
try {
const decoded = Buffer.from(credentials, 'base64').toString();
isValid = timingSafeCompare(decoded, auth);
} catch {
isValid = false;
}
}
if (!isValid) {
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="TORCH Dashboard"' });
res.end('Invalid credentials');
return;
}
}
// URL parsing
const url = new URL(req.url, `http://${req.headers.host}`);
let pathname = url.pathname;
@@ -82,12 +124,18 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT) {
fs.createReadStream(filePath).pipe(res);
});
server.listen(port, () => {
console.log(`Dashboard running at http://localhost:${port}/dashboard/`);
server.listen(port, host, () => {
const listenUrl = `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}/dashboard/`;
console.log(`Dashboard running at ${listenUrl}`);
if (auth) {
console.log('Authentication: enabled (Basic Auth)');
} else {
console.warn('Authentication: DISABLED (Dashboard is public)');
}
console.log(`Serving files from ${packageRoot}`);
console.log(`Using configuration from ${process.cwd()}`);
});
// Keep process alive
return new Promise(() => {});
return server;
}