mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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:
+49
-4
@@ -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
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user