-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathsecurity.ts
More file actions
106 lines (92 loc) · 3.86 KB
/
security.ts
File metadata and controls
106 lines (92 loc) · 3.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import type { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify'
import fp from 'fastify-plugin'
import { env } from '../lib/env.js'
import { detectSuspiciousActivity, logSecurityEvent } from '../lib/security.js'
type SecurityPluginOptions = Record<string, never>
const security: FastifyPluginAsync<SecurityPluginOptions> = async fastify => {
// Only add security headers if enabled
if (!env.SECURITY_HEADERS_ENABLED) return
// onRequest hook: security headers + suspicious activity detection
fastify.addHook('onRequest', async (request: FastifyRequest, reply: FastifyReply) => {
// Detect suspicious patterns
if (detectSuspiciousActivity(request))
logSecurityEvent(request, 'suspicious_activity_detected', {
method: request.method,
url: request.url,
ip: request.ip,
userAgent: request.headers['user-agent'],
})
// Log but don't block - let rate limiting handle abuse
// In production, you might want to block or add to blocklist
// Log all requests in production for security monitoring
if (env.NODE_ENV === 'production')
logSecurityEvent(request, 'request_received', {
method: request.method,
url: request.url,
})
// Prevent MIME type sniffing
reply.header('X-Content-Type-Options', 'nosniff')
// Prevent clickjacking attacks
reply.header('X-Frame-Options', 'DENY')
// Enable XSS protection (legacy but still useful)
reply.header('X-XSS-Protection', '1; mode=block')
// Control referrer information
reply.header('Referrer-Policy', 'strict-origin-when-cross-origin')
// Restrict browser features
reply.header(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=(), interest-cohort=()',
)
// Content Security Policy - more restrictive in production
const path = request.url?.split('?')[0] ?? ''
const isReferenceRoute = path.startsWith('/reference')
const isRootLanding = path === '/' || path === ''
if (env.NODE_ENV === 'production' && !isReferenceRoute && !isRootLanding) {
// Strict CSP for API routes in production
const cspDirectives = [
"default-src 'self'",
"script-src 'self'",
"style-src 'self'",
"img-src 'self' data: https:",
"font-src 'self' data:",
"connect-src 'self'",
"frame-ancestors 'none'",
]
reply.header('Content-Security-Policy', cspDirectives.join('; '))
} else {
// Relaxed CSP for Swagger UI (/reference routes) or development
const cspDirectives = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net", // Allow Scalar CDN
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net",
"img-src 'self' data: https:",
"font-src 'self' data: https://cdn.jsdelivr.net https://fonts.scalar.com", // Allow Scalar fonts
"connect-src 'self' http://localhost:* https://fonts.scalar.com", // Allow localhost with any port and Scalar fonts
"frame-ancestors 'none'",
]
reply.header('Content-Security-Policy', cspDirectives.join('; '))
}
// Strict Transport Security (HTTPS only in production)
if (env.NODE_ENV === 'production')
reply.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
})
// onError hook: security event logging
fastify.addHook(
'onError',
async (
request: FastifyRequest,
_reply: FastifyReply,
error: Error & { statusCode?: number },
) => {
// Log security-relevant errors
if (error.statusCode === 429) logSecurityEvent(request, 'rate_limit_exceeded')
else if (error.statusCode === 401 || error.statusCode === 403)
logSecurityEvent(request, 'authentication_failure', {
statusCode: error.statusCode,
})
},
)
}
export default fp(security, {
name: 'security-headers',
})