From 3b122251ae554421b295575295813a60ba5f3213 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 26 Apr 2025 12:43:02 -0700 Subject: [PATCH 1/2] feat(security): added additional security measures to next config & middleware --- sim/middleware.ts | 33 +++++++++++++++++-- sim/next.config.ts | 82 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 3 deletions(-) diff --git a/sim/middleware.ts b/sim/middleware.ts index dd752610351..5333b27c04d 100644 --- a/sim/middleware.ts +++ b/sim/middleware.ts @@ -5,6 +5,14 @@ import { verifyToken } from './lib/waitlist/token' // 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) @@ -80,15 +88,34 @@ 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) { + return new NextResponse(null, { + status: 403, + statusText: 'Forbidden', + headers: { + 'Content-Type': 'text/plain' + } + }) + } + + 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 75055fb899d..cffde193069 100644 --- a/sim/next.config.ts +++ b/sim/next.config.ts @@ -5,6 +5,12 @@ const isStandaloneBuild = process.env.USE_LOCAL_STORAGE === 'true' const nextConfig: NextConfig = { devIndicators: false, + crossOrigin: 'anonymous', + experimental: { + sri: { + algorithm: 'sha256' + } + }, images: { domains: [ 'avatars.githubusercontent.com', @@ -52,6 +58,82 @@ 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', + }, + ], + }, + { + // 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'", + }, + { + 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', + }, + { + key: 'Vary', + value: 'User-Agent', + }, + ], + }, + { + // 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', + }, ], }, { From 99ec56aef71ecdf076fe82ed433e98c1d0a64f21 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 26 Apr 2025 16:44:35 -0700 Subject: [PATCH 2/2] addressed PR comments --- sim/middleware.ts | 22 ++++++++++++++++++-- sim/next.config.ts | 51 +++++++++++++++++++++++++++++++++++----------- 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/sim/middleware.ts b/sim/middleware.ts index 5333b27c04d..f31573d8a99 100644 --- a/sim/middleware.ts +++ b/sim/middleware.ts @@ -1,6 +1,9 @@ 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' @@ -74,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)) @@ -95,11 +98,26 @@ export async function middleware(request: NextRequest) { ) 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' + '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' } }) } diff --git a/sim/next.config.ts b/sim/next.config.ts index cffde193069..6b3484e3414 100644 --- a/sim/next.config.ts +++ b/sim/next.config.ts @@ -5,7 +5,6 @@ const isStandaloneBuild = process.env.USE_LOCAL_STORAGE === 'true' const nextConfig: NextConfig = { devIndicators: false, - crossOrigin: 'anonymous', experimental: { sri: { algorithm: 'sha256' @@ -41,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' }, @@ -77,8 +76,30 @@ const nextConfig: NextConfig = { ], }, { - // Apply security headers to all routes + // 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', @@ -94,23 +115,29 @@ const nextConfig: NextConfig = { }, { key: 'Cache-Control', - value: 'no-store, no-cache, must-revalidate, proxy-revalidate', + value: 'public, max-age=0, must-revalidate', }, { - key: 'Pragma', - value: 'no-cache', + key: 'Vary', + value: 'User-Agent', }, + ], + }, + { + // Apply security headers to all routes + source: '/:path*', + headers: [ { - key: 'Expires', - value: '0', + key: 'X-Content-Type-Options', + value: 'nosniff', }, { - key: 'Surrogate-Control', - value: 'no-store', + key: 'X-Frame-Options', + value: 'SAMEORIGIN', }, { - key: 'Vary', - value: 'User-Agent', + 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'", }, ], },