From 503e07b500a8c41530faddbbb8d63c4f51fbfc70 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 26 Apr 2025 16:52:08 -0700 Subject: [PATCH] feat(security): added additional security measures to next config & middleware (#301) * feat(security): added additional security measures to next config & middleware * addressed PR comments --- sim/middleware.ts | 53 ++++++++++++++++++++-- sim/next.config.ts | 111 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 159 insertions(+), 5 deletions(-) diff --git a/sim/middleware.ts b/sim/middleware.ts index dd75261035..f31573d8a9 100644 --- a/sim/middleware.ts +++ b/sim/middleware.ts @@ -1,10 +1,21 @@ import { NextRequest, NextResponse } from 'next/server' import { getSessionCookie } from 'better-auth/cookies' import { verifyToken } from './lib/waitlist/token' +import { createLogger } from '@/lib/logs/console-logger' + +const logger = createLogger('Middleware') // Environment flag to check if we're in development mode const isDevelopment = process.env.NODE_ENV === 'development' +const SUSPICIOUS_UA_PATTERNS = [ + /^\s*$/, // Empty user agents + /\.\./, // Path traversal attempt + /<\s*script/i, // Potential XSS payloads + /^\(\)\s*{/, // Command execution attempt + /\b(sqlmap|nikto|gobuster|dirb|nmap)\b/i // Known scanning tools +] + export async function middleware(request: NextRequest) { // Check for active session const sessionCookie = getSessionCookie(request) @@ -66,7 +77,7 @@ export async function middleware(request: NextRequest) { return NextResponse.redirect(new URL('/', request.url)) } } catch (error) { - console.error('Token validation error:', error) + logger.error('Token validation error:', error) // In case of error, redirect signup attempts to home if (request.nextUrl.pathname === '/signup') { return NextResponse.redirect(new URL('/', request.url)) @@ -80,15 +91,49 @@ export async function middleware(request: NextRequest) { } } - return NextResponse.next() + const userAgent = request.headers.get('user-agent') || '' + + const isSuspicious = SUSPICIOUS_UA_PATTERNS.some(pattern => + pattern.test(userAgent) + ) + + if (isSuspicious) { + logger.warn('Blocked suspicious request', { + userAgent, + ip: request.headers.get('x-forwarded-for') || 'unknown', + url: request.url, + method: request.method, + pattern: SUSPICIOUS_UA_PATTERNS.find(pattern => pattern.test(userAgent))?.toString() + }) + + // Return 403 with security headers + return new NextResponse(null, { + status: 403, + statusText: 'Forbidden', + headers: { + 'Content-Type': 'text/plain', + 'X-Content-Type-Options': 'nosniff', + 'X-Frame-Options': 'DENY', + 'Content-Security-Policy': "default-src 'none'", + 'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + } + }) + } + + const response = NextResponse.next() + + response.headers.set('Vary', 'User-Agent') + + return response } -// Update matcher to include admin routes export const config = { matcher: [ '/w', // Match exactly /w '/w/:path*', // Match protected routes '/login', - '/signup', + '/signup' ], } diff --git a/sim/next.config.ts b/sim/next.config.ts index 75055fb899..6b3484e341 100644 --- a/sim/next.config.ts +++ b/sim/next.config.ts @@ -5,6 +5,11 @@ const isStandaloneBuild = process.env.USE_LOCAL_STORAGE === 'true' const nextConfig: NextConfig = { devIndicators: false, + experimental: { + sri: { + algorithm: 'sha256' + } + }, images: { domains: [ 'avatars.githubusercontent.com', @@ -35,7 +40,7 @@ const nextConfig: NextConfig = { async headers() { return [ { - // API routes CORS headers + // API routes CORS headers - keep no-cache for dynamic API endpoints source: '/api/:path*', headers: [ { key: 'Access-Control-Allow-Credentials', value: 'true' }, @@ -52,6 +57,110 @@ const nextConfig: NextConfig = { value: 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version', }, + { + key: 'Cache-Control', + value: 'no-store, no-cache, must-revalidate, proxy-revalidate', + }, + { + key: 'Pragma', + value: 'no-cache', + }, + { + key: 'Expires', + value: '0', + }, + { + key: 'Surrogate-Control', + value: 'no-store', + }, + ], + }, + { + // Static assets - long caching for better performance + // This targets common static file extensions + source: '/:path*.(js|css|svg|png|jpg|jpeg|gif|webp|avif|ico|woff|woff2|ttf|eot)', + headers: [ + { + key: 'Cache-Control', + value: 'public, max-age=31536000, immutable', + }, + { + key: 'Vary', + value: 'User-Agent', + }, + ], + }, + { + // HTML/dynamic content - use validation caching instead of no-cache + source: '/:path*', + has: [ + { + type: 'header', + key: 'Accept', + value: '(.*text/html.*)', + }, + ], + headers: [ + { + key: 'X-Content-Type-Options', + value: 'nosniff', + }, + { + key: 'X-Frame-Options', + value: 'SAMEORIGIN', + }, + { + key: 'Content-Security-Policy', + value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; form-action 'self'; base-uri 'self'; object-src 'none'", + }, + { + key: 'Cache-Control', + value: 'public, max-age=0, must-revalidate', + }, + { + key: 'Vary', + value: 'User-Agent', + }, + ], + }, + { + // Apply security headers to all routes + source: '/:path*', + headers: [ + { + key: 'X-Content-Type-Options', + value: 'nosniff', + }, + { + key: 'X-Frame-Options', + value: 'SAMEORIGIN', + }, + { + key: 'Content-Security-Policy', + value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; form-action 'self'; base-uri 'self'; object-src 'none'", + }, + ], + }, + { + // Dynamic routes containing user data - strict no caching + source: '/w/:path*', + headers: [ + { + key: 'Cache-Control', + value: 'private, no-store, no-cache, must-revalidate, proxy-revalidate', + }, + { + key: 'Pragma', + value: 'no-cache', + }, + { + key: 'Expires', + value: '0', + }, + { + key: 'Surrogate-Control', + value: 'no-store', + }, ], }, {