feat(dashboard): add HTTP security headers

Adds standard HTTP security headers (CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy) to all dashboard responses to mitigate XSS, clickjacking, and MIME sniffing attacks.

Includes verification tests in test/dashboard-security.test.mjs.

Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-02-18 03:29:21 +00:00
1 parent cea50da512
commit 75c9b669c0
2 files changed
+49 -10

No files matched your search

+17 -10
View File
@@ -7,6 +7,13 @@ import { fileURLToPath } from 'node:url';
import { DEFAULT_DASHBOARD_PORT } from './constants.mjs';
import { getDashboardAuth, parseTorchConfig } from './torch-config.mjs';
const SECURITY_HEADERS = {
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' wss:; object-src 'none'; base-uri 'self';",
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'Referrer-Policy': 'strict-origin-when-cross-origin'
};
function timingSafeCompare(a, b) {
const bufA = Buffer.from(a);
const bufB = Buffer.from(b);
@@ -30,7 +37,7 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
if (auth) {
const authHeader = req.headers.authorization;
if (!authHeader) {
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="TORCH Dashboard"' });
res.writeHead(401, { ...SECURITY_HEADERS, 'WWW-Authenticate': 'Basic realm="TORCH Dashboard"' });
res.end('Authentication required');
return;
}
@@ -48,7 +55,7 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
}
if (!isValid) {
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="TORCH Dashboard"' });
res.writeHead(401, { ...SECURITY_HEADERS, 'WWW-Authenticate': 'Basic realm="TORCH Dashboard"' });
res.end('Invalid credentials');
return;
}
@@ -60,7 +67,7 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
// Redirect / to /dashboard/
if (pathname === '/' || pathname === '/dashboard') {
res.writeHead(302, { 'Location': '/dashboard/' });
res.writeHead(302, { ...SECURITY_HEADERS, 'Location': '/dashboard/' });
res.end();
return;
}
@@ -111,17 +118,17 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
}
delete safeConfig.configPath;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.writeHead(200, { ...SECURITY_HEADERS, 'Content-Type': 'application/json' });
res.end(JSON.stringify(safeConfig, null, 2));
return;
} catch (err) {
console.error('Error parsing torch-config.json:', err);
res.writeHead(500);
res.writeHead(500, SECURITY_HEADERS);
res.end('Internal Server Error');
return;
}
} else {
res.writeHead(404);
res.writeHead(404, SECURITY_HEADERS);
res.end('Not Found');
return;
}
@@ -146,7 +153,7 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
});
if (!isAllowed) {
res.writeHead(403);
res.writeHead(403, SECURITY_HEADERS);
res.end('Forbidden');
return;
}
@@ -160,7 +167,7 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
// Check if file exists and is a file
if (!fileStat || !fileStat.isFile()) {
res.writeHead(404);
res.writeHead(404, SECURITY_HEADERS);
res.end('Not Found');
return;
}
@@ -181,12 +188,12 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
case '.md': contentType = 'text/markdown'; break;
}
res.writeHead(200, { 'Content-Type': contentType });
res.writeHead(200, { ...SECURITY_HEADERS, 'Content-Type': contentType });
fs.createReadStream(filePath).pipe(res);
} catch (err) {
console.error('Dashboard Server Error:', err);
if (!res.headersSent) {
res.writeHead(500);
res.writeHead(500, SECURITY_HEADERS);
res.end('Internal Server Error');
}
}
+32
View File
@@ -69,4 +69,36 @@ test('Dashboard File Access Security', async (t) => {
// path.normalize collapses this to /package.json, which is then blocked
assert.strictEqual(res.statusCode, 403);
});
await t.test('responses include security headers', async () => {
const res = await get('/dashboard/index.html');
assert.strictEqual(res.statusCode, 200);
const headers = res.headers;
// Content-Security-Policy
assert.ok(headers['content-security-policy'], 'CSP header missing');
const csp = headers['content-security-policy'];
assert.match(csp, /default-src 'self'/, 'CSP: missing default-src self');
assert.match(csp, /script-src 'self' 'unsafe-inline' https:\/\/cdn\.jsdelivr\.net/, 'CSP: missing script-src');
assert.match(csp, /object-src 'none'/, 'CSP: missing object-src none');
// X-Content-Type-Options
assert.strictEqual(headers['x-content-type-options'], 'nosniff', 'X-Content-Type-Options mismatch');
// X-Frame-Options
assert.strictEqual(headers['x-frame-options'], 'DENY', 'X-Frame-Options mismatch');
// Referrer-Policy
assert.strictEqual(headers['referrer-policy'], 'strict-origin-when-cross-origin', 'Referrer-Policy mismatch');
});
await t.test('error responses include security headers', async () => {
const res = await get('/dashboard/nonexistent.html');
assert.strictEqual(res.statusCode, 404);
const headers = res.headers;
assert.strictEqual(headers['x-content-type-options'], 'nosniff');
assert.strictEqual(headers['x-frame-options'], 'DENY');
});
});