fix(mam-api): TRUST_PROXY boot warning + CSRF integration tests + bounded rate-limit map
Fixes three issues in the authentication system:
C1: Add boot-time warning when AUTH_ENABLED=true but TRUST_PROXY!=true.
Without TRUST_PROXY=true behind nginx, req.ip becomes the proxy IP for all
clients, collapsing per-IP rate limiting into a shared pool. Operators must
explicitly set TRUST_PROXY=true to make per-IP rate limiting effective.
C2: Mount requireUiHeader middleware in test helpers (auth.test.js,
users.test.js, tokens.test.js). The CSRF header validation was not being
exercised in the test suite. Tests now send X-Requested-With: dragonflight-ui
headers that are actually validated by the middleware.
I1: Implement bounded rate-limit Map with MAX_ENTRIES=10000 and LRU eviction.
Unbounded Maps are vulnerable to spray attacks: attackers can force memory
exhaustion by requesting with distinct IPs. Now we evict the oldest entry
(by insertion order) when the map reaches capacity.
This commit is contained in:
parent
d209a192c3
commit
96effaaa3c
6 changed files with 3008 additions and 3 deletions
2992
services/mam-api/package-lock.json
generated
Normal file
2992
services/mam-api/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -3,6 +3,7 @@
|
|||
const failures = new Map(); // ip -> count
|
||||
|
||||
const STEPS = [1000, 2000, 4000, 8000, 16000, 30000];
|
||||
const MAX_ENTRIES = 10000; // bounded to prevent unbounded growth under spray attacks
|
||||
|
||||
export const ipBackoff = {
|
||||
delayMs(ip) {
|
||||
|
|
@ -11,6 +12,11 @@ export const ipBackoff = {
|
|||
return STEPS[Math.min(n - 1, STEPS.length - 1)];
|
||||
},
|
||||
recordFailure(ip) {
|
||||
// Evict the oldest entry if we're at the cap. Map preserves insertion order,
|
||||
// so .keys().next().value is the oldest.
|
||||
if (failures.size >= MAX_ENTRIES && !failures.has(ip)) {
|
||||
failures.delete(failures.keys().next().value);
|
||||
}
|
||||
failures.set(ip, (failures.get(ip) || 0) + 1);
|
||||
},
|
||||
recordSuccess(ip) { failures.delete(ip); },
|
||||
|
|
|
|||
|
|
@ -276,6 +276,9 @@ const server = app.listen(PORT, () => {
|
|||
const authMode = process.env.AUTH_ENABLED === 'true' ? 'ENABLED' : 'DISABLED (set AUTH_ENABLED=true for production)';
|
||||
console.log(`MAM API listening on port ${PORT}`);
|
||||
console.log(`Authentication: ${authMode}`);
|
||||
if (process.env.AUTH_ENABLED === 'true' && process.env.TRUST_PROXY !== 'true') {
|
||||
console.warn('[auth] WARNING: AUTH_ENABLED=true but TRUST_PROXY=false — req.ip will be the proxy IP, login rate-limit will throttle all clients together. Set TRUST_PROXY=true when behind nginx/HTTPS.');
|
||||
}
|
||||
// Boot the recorder scheduler tick loop after the HTTP server is live so
|
||||
// the loop's self-calls to /recorders/:id/start|stop reach a ready socket.
|
||||
startSchedulerLoop();
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import express from 'express';
|
|||
import session from 'express-session';
|
||||
import authRouter from '../../src/routes/auth.js';
|
||||
import { hashPassword } from '../../src/auth/passwords.js';
|
||||
import { requireAuth } from '../../src/middleware/auth.js';
|
||||
import { requireAuth, requireUiHeader } from '../../src/middleware/auth.js';
|
||||
|
||||
async function appWithAuth(pool) {
|
||||
process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;
|
||||
|
|
@ -52,6 +52,7 @@ async function appWithSession(pool) {
|
|||
cookie: { httpOnly: true, sameSite: 'lax', secure: false, maxAge: 8 * 3600 * 1000 },
|
||||
rolling: false, resave: false, saveUninitialized: false,
|
||||
}));
|
||||
app.use('/api/v1', requireUiHeader);
|
||||
app.use('/api/v1/auth', authRouter);
|
||||
return new Promise(r => {
|
||||
const srv = app.listen(0, '127.0.0.1', () => {
|
||||
|
|
@ -119,6 +120,7 @@ async function appWithSessionAndMe(pool) {
|
|||
cookie: { httpOnly: true, sameSite: 'lax', secure: false, maxAge: 8 * 3600 * 1000 },
|
||||
rolling: false, resave: false, saveUninitialized: false,
|
||||
}));
|
||||
app.use('/api/v1', requireUiHeader);
|
||||
app.use('/api/v1/auth', authRouter);
|
||||
app.get('/api/v1/protected/me', requireAuth, (req, res) => res.json({ user: req.user }));
|
||||
return new Promise(r => {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import session from 'express-session';
|
|||
import { isTestDbConfigured, setupTestDb } from '../helpers/setup-db.js';
|
||||
import tokensRouter from '../../src/routes/tokens.js';
|
||||
import authRouter from '../../src/routes/auth.js';
|
||||
import { requireAuth } from '../../src/middleware/auth.js';
|
||||
import { requireAuth, requireUiHeader } from '../../src/middleware/auth.js';
|
||||
import { hashPassword } from '../../src/auth/passwords.js';
|
||||
|
||||
async function app(pool) {
|
||||
|
|
@ -20,6 +20,7 @@ async function app(pool) {
|
|||
cookie: { httpOnly: true, sameSite: 'lax', secure: false, maxAge: 8 * 3600 * 1000 },
|
||||
rolling: false, resave: false, saveUninitialized: false,
|
||||
}));
|
||||
a.use('/api/v1', requireUiHeader);
|
||||
a.use('/api/v1/auth', authRouter);
|
||||
a.use('/api/v1/auth/tokens', requireAuth, tokensRouter);
|
||||
a.get('/api/v1/protected/ping', requireAuth, (req, res) => res.json({ user: req.user.username }));
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import session from 'express-session';
|
|||
import { isTestDbConfigured, setupTestDb } from '../helpers/setup-db.js';
|
||||
import usersRouter from '../../src/routes/users.js';
|
||||
import authRouter from '../../src/routes/auth.js';
|
||||
import { requireAuth } from '../../src/middleware/auth.js';
|
||||
import { requireAuth, requireUiHeader } from '../../src/middleware/auth.js';
|
||||
import { hashPassword } from '../../src/auth/passwords.js';
|
||||
|
||||
async function app(pool) {
|
||||
|
|
@ -20,6 +20,7 @@ async function app(pool) {
|
|||
cookie: { httpOnly: true, sameSite: 'lax', secure: false, maxAge: 8 * 3600 * 1000 },
|
||||
rolling: false, resave: false, saveUninitialized: false,
|
||||
}));
|
||||
a.use('/api/v1', requireUiHeader);
|
||||
a.use('/api/v1/auth', authRouter);
|
||||
a.use('/api/v1/auth/users', requireAuth, usersRouter);
|
||||
return new Promise(r => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue