fix: apply security and correctness review findings across both stacks
Legacy panel: - upstream body reads now carry their own deadline and a 10 MB byte budget; a stalled modem can no longer hang /api/status fan-out forever nor OOM the proxy (request headers alone had the timeout, bodies had none) - add X-Frame-Options DENY / CSP frame-ancestors none / nosniff; the panel (delete-instance and confirmed-write dialogs) is no longer clickjackable - only send a JSON content-type when the API console request has a body, so payload-less dangerous writes stop failing with 400 and burning the one-use confirmation token - register a form-urlencoded parser (the proxy branch was unreachable) and drop the multipart parser that buffered up to 60 MB before rejecting; bodyLimit drops to 2 MB; framework-level 415 keeps the stable error body - remove /api/sms/send from readable paths: GET bypassed the write confirmation for a send endpoint - /api/instances/:id/login maps upstream failures to a stable 502 instead of leaking raw error text - guard MULTI_SIMADMIN_TIMEOUT_MS parsing (NaN aborted every request); prune dead code (buildClients, cookie expando no-op) Control plane: - deleting a notification channel detaches it from rules instead of leaving dangling ids that made every referencing rule unreadable and silently dropped future notifications; rule reads tolerate unknown ids - startup sweep resets notification_queue rows stranded in 'sending' by a crash (mirrors the sms outbox sweep); terminal outbox rows are pruned on the retention timer - /api/v1/metrics no longer emits operator-assigned node names on the session-free scrape; login limiter map is bounded and pruned; Secure cookie honors the gateway-declared x-forwarded-proto - webhook delivery sets redirect: manual (signed payloads are not replayed) - SMTP envelope sender is validated against CR/LF smuggling - scheduled reboots with delaySeconds != 3 fail fast at the dispatcher with a clear reason instead of burning every retry; contract narrowed to the pinned baseline
This commit is contained in:
@@ -33,7 +33,8 @@ const cookie = (token: string, secure: boolean): string =>
|
||||
`${CONSOLE_SESSION_COOKIE}=${token}; Path=${COOKIE_PATH}; HttpOnly; SameSite=Strict${secure ? '; Secure' : ''}; Max-Age=${MAX_AGE_SECONDS}`;
|
||||
const clearCookie = (secure: boolean): string =>
|
||||
`${CONSOLE_SESSION_COOKIE}=; Path=${COOKIE_PATH}; HttpOnly; SameSite=Strict${secure ? '; Secure' : ''}; Max-Age=0`;
|
||||
const usesSecureCookie = (request: FastifyRequest): boolean => request.protocol === 'https';
|
||||
const usesSecureCookie = (request: FastifyRequest): boolean =>
|
||||
request.protocol === 'https' || request.headers['x-forwarded-proto'] === 'https';
|
||||
|
||||
const problem = (request: FastifyRequest, status: number, code: string, detail: string) => ({
|
||||
type: 'about:blank',
|
||||
@@ -83,8 +84,26 @@ const loginSchema = {
|
||||
properties: { password: { type: 'string', minLength: 1, maxLength: 1024 } },
|
||||
} as const;
|
||||
|
||||
const LOGIN_BUCKETS_MAX = 10_000;
|
||||
|
||||
export function registerConsoleAuth(app: FastifyInstance, auth: ConsoleAuthService): void {
|
||||
const loginAttempts = new Map<string, { count: number; resetAt: number }>();
|
||||
const rememberAttempt = (key: string, attempt: { count: number; resetAt: number }): void => {
|
||||
// Direct-to-API callers can send arbitrary client-ip values; cap the map so
|
||||
// bucket keys cannot grow without bound.
|
||||
if (loginAttempts.size >= LOGIN_BUCKETS_MAX) {
|
||||
const now = Date.now();
|
||||
for (const [existing, value] of loginAttempts) {
|
||||
if (value.resetAt <= now) loginAttempts.delete(existing);
|
||||
}
|
||||
while (loginAttempts.size >= LOGIN_BUCKETS_MAX) {
|
||||
const oldest = loginAttempts.keys().next().value;
|
||||
if (oldest === undefined) break;
|
||||
loginAttempts.delete(oldest);
|
||||
}
|
||||
}
|
||||
loginAttempts.set(key, attempt);
|
||||
};
|
||||
app.addHook('preHandler', async (request, reply) => {
|
||||
const pathname = request.url.split(/[?#]/u, 1)[0] ?? request.url;
|
||||
if (!auth.isProtectedPath(pathname)) return;
|
||||
@@ -121,7 +140,7 @@ export function registerConsoleAuth(app: FastifyInstance, auth: ConsoleAuthServi
|
||||
attempts && attempts.resetAt > now
|
||||
? attempts
|
||||
: { count: 0, resetAt: now + LOGIN_WINDOW_MS };
|
||||
loginAttempts.set(key, { count: active.count + 1, resetAt: active.resetAt });
|
||||
rememberAttempt(key, { count: active.count + 1, resetAt: active.resetAt });
|
||||
return handle(request, reply, error);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user