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
+49 -11
View File
@@ -25,6 +25,20 @@
const crypto = require('crypto');
const { Router } = require('express');
/**
* Валидирует redirect-цель: разрешает только локальные пути (начинается с '/',
* не начинается с '//').
* Защита от open redirect: //evil.com и https://evil.com → '/'.
* @param {string|undefined} target
* @returns {string}
*/
function safeReturn(target) {
if (typeof target === 'string' && target.startsWith('/') && !target.startsWith('//')) {
return target;
}
return '/';
}
function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USERS }) {
const router = Router();
@@ -57,7 +71,7 @@ function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USER
return res.redirect('/login?error=' + encodeURIComponent('Пользователь не найден'));
}
req.session.user = {
const userData = {
email: user.email,
clientId: user.clientId,
companyId: user.companyId,
@@ -65,7 +79,16 @@ function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USER
isAdmin: user.role === 'admin',
};
res.redirect(req.query.returnTo || '/');
// session.regenerate() меняет идентификатор сессии — защита от session fixation (A07).
// Данные пишем уже в новую сессию.
req.session.regenerate((err) => {
if (err) { console.error('[login] session.regenerate error:', err); return res.status(500).send('Session error'); }
req.session.user = userData;
req.session.save((saveErr) => {
if (saveErr) { console.error('[login] session.save error:', saveErr); return res.status(500).send('Session error'); }
res.redirect(safeReturn(req.query.returnTo));
});
});
});
// ── GET /callback (только OIDC-режим) ──────────────────────────────────────
@@ -87,12 +110,19 @@ function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USER
try {
const { user, idToken, refreshToken } = await auth.exchangeCode(code);
req.session.user = user;
req.session.idToken = idToken;
req.session.refreshToken = refreshToken;
const returnTo = req.session.returnTo || '/';
delete req.session.returnTo;
res.redirect(returnTo);
// Сохраняем returnTo до regenerate, так как regenerate сбросит сессию.
const returnTo = safeReturn(req.session.returnTo);
// session.regenerate() — защита от session fixation (A07).
req.session.regenerate((err) => {
if (err) { console.error('[callback] session.regenerate error:', err); return res.status(500).send('Session error'); }
req.session.user = user;
req.session.idToken = idToken;
req.session.refreshToken = refreshToken;
req.session.save((saveErr) => {
if (saveErr) { console.error('[callback] session.save error:', saveErr); return res.status(500).send('Session error'); }
res.redirect(returnTo);
});
});
} catch (e) {
console.error('[auth] exchangeCode error:', e.message);
res.redirect('/login?error=' + encodeURIComponent('Ошибка авторизации: ' + e.message));
@@ -159,8 +189,16 @@ function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USER
};
}
req.session.user = user;
res.redirect('/');
req.session.regenerate((err) => {
if (err) { console.error('[dev-login] session.regenerate error:', err); return res.status(500).send('Session error'); }
req.session.user = user;
// Восстанавливаем devKey в новой сессии — иначе следующий запрос потребует повторного ?key=
if (auth.DEV_SECRET) req.session.devKey = auth.DEV_SECRET;
req.session.save((saveErr) => {
if (saveErr) { console.error('[dev-login] session.save error:', saveErr); return res.status(500).send('Session error'); }
res.redirect('/');
});
});
});
// ── GET /logout ────────────────────────────────────────────────────────────
@@ -182,4 +220,4 @@ function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, MOCK_USER
return router;
}
module.exports = { createRouter };
module.exports = { createRouter, safeReturn };