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:
+49
-11
@@ -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 };
|
||||
|
||||
+29
-11
@@ -1,12 +1,14 @@
|
||||
/**
|
||||
* src/routes/export.js — публичный маршрут GET /export.
|
||||
* src/routes/export.js — маршрут GET /export (требует авторизации).
|
||||
*
|
||||
* Назначение: отдаёт агрегированный список всех активных CIDR (все компании)
|
||||
* в виде текстового файла (один адрес/подсеть на строку).
|
||||
* Назначение: отдаёт агрегированный список активных CIDR в виде текстового файла.
|
||||
*
|
||||
* Изоляция по ролям:
|
||||
* - Обычный пользователь → только CIDR своей компании.
|
||||
* - Администратор → все компании (или фильтр по ?company=<id>).
|
||||
*
|
||||
* Особенности:
|
||||
* - НЕ требует авторизации (ТЗ: «доступ ограничивается на сетевом уровне»).
|
||||
* - Регистрируется ДО auth.middleware в server.js.
|
||||
* - Требует авторизации: регистрируется ПОСЛЕ auth.middleware в server.js.
|
||||
* - Ограничен exportLimiter: не более 20 запросов/мин с одного IP.
|
||||
* - aggregateCIDRs объединяет перекрывающиеся диапазоны → минимальный набор.
|
||||
*
|
||||
@@ -25,15 +27,31 @@ const { Router } = require('express');
|
||||
function createRouter({ q, exportLimiter, aggregateCIDRs }) {
|
||||
const router = Router();
|
||||
|
||||
// GET /export — агрегированный whitelist всех компаний в текстовом виде.
|
||||
// exportLimiter ограничивает частоту запросов (20/мин) до проверки авторизации —
|
||||
// это важно, так как маршрут публичный.
|
||||
// GET /export — агрегированный whitelist CIDR в текстовом виде.
|
||||
// Требует авторизации (регистрируется ПОСЛЕ auth.middleware в server.js).
|
||||
// Изоляция по ролям:
|
||||
// - Обычный пользователь → только CIDR своей компании.
|
||||
// - Администратор → все компании, или фильтр по ?company=<id>.
|
||||
// exportLimiter ограничивает частоту запросов (20/мин).
|
||||
router.get('/export', exportLimiter, async (req, res) => {
|
||||
try {
|
||||
// Получаем все активные CIDR без фильтра по компании
|
||||
const cidrs = await q.getExportCIDRs();
|
||||
let companyId = null;
|
||||
|
||||
// Суммаризация: объединяем пересекающиеся и смежные диапазоны
|
||||
if (!req.user.isAdmin) {
|
||||
// Обычный пользователь: только его компания.
|
||||
// getOrCreateCompany — атомарная UPSERT, безопасна для параллельных вызовов.
|
||||
const company = await q.getOrCreateCompany(req.user.clientId, req.user.companyName);
|
||||
companyId = company.id;
|
||||
} else if (req.query.company) {
|
||||
// Администратор с необязательным фильтром по компании.
|
||||
const parsed = parseInt(req.query.company, 10);
|
||||
if (Number.isFinite(parsed) && parsed > 0) companyId = parsed;
|
||||
}
|
||||
// Администратор без ?company → companyId остаётся null → все компании.
|
||||
|
||||
const cidrs = await q.getExportCIDRs(companyId);
|
||||
|
||||
// Суммаризация: объединяем пересекающиеся и смежные диапазоны.
|
||||
// Например: 192.168.0.0/25 + 192.168.0.128/25 → 192.168.0.0/24
|
||||
const aggregated = aggregateCIDRs(cidrs);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user