security: fix 7 vulnerabilities from Opus code review

- [A01] /export: mount after auth.middleware, filter by company for non-admin
- [A02] OIDC: verify issuer, audience, select key by kid; preload JWKS at startup
- [A07] session fixation: session.regenerate() in POST /login, GET /callback, POST /dev-login
- [A10] open redirect: safeReturn() helper validates returnTo (blocks //evil.com, https:// etc)
- [A05] CSRF: getSessionIdentifier uses req.sessionID instead of dead req.cookies.jwt
- [A05] fail-fast: reject default SESSION_SECRET / CSRF_SECRET in NODE_ENV=production
- [A05] DEV_MODE=true blocked in production

Tests: 68 PASS, 0 FAIL (was 50, added 18 new security-focused tests)
This commit is contained in:
2026-05-30 15:25:57 +03:00
parent ed6e9c4464
commit a5170a062c
6 changed files with 249 additions and 42 deletions
+26 -9
View File
@@ -75,20 +75,28 @@ async function start() {
// ── Публичные маршруты (до auth.middleware) ──────────────────────────────────
// ── Fail-fast в production ────────────────────────────────────────────────
// Если секреты не заданы — приложение в проде немедленно падает.
// Дефолтные значения — только для локальной разработки.
if (process.env.NODE_ENV === 'production') {
const DEFAULT_SESSION = 'dev-session-secret-change-me';
const DEFAULT_CSRF = 'dev-csrf-secret-change-in-prod';
if (!process.env.SESSION_SECRET || process.env.SESSION_SECRET === DEFAULT_SESSION) {
console.error('FATAL: SESSION_SECRET не задан или равен дефолту в production!');
process.exit(1);
}
if (!process.env.CSRF_SECRET || process.env.CSRF_SECRET === DEFAULT_CSRF) {
console.error('FATAL: CSRF_SECRET не задан или равен дефолту в production!');
process.exit(1);
}
}
// k8s liveness probe — простая проверка что сервер жив
app.get('/healthz', (req, res) => res.send('OK'));
// JWKS endpoint для валидации токенов внешними сервисами (только в mock-режиме)
if (auth.jwksHandler) app.get('/.well-known/jwks.json', auth.jwksHandler);
// GET /export: агрегированный список CIDR, без авторизации.
// src/routes/export.js содержит логику и комментарии.
app.use(require('./src/routes/export').createRouter({
q,
exportLimiter,
aggregateCIDRs,
}));
// GET /login, POST /login, GET /logout.
// src/routes/auth.js содержит логику и комментарии.
app.use(require('./src/routes/auth').createRouter({
@@ -98,9 +106,18 @@ async function start() {
MOCK_USERS,
}));
// ── Все маршруты ниже требуют валидного JWT (проверяет auth.middleware) ───────
// ── Все маршруты ниже требуют авторизации (auth.middleware) ────────────────
app.use(auth.middleware);
// GET /export: CIDR текущего пользователя (или всех компаний для admin).
// Перемещён за auth.middleware — изоляция данных по компании.
// src/routes/export.js содержит логику и комментарии.
app.use(require('./src/routes/export').createRouter({
q,
exportLimiter,
aggregateCIDRs,
}));
// GET /, POST /add, POST /edit/:id, POST /delete/:id.
// src/routes/entries.js содержит логику и комментарии.
app.use(require('./src/routes/entries').createRouter({