eba5a2a38e
Closes the account-takeover chain and related issues found in audit: - WS auth brute-force protection: per-session lockout (authThrottle) + per- socket failure cap that closes the socket (1008); applied to both web-client and desktop auth. Failed auth no longer leaves the socket open for unlimited guesses. - WS upgrades now pass an IP-based rate limiter and Origin allowlist before handleUpgrade (previously bypassed all HTTP middleware); trust proxy set so limiting keys on the real client IP. - /health no longer leaks live session IDs (counts only). - .env.example rate-limit fixed (900000ms / 300) from the accidental ~11k rps. - Credentials moved out of URLs into the X-MacroPad-Password header; images fetched via header + blob URLs; login stores creds in sessionStorage. - Session creation bounded (max sessions, min password length) and TTL-pruned; session store writes are now atomic (temp+rename) and debounced. - Session IDs lengthened to 12 chars with rejection sampling (no modulo bias). - Enable helmet CSP; restrict CORS to configured origins. - Request IDs use crypto.randomUUID; drop postinstall build hook and uuid dep. - Uniform response for unknown session IDs (removes enumeration oracle). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
72 lines
2.5 KiB
TypeScript
72 lines
2.5 KiB
TypeScript
// Configuration for MacroPad Relay Server
|
|
|
|
import dotenv from 'dotenv';
|
|
import path from 'path';
|
|
|
|
dotenv.config();
|
|
|
|
function parseTrustProxy(): boolean | number | string {
|
|
const v = process.env.TRUST_PROXY ?? '1';
|
|
if (v === 'false') return false;
|
|
if (v === 'true') return true;
|
|
const n = Number(v);
|
|
return Number.isNaN(n) ? v : n;
|
|
}
|
|
|
|
export const config = {
|
|
port: parseInt(process.env.PORT || '3000', 10),
|
|
host: process.env.HOST || '0.0.0.0',
|
|
|
|
// Data storage
|
|
dataDir: process.env.DATA_DIR || path.join(__dirname, '..', 'data'),
|
|
|
|
// Session settings
|
|
sessionIdLength: parseInt(process.env.SESSION_ID_LENGTH || '12', 10),
|
|
|
|
// Maximum number of sessions that may exist at once
|
|
maxSessions: parseInt(process.env.MAX_SESSIONS || '1000', 10),
|
|
|
|
// Minimum password length enforced when creating a new session
|
|
minPasswordLength: parseInt(process.env.MIN_PASSWORD_LENGTH || '6', 10),
|
|
|
|
// Sessions unused for this many days are pruned
|
|
sessionTtlDays: parseInt(process.env.SESSION_TTL_DAYS || '90', 10),
|
|
|
|
// Security
|
|
bcryptRounds: parseInt(process.env.BCRYPT_ROUNDS || '10', 10),
|
|
|
|
// Rate limiting (HTTP)
|
|
rateLimitWindowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000', 10), // 15 minutes
|
|
rateLimitMax: parseInt(process.env.RATE_LIMIT_MAX || '300', 10),
|
|
|
|
// WebSocket upgrade rate limiting (per IP)
|
|
wsUpgradeWindowMs: parseInt(process.env.WS_UPGRADE_WINDOW_MS || '60000', 10), // 1 minute
|
|
wsUpgradeMax: parseInt(process.env.WS_UPGRADE_MAX || '60', 10), // max upgrades per IP per window
|
|
|
|
// Auth brute-force protection
|
|
authMaxSocketFailures: parseInt(process.env.AUTH_MAX_SOCKET_FAILURES || '5', 10),
|
|
authLockoutMaxAttempts: parseInt(process.env.AUTH_LOCKOUT_MAX_ATTEMPTS || '10', 10),
|
|
authLockoutWindowMs: parseInt(process.env.AUTH_LOCKOUT_WINDOW_MS || '900000', 10), // 15 minutes
|
|
authLockoutDurationMs: parseInt(process.env.AUTH_LOCKOUT_DURATION_MS || '900000', 10), // 15 minutes
|
|
|
|
// WebSocket
|
|
pingInterval: parseInt(process.env.PING_INTERVAL || '30000', 10), // 30 seconds
|
|
requestTimeout: parseInt(process.env.REQUEST_TIMEOUT || '30000', 10), // 30 seconds
|
|
|
|
// Origin / CORS allowlist (comma-separated). Empty = same-host only.
|
|
allowedOrigins: (process.env.ALLOWED_ORIGINS || '')
|
|
.split(',')
|
|
.map((o) => o.trim())
|
|
.filter((o) => o.length > 0),
|
|
|
|
// Trust the X-Forwarded-* headers from a reverse proxy
|
|
trustProxy: parseTrustProxy(),
|
|
|
|
// Logging
|
|
logLevel: process.env.LOG_LEVEL || 'info',
|
|
|
|
// Environment
|
|
nodeEnv: process.env.NODE_ENV || 'development',
|
|
isDevelopment: process.env.NODE_ENV !== 'production',
|
|
};
|