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:
@@ -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 {}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user