feat(dashboard): implement rate limiting for basic auth

- Add rate limiting to prevent brute force attacks on dashboard authentication
- Block IPs for 5 minutes after 5 failed attempts
- Return 429 Too Many Requests when blocked
- Clean up state periodically to prevent memory leaks

Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-02-21 18:35:13 +00:00
1 parent 79e261d7b2
commit f719f6bf97
1 file changed
+56
+56
View File
@@ -37,10 +37,50 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const auth = await getDashboardAuth();
// Rate limiting state
const MAX_AUTH_ATTEMPTS = 5;
const BLOCK_DURATION_MS = 300000; // 5 minutes
const CLEANUP_INTERVAL_MS = 600000; // 10 minutes
const rateLimit = new Map(); // ip -> { attempts, blockExpires, lastSeen }
// Cleanup old entries
const cleanupInterval = setInterval(() => {
const now = Date.now();
for (const [ip, data] of rateLimit.entries()) {
if (data.blockExpires && data.blockExpires < now) {
rateLimit.delete(ip);
} else if (!data.blockExpires && (now - data.lastSeen > CLEANUP_INTERVAL_MS)) {
rateLimit.delete(ip);
}
}
}, CLEANUP_INTERVAL_MS);
const server = http.createServer(async (req, res) => {
try {
// Basic Auth check
if (auth) {
const clientIp = req.socket.remoteAddress;
const now = Date.now();
// Check rate limit
const clientState = rateLimit.get(clientIp) || { attempts: 0, blockExpires: 0, lastSeen: now };
// Update last seen
clientState.lastSeen = now;
if (clientState.blockExpires > now) {
res.writeHead(429, {
...SECURITY_HEADERS,
'Retry-After': Math.ceil((clientState.blockExpires - now) / 1000)
});
res.end('Too Many Requests');
return;
} else if (clientState.blockExpires) {
// Block expired, reset state
clientState.attempts = 0;
clientState.blockExpires = 0;
}
const authHeader = req.headers.authorization;
if (!authHeader) {
res.writeHead(401, { ...SECURITY_HEADERS, 'WWW-Authenticate': 'Basic realm="TORCH Dashboard"' });
@@ -61,10 +101,22 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
}
if (!isValid) {
// Increment attempts
clientState.attempts += 1;
if (clientState.attempts >= MAX_AUTH_ATTEMPTS) {
clientState.blockExpires = Date.now() + BLOCK_DURATION_MS;
}
rateLimit.set(clientIp, clientState);
res.writeHead(401, { ...SECURITY_HEADERS, 'WWW-Authenticate': 'Basic realm="TORCH Dashboard"' });
res.end('Invalid credentials');
return;
}
// Reset attempts on success
if (rateLimit.has(clientIp)) {
rateLimit.delete(clientIp);
}
}
// URL parsing
@@ -219,6 +271,10 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
}
});
server.on('close', () => {
clearInterval(cleanupInterval);
});
return new Promise((resolve, reject) => {
server.listen(port, host, () => {
const listenUrl = `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}/dashboard/`;