refactor: шаг 3 — CSRF-инициализация вынесена в src/middleware/csrf.js
This commit is contained in:
@@ -4,7 +4,8 @@ const path = require('path');
|
|||||||
const helmet = require('helmet');
|
const helmet = require('helmet');
|
||||||
// rateLimit.js: mutationLimiter (POST-роуты) и exportLimiter (GET /export)
|
// rateLimit.js: mutationLimiter (POST-роуты) и exportLimiter (GET /export)
|
||||||
const { mutationLimiter, exportLimiter } = require('./src/middleware/rateLimit');
|
const { mutationLimiter, exportLimiter } = require('./src/middleware/rateLimit');
|
||||||
const { doubleCsrf } = require('csrf-csrf');
|
// csrf.js: фабрика для инициализации CSRF-защиты (double-submit cookie pattern)
|
||||||
|
const { initCsrf } = require('./src/middleware/csrf');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
const { checkConnection } = require('./src/db');
|
const { checkConnection } = require('./src/db');
|
||||||
const { initAuth, requireAdmin } = require('./src/auth');
|
const { initAuth, requireAdmin } = require('./src/auth');
|
||||||
@@ -31,21 +32,11 @@ app.use(cookieParser());
|
|||||||
async function start() {
|
async function start() {
|
||||||
const auth = await initAuth();
|
const auth = await initAuth();
|
||||||
|
|
||||||
// ── CSRF защита (double-submit cookie pattern) ─────────────────────────────
|
// ── CSRF защита ────────────────────────────────────────────────────────────
|
||||||
// getSecret: секрет для HMAC подписи CSRF-токена. В проде берётся из env.
|
// initCsrf() читает CSRF_SECRET и NODE_ENV из process.env.
|
||||||
// getSessionIdentifier: привязывает токен к конкретной сессии (JWT cookie).
|
// Возвращает: doubleCsrfProtection (middleware на POST) и
|
||||||
// Если пользователь не залогинен (пустая строка) — токен не привязан к сессии,
|
// generateCsrfToken (вызов в GET-роутах для передачи токена в шаблон).
|
||||||
// но это допустимо для login-формы (mock-auth).
|
const { doubleCsrfProtection, generateCsrfToken } = initCsrf();
|
||||||
const { doubleCsrfProtection, generateCsrfToken } = doubleCsrf({
|
|
||||||
getSecret: () => process.env.CSRF_SECRET || 'dev-csrf-secret-change-in-prod',
|
|
||||||
getSessionIdentifier: (req) => req.cookies.jwt || '',
|
|
||||||
cookieOptions: {
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'lax',
|
|
||||||
httpOnly: true,
|
|
||||||
},
|
|
||||||
size: 64, // длина токена в байтах
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Публичные маршруты (до auth.middleware) ──────────────────────────────────
|
// ── Публичные маршруты (до auth.middleware) ──────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* src/middleware/csrf.js — инициализация CSRF-защиты.
|
||||||
|
*
|
||||||
|
* Используется паттерн «double-submit cookie»:
|
||||||
|
* 1. Сервер генерирует подписанный CSRF-токен и кладёт его в cookie.
|
||||||
|
* 2. Шаблон (EJS) вставляет токен в скрытое поле формы (<input name="_csrf">).
|
||||||
|
* 3. При POST-запросе doubleCsrfProtection сравнивает cookie и поле формы.
|
||||||
|
* 4. Если они не совпадают или токен протух — 403 Forbidden.
|
||||||
|
*
|
||||||
|
* Почему не стандартный csurf:
|
||||||
|
* csurf устарел (deprecated), csrf-csrf — его современная замена для Express 5+.
|
||||||
|
*
|
||||||
|
* Функция initCsrf() возвращает два объекта:
|
||||||
|
* - doubleCsrfProtection — middleware, вешается на POST-роуты
|
||||||
|
* - generateCsrfToken(req, res) — вызывается в GET-роутах для получения токена
|
||||||
|
* и передачи его в шаблон через csrfToken.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { doubleCsrf } = require('csrf-csrf');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создаёт и возвращает CSRF middleware и генератор токенов.
|
||||||
|
*
|
||||||
|
* Вызывается один раз при старте приложения (в server.js → start()).
|
||||||
|
* Параметры берутся из process.env:
|
||||||
|
* - CSRF_SECRET: секрет для HMAC подписи. В dev используется дефолтное
|
||||||
|
* значение — в продакшене ОБЯЗАТЕЛЬНО задать через env!
|
||||||
|
* - NODE_ENV: если 'production' — cookie выставляется с флагом secure (HTTPS only).
|
||||||
|
*
|
||||||
|
* @returns {{ doubleCsrfProtection: Function, generateCsrfToken: Function }}
|
||||||
|
*/
|
||||||
|
function initCsrf() {
|
||||||
|
const { doubleCsrfProtection, generateCsrfToken } = doubleCsrf({
|
||||||
|
// Секрет для HMAC-подписи токена.
|
||||||
|
// ⚠️ В продакшене задать CSRF_SECRET в .env или через k8s Secret.
|
||||||
|
getSecret: () => process.env.CSRF_SECRET || 'dev-csrf-secret-change-in-prod',
|
||||||
|
|
||||||
|
// Идентификатор сессии — привязывает CSRF-токен к конкретному пользователю.
|
||||||
|
// Используем JWT-cookie: если его нет (не залогинен) — пустая строка,
|
||||||
|
// что допустимо для формы /login (она сама создаёт сессию).
|
||||||
|
getSessionIdentifier: (req) => req.cookies.jwt || '',
|
||||||
|
|
||||||
|
cookieOptions: {
|
||||||
|
// secure: true → cookie передаётся только по HTTPS. Важно в продакшене.
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
sameSite: 'lax', // lax: cookie отправляется при навигации, но не из iframe
|
||||||
|
httpOnly: true, // cookie недоступна из JS (защита от XSS-кражи CSRF-токена)
|
||||||
|
},
|
||||||
|
|
||||||
|
size: 64, // длина случайной части токена в байтах (достаточно для безопасности)
|
||||||
|
});
|
||||||
|
|
||||||
|
return { doubleCsrfProtection, generateCsrfToken };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { initCsrf };
|
||||||
Reference in New Issue
Block a user