Import source: app/backend/src/plugins/csrf

Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
arsam
2026-09-18 23:19:43 +03:30
parent ad99a85a16
commit a690c1bc2c
+62
View File
@@ -0,0 +1,62 @@
import fp from 'fastify-plugin';
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { redis } from '../redis.js';
import { logSecurityEvent } from '../lib/security-audit.js';
import { sessionKey, CSRF_COOKIE, csrfCookieOpts } from './session.js';
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
const EXEMPT_PATHS = new Set([
'/api/v1/auth/login',
'/api/v1/auth/signup',
'/api/v1/auth/forgot-password',
'/api/v1/auth/reset-password',
'/api/v1/auth/otp/send',
'/api/v1/auth/otp/verify',
'/api/v1/auth/2fa/verify',
'/api/v1/newsletter',
'/api/v1/analytics/events',
'/api/v1/errors',
'/api/v1/jobs',
'/api/v1/csp-report',
]);
const EXEMPT_PREFIX = '/api/v1/auth/oauth/';
export default fp(async function csrfPlugin(app: FastifyInstance) {
app.addHook('preHandler', async (req: FastifyRequest, reply: FastifyReply) => {
if (SAFE_METHODS.has(req.method)) return;
const path = req.url.split('?')[0];
if (EXEMPT_PATHS.has(path) || path.startsWith(EXEMPT_PREFIX)) return;
if (!req.session) {
return reply.code(401).send({ error: 'unauthenticated' });
}
const headerToken = req.headers['x-csrf-token'];
if (typeof headerToken !== 'string' || headerToken.length === 0) {
await logSecurityEvent({ type: 'csrf_failure', userId: req.session.userId, ip: req.ip, userAgent: req.headers['user-agent'], metadata: { reason: 'missing' } });
return reply.code(403).send({ error: 'csrf_missing' });
}
if (headerToken !== req.session.csrfToken) {
await logSecurityEvent({ type: 'csrf_failure', userId: req.session.userId, ip: req.ip, userAgent: req.headers['user-agent'], metadata: { reason: 'mismatch' } });
return reply.code(403).send({ error: 'csrf_mismatch' });
}
});
app.addHook('onResponse', async (req: FastifyRequest, reply: FastifyReply) => {
if (SAFE_METHODS.has(req.method)) return;
if (!req.session?.sid || reply.statusCode >= 400) return;
const { randomBytes } = await import('node:crypto');
const newToken = randomBytes(16).toString('base64url');
const raw = await redis.get(sessionKey(req.session.sid));
if (!raw) return;
try {
const data = JSON.parse(raw);
data.csrfToken = newToken;
await redis.set(sessionKey(req.session.sid), JSON.stringify(data));
reply.setCookie(CSRF_COOKIE, newToken, { ...csrfCookieOpts, maxAge: 14 * 24 * 60 * 60 });
reply.header('X-CSRF-Token', newToken);
} catch {}
});
});