forked from Talenttrust/Talenttrust-Backend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsecurity.ts
More file actions
131 lines (119 loc) · 5 KB
/
Copy pathsecurity.ts
File metadata and controls
131 lines (119 loc) · 5 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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
/**
* @title Security Configuration
* @notice Centralized configuration for CORS and Helmet security headers
* @dev Provides configurable options driven by environment variables
*/
import { CorsOptions } from 'cors';
import { HelmetOptions } from 'helmet';
/**
* @notice Validates CORS allowlist configuration
* @dev Enforces strict denial of wildcard origins in production mode
* @param origins Array of allowed origins
* @throws Error if wildcard is used in production or allowlist is empty
*/
function validateCorsAllowlist(origins: string[]): void {
const isProduction = process.env.NODE_ENV === 'production';
// Check for wildcard in production
if (isProduction && origins.includes('*')) {
throw new Error('Wildcard CORS origin (*) is not allowed in production mode');
}
// Check for localhost in production
if (isProduction && origins.some(origin => origin.includes('localhost'))) {
throw new Error('Localhost CORS origin is not allowed in production mode');
}
// Warn about invalid origin formats
origins.forEach(origin => {
if (origin !== '*' && !origin.startsWith('http://') && !origin.startsWith('https://')) {
console.warn(`[CORS] Warning: Origin "${origin}" does not start with http:// or https://`);
}
});
}
/**
* @notice Parses and validates CORS allowed origins from environment
* @dev Production defaults to deny-by-default (empty list).
* Non-production defaults to localhost origins for convenience.
* @returns Array of validated allowed origins
*/
function parseAllowedOrigins(): string[] {
const isProduction = process.env.NODE_ENV === 'production';
const hasAllowedOrigins = 'CORS_ALLOWED_ORIGINS' in process.env;
let origins: string[];
if (hasAllowedOrigins) {
const raw = process.env.CORS_ALLOWED_ORIGINS!;
origins = raw ? raw.split(',').map(o => o.trim()).filter(Boolean) : [];
} else {
// Production defaults to deny-by-default; development defaults to localhost
origins = isProduction ? [] : ['http://localhost:3000', 'http://localhost:3001'];
}
if (origins.length > 0) {
validateCorsAllowlist(origins);
} else if (!isProduction) {
// Warn (do not throw) in non-production when the resolved allowlist is empty —
// whether CORS_ALLOWED_ORIGINS was explicitly set to an empty/whitespace value or
// omitted entirely. An empty allowlist blocks all browser cross-origin requests,
// which is almost certainly a misconfiguration in dev/staging, but it should not
// hard-crash the process. Production legitimately starts with deny-by-default.
console.warn(
'[CORS] Allowlist is empty — all cross-origin requests from browsers will be rejected. ' +
'Provide at least one allowed origin via CORS_ALLOWED_ORIGINS or remove the variable to use the defaults.',
);
}
return origins;
}
// Array of allowed origins. Defaults to localhost for development, overridable by env.
const allowedOrigins = parseAllowedOrigins();
/**
* @notice Creates a CORS configuration from an explicit allowlist.
* @dev Used when the validated environment config drives CORS (preferred over process.env).
* @param origins Array of allowed origins
*/
export function createCorsConfig(origins: string[]): CorsOptions {
if (origins.length > 0) {
validateCorsAllowlist(origins);
}
return {
origin: (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => {
if (!origin || origins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS policy'));
}
},
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400,
};
}
/**
* @notice CORS configuration options
* @dev Rejects requests from origins not in the allowed pool.
* Never echoes arbitrary origins and never uses wildcard with credentials enabled.
*/
export const corsConfig: CorsOptions = createCorsConfig(allowedOrigins);
/**
* @notice Helmet configuration options
* @dev Sets up restrictive Content-Security-Policy and HSTS policy
*/
export const helmetConfig: HelmetOptions = {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'", "https:", "data:"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
},
hsts: {
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true,
},
crossOriginResourcePolicy: { policy: "same-origin" },
referrerPolicy: { policy: "strict-origin-when-cross-origin" },
};