Merge pull request #277 from PR0M3TH3AN/dashboard-security-headers-10540095592303062285

Add HTTP Security Headers to Dashboard
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-18 09:47:54 -05:00
commit 10ca8a34da
2 files changed
+50 -11

No files matched your search

+18 -11
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;
}
@@ -121,17 +128,17 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
delete publicConfig.dashboard.auth;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(publicConfig, null, 2));
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;
}
@@ -158,7 +165,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;
}
@@ -172,7 +179,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;
}
@@ -193,12 +200,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');
});
});