Files
telegram-shop/src/admin/csrf.js

76 lines
2.6 KiB
JavaScript

import crypto from 'crypto';
import logger from '../utils/logger.js';
const CSRF_COOKIE = '_csrf';
const CSRF_HEADER = 'x-csrf-token';
function generateToken() {
return crypto.randomBytes(32).toString('hex');
}
export function csrfMiddleware(req, res, next) {
let token = req.cookies?.[CSRF_COOKIE];
if (!token) {
token = generateToken();
res.cookie(CSRF_COOKIE, token, {
httpOnly: false,
sameSite: false,
path: '/'
});
}
res.locals.csrfToken = token;
next();
}
export function validateCsrf(req, res, next) {
// Only validate state-changing methods
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next();
// Skip multipart/form-data — multer hasn't parsed the body yet.
// Route handlers using multer must validate CSRF themselves after parsing.
const ct = req.headers['content-type'] || '';
if (ct.startsWith('multipart/form-data')) return next();
const token = req.body?._csrf || req.headers[CSRF_HEADER];
const cookieToken = req.cookies?.[CSRF_COOKIE];
if (!token || !cookieToken) {
logger.warn({ ip: req.ip, url: req.originalUrl, method: req.method, hasBodyToken: !!token, hasCookieToken: !!cookieToken, contentType: ct }, 'CSRF token missing');
return res.status(403).send('CSRF token missing. Please reload the page.');
}
const provided = Buffer.from(token, 'utf8');
const expected = Buffer.from(cookieToken, 'utf8');
if (provided.length !== expected.length || !crypto.timingSafeEqual(provided, expected)) {
logger.warn({ ip: req.ip, url: req.originalUrl, method: req.method }, 'CSRF token mismatch');
return res.status(403).send('CSRF token invalid. Please reload the page.');
}
next();
}
/**
* Validate CSRF token from req.body._csrf (for use inside multer handlers).
* Returns true if valid, false otherwise. Sends 403 response on failure.
*/
export function validateCsrfFromBody(req, res) {
const token = req.body?._csrf;
const cookieToken = req.cookies?.[CSRF_COOKIE];
if (!token || !cookieToken) {
logger.warn({ ip: req.ip, url: req.originalUrl, method: req.method }, 'CSRF token missing (multipart)');
res.status(403).send('CSRF token missing. Please reload the page.');
return false;
}
const provided = Buffer.from(token, 'utf8');
const expected = Buffer.from(cookieToken, 'utf8');
if (provided.length !== expected.length || !crypto.timingSafeEqual(provided, expected)) {
logger.warn({ ip: req.ip, url: req.originalUrl, method: req.method }, 'CSRF token mismatch (multipart)');
res.status(403).send('CSRF token invalid. Please reload the page.');
return false;
}
return true;
}