diff --git a/server.js b/server.js index 0083437..14982a1 100644 --- a/server.js +++ b/server.js @@ -4,7 +4,8 @@ const path = require('path'); const helmet = require('helmet'); // rateLimit.js: mutationLimiter (POST-роуты) и exportLimiter (GET /export) 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(); const { checkConnection } = require('./src/db'); const { initAuth, requireAdmin } = require('./src/auth'); @@ -31,21 +32,11 @@ app.use(cookieParser()); async function start() { const auth = await initAuth(); - // ── CSRF защита (double-submit cookie pattern) ───────────────────────────── - // getSecret: секрет для HMAC подписи CSRF-токена. В проде берётся из env. - // getSessionIdentifier: привязывает токен к конкретной сессии (JWT cookie). - // Если пользователь не залогинен (пустая строка) — токен не привязан к сессии, - // но это допустимо для login-формы (mock-auth). - 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, // длина токена в байтах - }); + // ── CSRF защита ──────────────────────────────────────────────────────────── + // initCsrf() читает CSRF_SECRET и NODE_ENV из process.env. + // Возвращает: doubleCsrfProtection (middleware на POST) и + // generateCsrfToken (вызов в GET-роутах для передачи токена в шаблон). + const { doubleCsrfProtection, generateCsrfToken } = initCsrf(); // ── Публичные маршруты (до auth.middleware) ────────────────────────────────── diff --git a/src/middleware/csrf.js b/src/middleware/csrf.js new file mode 100644 index 0000000..bd85572 --- /dev/null +++ b/src/middleware/csrf.js @@ -0,0 +1,58 @@ +/** + * src/middleware/csrf.js — инициализация CSRF-защиты. + * + * Используется паттерн «double-submit cookie»: + * 1. Сервер генерирует подписанный CSRF-токен и кладёт его в cookie. + * 2. Шаблон (EJS) вставляет токен в скрытое поле формы (). + * 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 };