/
zxclovly
/
LUME
Обзор
Документация
Войти
/
zxclovly
/
LUME
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
backend/src/csrf.js
182 строки
7 KB
lovlygod
feat: add backend
18 мар 2026, 08:25
18 мар 2026, 08:25
e42f46a
Код
Авторство
О чём код?
/** * CSRF Protection Middleware для LUME * Рспользует Double Submit Cookie pattern */ const crypto = require('crypto'); // Токены для сессий (РІ памяти для production лучше использовать Redis) const csrfTokens = new Map(); /** * Генерация CSRF токена */ const generateCSRFToken = () => { return crypto.randomBytes(32).toString('hex'); }; /** * Middleware для генерации CSRF токена * Добавляет токен РІ cookie Рё делает доступным через res.locals */ const generateCSRFTokenMiddleware = (req, res, next) => { // Генерируем токен если нет let token = req.cookies?.csrfToken; if (!token) { token = generateCSRFToken(); } const isProd = process.env.NODE_ENV === 'production'; const isCrossSite = Boolean(process.env.FRONTEND_URL) || Boolean(process.env.FRONTEND_URLS); const cookieSecure = isCrossSite || isProd; const cookieSameSite = isCrossSite ? 'none' : (isProd ? 'strict' : 'lax'); // Устанавливаем cookie (httpOnly: false, чтобы JS РјРѕРі читать) res.cookie('csrfToken', token, { httpOnly: false, // JS должен иметСФдоступ secure: cookieSecure, sameSite: cookieSameSite, maxAge: 24 * 60 * 60 * 1000, // 24 часа path: '/', }); // Сохраняем токен для сессии const sessionId = req.sessionID || req.cookies?.sessionId || 'anonymous'; csrfTokens.set(sessionId, token); next(); }; /** * Middleware для проверки CSRF токена * Должен использоваться для всех state-changing операций (POST, PUT, DELETE, PATCH) */ const verifyCSRFToken = (req, res, next) => { // Пропускаем для GET запросов if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) { return next(); } // Пропускаем для login/register (там ещё нет сессии) // req.path РЅРµ включает префикс роутера (/api) if (['/login', '/register', '/refresh'].includes(req.path)) { return next(); } const tokenFromCookie = req.cookies?.csrfToken; const tokenFromHeader = req.headers['x-csrf-token']; const tokenFromBody = req.body?._csrf; // Токен должен быть либо РІ заголовке, либо РІ теле запроса const token = tokenFromHeader || tokenFromBody || tokenFromCookie; if (!token) { return res.status(403).json({ error: { message: 'CSRF token missing', code: 'CSRF_TOKEN_MISSING', statusCode: 403, }, }); } if (!tokenFromCookie) { return res.status(403).json({ error: { message: 'CSRF cookie missing', code: 'CSRF_COOKIE_MISSING', statusCode: 403, }, }); } // Сравниваем токен РёР· заголовка/тела СЃ токеном РёР· cookie if (token !== tokenFromCookie) { return res.status(403).json({ error: { message: 'CSRF token mismatch', code: 'CSRF_TOKEN_MISMATCH', statusCode: 403, }, }); } // Проверяем токен РІ хранилище сессий const sessionId = req.sessionID || req.cookies?.sessionId || 'anonymous'; const storedToken = csrfTokens.get(sessionId); if (storedToken && storedToken !== token) { return res.status(403).json({ error: { message: 'Invalid CSRF session token', code: 'CSRF_INVALID_SESSION', statusCode: 403, }, }); } next(); }; /** * Middleware для получения РЅРѕРІРѕРіРѕ CSRF токена * РњРѕР¶РЅРѕ вызвать через GET /api/csrf-token */ const refreshCSRFToken = (req, res) => { const newToken = generateCSRFToken(); const isProd = process.env.NODE_ENV === 'production'; const isCrossSite = Boolean(process.env.FRONTEND_URL) || Boolean(process.env.FRONTEND_URLS); const cookieSecure = isCrossSite || isProd; const cookieSameSite = isCrossSite ? 'none' : (isProd ? 'strict' : 'lax'); res.cookie('csrfToken', newToken, { httpOnly: false, secure: cookieSecure, sameSite: cookieSameSite, maxAge: 24 * 60 * 60 * 1000, path: '/', }); const sessionId = req.sessionID || req.cookies?.sessionId || 'anonymous'; csrfTokens.set(sessionId, newToken); res.json({ csrfToken: newToken }); }; /** * Очистка CSRF токена РїСЂРё logout */ const clearCSRFToken = (req, res, next) => { const sessionId = req.sessionID || req.cookies?.sessionId || 'anonymous'; csrfTokens.delete(sessionId); res.clearCookie('csrfToken', { path: '/' }); next(); }; /** * Очистка старых токенов (каждые 5 РјРёРЅСѓС‚) */ setInterval(() => { // Очищаем токены старше 1 часа const now = Date.now(); const oneHour = 60 * 60 * 1000; // Р’ production лучше использовать Redis СЃ TTL // Для простоты просто ограничиваем размер Map if (csrfTokens.size > 10000) { const entries = Array.from(csrfTokens.entries()); const toDelete = entries.slice(0, entries.length - 5000); toDelete.forEach(([key]) => csrfTokens.delete(key)); } }, 5 * 60 * 1000); module.exports = { generateCSRFTokenMiddleware, verifyCSRFToken, refreshCSRFToken, clearCSRFToken, };