Skip to content

Commit 503e07b

Browse files
authored
feat(security): added additional security measures to next config & middleware (#301)
* feat(security): added additional security measures to next config & middleware * addressed PR comments
1 parent 11922d2 commit 503e07b

2 files changed

Lines changed: 159 additions & 5 deletions

File tree

sim/middleware.ts

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
11
import { NextRequest, NextResponse } from 'next/server'
22
import { getSessionCookie } from 'better-auth/cookies'
33
import { verifyToken } from './lib/waitlist/token'
4+
import { createLogger } from '@/lib/logs/console-logger'
5+
6+
const logger = createLogger('Middleware')
47

58
// Environment flag to check if we're in development mode
69
const isDevelopment = process.env.NODE_ENV === 'development'
710

11+
const SUSPICIOUS_UA_PATTERNS = [
12+
/^\s*$/, // Empty user agents
13+
/\.\./, // Path traversal attempt
14+
/<\s*script/i, // Potential XSS payloads
15+
/^\(\)\s*{/, // Command execution attempt
16+
/\b(sqlmap|nikto|gobuster|dirb|nmap)\b/i // Known scanning tools
17+
]
18+
819
export async function middleware(request: NextRequest) {
920
// Check for active session
1021
const sessionCookie = getSessionCookie(request)
@@ -66,7 +77,7 @@ export async function middleware(request: NextRequest) {
6677
return NextResponse.redirect(new URL('/', request.url))
6778
}
6879
} catch (error) {
69-
console.error('Token validation error:', error)
80+
logger.error('Token validation error:', error)
7081
// In case of error, redirect signup attempts to home
7182
if (request.nextUrl.pathname === '/signup') {
7283
return NextResponse.redirect(new URL('/', request.url))
@@ -80,15 +91,49 @@ export async function middleware(request: NextRequest) {
8091
}
8192
}
8293

83-
return NextResponse.next()
94+
const userAgent = request.headers.get('user-agent') || ''
95+
96+
const isSuspicious = SUSPICIOUS_UA_PATTERNS.some(pattern =>
97+
pattern.test(userAgent)
98+
)
99+
100+
if (isSuspicious) {
101+
logger.warn('Blocked suspicious request', {
102+
userAgent,
103+
ip: request.headers.get('x-forwarded-for') || 'unknown',
104+
url: request.url,
105+
method: request.method,
106+
pattern: SUSPICIOUS_UA_PATTERNS.find(pattern => pattern.test(userAgent))?.toString()
107+
})
108+
109+
// Return 403 with security headers
110+
return new NextResponse(null, {
111+
status: 403,
112+
statusText: 'Forbidden',
113+
headers: {
114+
'Content-Type': 'text/plain',
115+
'X-Content-Type-Options': 'nosniff',
116+
'X-Frame-Options': 'DENY',
117+
'Content-Security-Policy': "default-src 'none'",
118+
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
119+
'Pragma': 'no-cache',
120+
'Expires': '0'
121+
}
122+
})
123+
}
124+
125+
const response = NextResponse.next()
126+
127+
response.headers.set('Vary', 'User-Agent')
128+
129+
return response
84130
}
85131

86-
// Update matcher to include admin routes
87132
export const config = {
88133
matcher: [
89134
'/w', // Match exactly /w
90135
'/w/:path*', // Match protected routes
91136
'/login',
92-
'/signup',
137+
'/signup'
93138
],
94139
}

sim/next.config.ts

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ const isStandaloneBuild = process.env.USE_LOCAL_STORAGE === 'true'
55

66
const nextConfig: NextConfig = {
77
devIndicators: false,
8+
experimental: {
9+
sri: {
10+
algorithm: 'sha256'
11+
}
12+
},
813
images: {
914
domains: [
1015
'avatars.githubusercontent.com',
@@ -35,7 +40,7 @@ const nextConfig: NextConfig = {
3540
async headers() {
3641
return [
3742
{
38-
// API routes CORS headers
43+
// API routes CORS headers - keep no-cache for dynamic API endpoints
3944
source: '/api/:path*',
4045
headers: [
4146
{ key: 'Access-Control-Allow-Credentials', value: 'true' },
@@ -52,6 +57,110 @@ const nextConfig: NextConfig = {
5257
value:
5358
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version',
5459
},
60+
{
61+
key: 'Cache-Control',
62+
value: 'no-store, no-cache, must-revalidate, proxy-revalidate',
63+
},
64+
{
65+
key: 'Pragma',
66+
value: 'no-cache',
67+
},
68+
{
69+
key: 'Expires',
70+
value: '0',
71+
},
72+
{
73+
key: 'Surrogate-Control',
74+
value: 'no-store',
75+
},
76+
],
77+
},
78+
{
79+
// Static assets - long caching for better performance
80+
// This targets common static file extensions
81+
source: '/:path*.(js|css|svg|png|jpg|jpeg|gif|webp|avif|ico|woff|woff2|ttf|eot)',
82+
headers: [
83+
{
84+
key: 'Cache-Control',
85+
value: 'public, max-age=31536000, immutable',
86+
},
87+
{
88+
key: 'Vary',
89+
value: 'User-Agent',
90+
},
91+
],
92+
},
93+
{
94+
// HTML/dynamic content - use validation caching instead of no-cache
95+
source: '/:path*',
96+
has: [
97+
{
98+
type: 'header',
99+
key: 'Accept',
100+
value: '(.*text/html.*)',
101+
},
102+
],
103+
headers: [
104+
{
105+
key: 'X-Content-Type-Options',
106+
value: 'nosniff',
107+
},
108+
{
109+
key: 'X-Frame-Options',
110+
value: 'SAMEORIGIN',
111+
},
112+
{
113+
key: 'Content-Security-Policy',
114+
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'",
115+
},
116+
{
117+
key: 'Cache-Control',
118+
value: 'public, max-age=0, must-revalidate',
119+
},
120+
{
121+
key: 'Vary',
122+
value: 'User-Agent',
123+
},
124+
],
125+
},
126+
{
127+
// Apply security headers to all routes
128+
source: '/:path*',
129+
headers: [
130+
{
131+
key: 'X-Content-Type-Options',
132+
value: 'nosniff',
133+
},
134+
{
135+
key: 'X-Frame-Options',
136+
value: 'SAMEORIGIN',
137+
},
138+
{
139+
key: 'Content-Security-Policy',
140+
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'",
141+
},
142+
],
143+
},
144+
{
145+
// Dynamic routes containing user data - strict no caching
146+
source: '/w/:path*',
147+
headers: [
148+
{
149+
key: 'Cache-Control',
150+
value: 'private, no-store, no-cache, must-revalidate, proxy-revalidate',
151+
},
152+
{
153+
key: 'Pragma',
154+
value: 'no-cache',
155+
},
156+
{
157+
key: 'Expires',
158+
value: '0',
159+
},
160+
{
161+
key: 'Surrogate-Control',
162+
value: 'no-store',
163+
},
55164
],
56165
},
57166
{

0 commit comments

Comments
 (0)