diff --git a/src/lib/apiClient.ts b/src/lib/apiClient.ts new file mode 100644 index 0000000..bf1daec --- /dev/null +++ b/src/lib/apiClient.ts @@ -0,0 +1,42 @@ +/** + * Utility tool to handle CSRF tokens and safe data-mutating network configurations + */ + +// Helper to securely extract cookie string tokens browser-side +function getCookieValue(cookieName: string): string | null { + if (typeof document === 'undefined') return null; + const match = document.cookie.match(new RegExp('(^| )' + cookieName + '=([^;]+)')); + return match ? decodeURIComponent(match[2]) : null; +} + +// Helper to generate a random cryptographic token string if missing +function generateSecureToken(): string { + return Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2); +} + +/** + * Custom Fetch Wrapper that automatically injects CSRF headers for mutating requests + */ +export async function secureFetch(url: string, options: RequestInit = {}): Promise { + const method = options.method?.toUpperCase() || 'GET'; + const mutatingMethods = ['POST', 'PUT', 'DELETE', 'PATCH']; + + // Initialize or maintain client-side tracking tokens if missing + if (typeof document !== 'undefined' && !getCookieValue('csrfToken')) { + const freshToken = generateSecureToken(); + document.cookie = `csrfToken=${freshToken}; path=/; max-age=3600; SameSite=Strict; Secure`; + } + + const token = getCookieValue('csrfToken'); + const secureHeaders = new Headers(options.headers || {}); + + // Automatically attach the required token to the matching header fields + if (mutatingMethods.includes(method) && token) { + secureHeaders.set('x-csrf-token', token); + } + + return fetch(url, { + ...options, + headers: secureHeaders, + }); +} diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..db46b2d --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,27 @@ +// Crucial Security Patch: Implement CSRF protection for state-mutating requests (#359) +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; + +export async function middleware(request: NextRequest) { + const mutatingMethods = ['POST', 'PUT', 'DELETE', 'PATCH']; + + // Check if the request is trying to mutate data + if (mutatingMethods.includes(request.method)) { + const csrfTokenHeader = request.headers.get('x-csrf-token'); + const csrfCookieToken = request.cookies.get('csrfToken')?.value; + + // Block request if tokens are missing or do not match + if (!csrfTokenHeader || !csrfCookieToken || csrfTokenHeader !== csrfCookieToken) { + return new NextResponse( + JSON.stringify({ success: false, message: 'CSRF token validation failed.' }), + { status: 403, headers: { 'content-type': 'application/json' } } + ); + } + } + return NextResponse.next(); +} + +// Config to apply this middleware across all API routes +export const config = { + matcher: '/api/:path*', +};