feat(security): added additional security measures to next config & middleware (#301)

* feat(security): added additional security measures to next config & middleware

* addressed PR comments
This commit is contained in:
Waleed Latif
2025-04-26 16:52:08 -07:00
committed by GitHub
parent 11922d24ae
commit 503e07b500
2 changed files with 159 additions and 5 deletions
+49 -4
View File
@@ -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'
],
}
+110 -1
View File
@@ -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',
},
],
},
{