v0.5.173: OIDC state всегда, CSRF смонтирован, session.regenerate
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ipwhitelist",
|
"name": "ipwhitelist",
|
||||||
"version": "0.5.172",
|
"version": "0.5.173",
|
||||||
"description": "IP WhiteList microservice for cloud provider",
|
"description": "IP WhiteList microservice for cloud provider",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const { authLimiter } = require('./src/middleware/rateLimit');
|
|||||||
const { createSessionMiddleware } = require('./src/middleware/session');
|
const { createSessionMiddleware } = require('./src/middleware/session');
|
||||||
const { aggregateCIDRs } = require('./src/validators');
|
const { aggregateCIDRs } = require('./src/validators');
|
||||||
const { CSP_DIRECTIVES } = require('./src/middleware/csp');
|
const { CSP_DIRECTIVES } = require('./src/middleware/csp');
|
||||||
|
const { initCsrf } = require('./src/middleware/csrf');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
@@ -85,12 +86,15 @@ async function start() {
|
|||||||
const { createRouter: createOidcRouter } = require('./src/routes/oidc');
|
const { createRouter: createOidcRouter } = require('./src/routes/oidc');
|
||||||
app.use('/', createOidcRouter({ auth }));
|
app.use('/', createOidcRouter({ auth }));
|
||||||
|
|
||||||
|
// ── CSRF — инициализация после сессий ─────────────────────────────────
|
||||||
|
const { generateCsrfToken, doubleCsrfProtection } = initCsrf();
|
||||||
|
|
||||||
// ── V2 — основной код ─────────────────────────────────────────────────
|
// ── V2 — основной код ─────────────────────────────────────────────────
|
||||||
app.get('/', (req, res) => res.redirect('/v2/app'));
|
app.get('/', (req, res) => res.redirect('/v2/app'));
|
||||||
// Редирект /export → /v2/export (для обратной совместимости)
|
// Редирект /export → /v2/export (для обратной совместимости)
|
||||||
app.get('/export', (req, res) => res.redirect('/v2/export' + (req.originalUrl.includes('?') ? req.originalUrl.substring(req.originalUrl.indexOf('?')) : '')));
|
app.get('/export', (req, res) => res.redirect('/v2/export' + (req.originalUrl.includes('?') ? req.originalUrl.substring(req.originalUrl.indexOf('?')) : '')));
|
||||||
const { createV2Router } = require('./v2/server');
|
const { createV2Router } = require('./v2/server');
|
||||||
app.use('/v2', createV2Router());
|
app.use('/v2', createV2Router({ generateCsrfToken, doubleCsrfProtection }));
|
||||||
|
|
||||||
// ── Общий обработчик ошибок ───────────────────────────────────────────────
|
// ── Общий обработчик ошибок ───────────────────────────────────────────────
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
|||||||
+6
-5
@@ -32,15 +32,11 @@ function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, authLimit
|
|||||||
router.get('/callback', async (req, res) => {
|
router.get('/callback', async (req, res) => {
|
||||||
const { code, state } = req.query;
|
const { code, state } = req.query;
|
||||||
|
|
||||||
// Проверка state — защита от CSRF в OAuth-потоке
|
// Проверка state — защита от CSRF в OAuth-потоке (всегда)
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
if (req.session) delete req.session.oidcState;
|
|
||||||
} else {
|
|
||||||
if (!state || state !== req.session.oidcState) {
|
if (!state || state !== req.session.oidcState) {
|
||||||
return res.status(403).send('Invalid state parameter');
|
return res.status(403).send('Invalid state parameter');
|
||||||
}
|
}
|
||||||
delete req.session.oidcState;
|
delete req.session.oidcState;
|
||||||
}
|
|
||||||
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return res.status(400).send('Missing authorization code');
|
return res.status(400).send('Missing authorization code');
|
||||||
@@ -57,6 +53,11 @@ function createRouter({ auth, doubleCsrfProtection, generateCsrfToken, authLimit
|
|||||||
const payload = jwt.decode(accessToken);
|
const payload = jwt.decode(accessToken);
|
||||||
if (!payload) throw new Error('Failed to decode token');
|
if (!payload) throw new Error('Failed to decode token');
|
||||||
|
|
||||||
|
// Регенерируем ID сессии — защита от session fixation
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
req.session.regenerate(err => err ? reject(err) : resolve());
|
||||||
|
});
|
||||||
|
|
||||||
req.session.token = accessToken;
|
req.session.token = accessToken;
|
||||||
req.session.idToken = tokenData.idToken || null;
|
req.session.idToken = tokenData.idToken || null;
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -32,7 +32,7 @@ const { createAdminRouter } = require('./src/admin');
|
|||||||
const { pool } = require('./src/db');
|
const { pool } = require('./src/db');
|
||||||
const { ensureSchema } = require('./src/db/schema');
|
const { ensureSchema } = require('./src/db/schema');
|
||||||
|
|
||||||
function createV2Router() {
|
function createV2Router({ generateCsrfToken, doubleCsrfProtection } = {}) {
|
||||||
// Автосоздание таблиц v2_companies, v2_entries, v2_audit при старте
|
// Автосоздание таблиц v2_companies, v2_entries, v2_audit при старте
|
||||||
ensureSchema(pool).catch(e => console.error('[v2:db] Schema error:', e.message));
|
ensureSchema(pool).catch(e => console.error('[v2:db] Schema error:', e.message));
|
||||||
|
|
||||||
@@ -102,10 +102,10 @@ function createV2Router() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── /v2/app — пользовательский CRUD (только с сессией) ──────────────
|
// ── /v2/app — пользовательский CRUD (только с сессией) ──────────────
|
||||||
router.use('/app', enhanceImpersonation, resolveContext, createUserRouter());
|
router.use('/app', enhanceImpersonation, resolveContext, doubleCsrfProtection || ((req,res,next)=>next()), createUserRouter({ generateCsrfToken }));
|
||||||
|
|
||||||
// ── /v2/admin — админка: лимиты + аудит ──────────────────────────
|
// ── /v2/admin — админка: лимиты + аудит ──────────────────────────
|
||||||
router.use('/admin', enhanceImpersonation, resolveContext, createAdminRouter());
|
router.use('/admin', enhanceImpersonation, resolveContext, doubleCsrfProtection || ((req,res,next)=>next()), createAdminRouter({ generateCsrfToken }));
|
||||||
|
|
||||||
// ── GET /v2/ — редирект на /v2/app ──────────────────────────────────
|
// ── GET /v2/ — редирект на /v2/app ──────────────────────────────────
|
||||||
router.get('/', (req, res) => res.redirect('/v2/app'));
|
router.get('/', (req, res) => res.redirect('/v2/app'));
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
version: '0.5.172',
|
version: '0.5.173',
|
||||||
|
|
||||||
// ── IAM ──────────────────────────────────────────────────────────────────
|
// ── IAM ──────────────────────────────────────────────────────────────────
|
||||||
iamUrl: process.env.V2_IAM_URL || 'https://auth-api.ngcloud.ru/api/v1/auth/user',
|
iamUrl: process.env.V2_IAM_URL || 'https://auth-api.ngcloud.ru/api/v1/auth/user',
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const crud = require('../crud');
|
|||||||
const q = require('../db/queries');
|
const q = require('../db/queries');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
|
|
||||||
function createUserRouter() {
|
function createUserRouter({ generateCsrfToken } = {}) {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// ── GET / — список записей + форма ──────────────────────────────────
|
// ── GET / — список записей + форма ──────────────────────────────────
|
||||||
@@ -84,6 +84,7 @@ function createUserRouter() {
|
|||||||
user: templateUser,
|
user: templateUser,
|
||||||
includeDeleted,
|
includeDeleted,
|
||||||
version: config.version,
|
version: config.version,
|
||||||
|
csrfToken: generateCsrfToken ? generateCsrfToken(req, res) : '',
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send('<h2>Ошибка</h2><pre>' + e.message + '</pre><a href="/v2/app">Назад</a>');
|
res.status(500).send('<h2>Ошибка</h2><pre>' + e.message + '</pre><a href="/v2/app">Назад</a>');
|
||||||
|
|||||||
@@ -188,6 +188,7 @@
|
|||||||
<div class="card-header">Добавить адрес</div>
|
<div class="card-header">Добавить адрес</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST" action="/v2/app/add">
|
<form method="POST" action="/v2/app/add">
|
||||||
|
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>IPv4 адрес или подсеть CIDR</label>
|
<label>IPv4 адрес или подсеть CIDR</label>
|
||||||
@@ -233,6 +234,7 @@
|
|||||||
<td style="text-align:center;white-space:nowrap;">
|
<td style="text-align:center;white-space:nowrap;">
|
||||||
<% if (!e.deleted_at) { %>
|
<% if (!e.deleted_at) { %>
|
||||||
<form method="POST" action="/v2/app/edit/<%= e.id %>" style="display:inline;">
|
<form method="POST" action="/v2/app/edit/<%= e.id %>" style="display:inline;">
|
||||||
|
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
|
||||||
<input name="cidr" value="<%= e.value_cidr %>" size="18" style="width:125px;padding:.2rem .3rem;border:1px solid var(--border);border-radius:4px;font-size:.78rem;">
|
<input name="cidr" value="<%= e.value_cidr %>" size="18" style="width:125px;padding:.2rem .3rem;border:1px solid var(--border);border-radius:4px;font-size:.78rem;">
|
||||||
<input name="comment" value="<%= e.comment || '' %>" size="30" style="width:200px;padding:.2rem .3rem;border:1px solid var(--border);border-radius:4px;font-size:.78rem;">
|
<input name="comment" value="<%= e.comment || '' %>" size="30" style="width:200px;padding:.2rem .3rem;border:1px solid var(--border);border-radius:4px;font-size:.78rem;">
|
||||||
<button class="btn btn-sm" title="Сохранить">💾</button>
|
<button class="btn btn-sm" title="Сохранить">💾</button>
|
||||||
@@ -244,11 +246,13 @@
|
|||||||
<td style="text-align:center;">
|
<td style="text-align:center;">
|
||||||
<% if (!e.deleted_at) { %>
|
<% if (!e.deleted_at) { %>
|
||||||
<form method="POST" action="/v2/app/delete/<%= e.id %>" style="display:inline;">
|
<form method="POST" action="/v2/app/delete/<%= e.id %>" style="display:inline;">
|
||||||
|
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
|
||||||
<input type="hidden" name="deleted" value="<%= includeDeleted ? '1' : '0' %>">
|
<input type="hidden" name="deleted" value="<%= includeDeleted ? '1' : '0' %>">
|
||||||
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Удалить?')" title="Удалить">🗑️</button>
|
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Удалить?')" title="Удалить">🗑️</button>
|
||||||
</form>
|
</form>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<form method="POST" action="/v2/app/restore/<%= e.id %>" style="display:inline;">
|
<form method="POST" action="/v2/app/restore/<%= e.id %>" style="display:inline;">
|
||||||
|
<input type="hidden" name="_csrf" value="<%= csrfToken %>">
|
||||||
<input type="hidden" name="deleted" value="<%= includeDeleted ? '1' : '0' %>">
|
<input type="hidden" name="deleted" value="<%= includeDeleted ? '1' : '0' %>">
|
||||||
<button type="submit" class="btn btn-sm" title="Восстановить" style="color:var(--green);border-color:var(--green);">↺</button>
|
<button type="submit" class="btn btn-sm" title="Восстановить" style="color:var(--green);border-color:var(--green);">↺</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user