fix(security): harden dashboard auth timing, XSS, and CSP

- Fix timingSafeCompare to pad buffers to equal length before comparing,
  closing the content timing side-channel on length mismatch
- Replace innerHTML with textContent for error messages to prevent XSS
- Extract inline script from dashboard/index.html into dashboard/app.js
- Remove 'unsafe-inline' from script-src CSP directive
- Update security test to match new CSP policy

https://claude.ai/code/session_01GfwUNbgeMgXreFDbsNQMsS
This commit is contained in:
Claude committed 2026-02-19 20:53:15 +00:00
1 parent d74d81c0c3
commit e7ecd2617e
4 files changed
+811 -813

No files matched your search

+10 -3
View File
@@ -8,7 +8,7 @@ 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';",
'Content-Security-Policy': "default-src 'self'; script-src 'self' 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'
@@ -18,8 +18,15 @@ 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);
// Pad both buffers to equal length so the comparison takes constant time
// regardless of input lengths. The length difference is still observable,
// but the content timing channel is closed.
const maxLen = Math.max(bufA.length, bufB.length);
const paddedA = Buffer.alloc(maxLen);
const paddedB = Buffer.alloc(maxLen);
bufA.copy(paddedA);
bufB.copy(paddedB);
crypto.timingSafeEqual(paddedA, paddedB);
return false;
}
return crypto.timingSafeEqual(bufA, bufB);