tlc-claude-code 1.4.8 → 1.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/server/index.js +229 -14
- package/server/lib/compliance/control-mapper.js +401 -0
- package/server/lib/compliance/control-mapper.test.js +117 -0
- package/server/lib/compliance/evidence-linker.js +296 -0
- package/server/lib/compliance/evidence-linker.test.js +121 -0
- package/server/lib/compliance/gdpr-checklist.js +416 -0
- package/server/lib/compliance/gdpr-checklist.test.js +131 -0
- package/server/lib/compliance/hipaa-checklist.js +277 -0
- package/server/lib/compliance/hipaa-checklist.test.js +101 -0
- package/server/lib/compliance/iso27001-checklist.js +287 -0
- package/server/lib/compliance/iso27001-checklist.test.js +99 -0
- package/server/lib/compliance/multi-framework-reporter.js +284 -0
- package/server/lib/compliance/multi-framework-reporter.test.js +127 -0
- package/server/lib/compliance/pci-dss-checklist.js +214 -0
- package/server/lib/compliance/pci-dss-checklist.test.js +95 -0
- package/server/lib/compliance/trust-centre.js +187 -0
- package/server/lib/compliance/trust-centre.test.js +93 -0
- package/server/lib/dashboard/api-server.js +155 -0
- package/server/lib/dashboard/api-server.test.js +155 -0
- package/server/lib/dashboard/health-api.js +199 -0
- package/server/lib/dashboard/health-api.test.js +122 -0
- package/server/lib/dashboard/notes-api.js +234 -0
- package/server/lib/dashboard/notes-api.test.js +134 -0
- package/server/lib/dashboard/router-api.js +176 -0
- package/server/lib/dashboard/router-api.test.js +132 -0
- package/server/lib/dashboard/tasks-api.js +289 -0
- package/server/lib/dashboard/tasks-api.test.js +161 -0
- package/server/lib/dashboard/tlc-introspection.js +197 -0
- package/server/lib/dashboard/tlc-introspection.test.js +138 -0
- package/server/lib/dashboard/version-api.js +222 -0
- package/server/lib/dashboard/version-api.test.js +112 -0
- package/server/lib/dashboard/websocket-server.js +104 -0
- package/server/lib/dashboard/websocket-server.test.js +118 -0
- package/server/lib/deploy/branch-classifier.js +163 -0
- package/server/lib/deploy/branch-classifier.test.js +164 -0
- package/server/lib/deploy/deployment-approval.js +299 -0
- package/server/lib/deploy/deployment-approval.test.js +296 -0
- package/server/lib/deploy/deployment-audit.js +374 -0
- package/server/lib/deploy/deployment-audit.test.js +307 -0
- package/server/lib/deploy/deployment-executor.js +335 -0
- package/server/lib/deploy/deployment-executor.test.js +329 -0
- package/server/lib/deploy/deployment-rules.js +163 -0
- package/server/lib/deploy/deployment-rules.test.js +188 -0
- package/server/lib/deploy/rollback-manager.js +379 -0
- package/server/lib/deploy/rollback-manager.test.js +321 -0
- package/server/lib/deploy/security-gates.js +236 -0
- package/server/lib/deploy/security-gates.test.js +222 -0
- package/server/lib/k8s/gitops-config.js +188 -0
- package/server/lib/k8s/gitops-config.test.js +59 -0
- package/server/lib/k8s/helm-generator.js +196 -0
- package/server/lib/k8s/helm-generator.test.js +59 -0
- package/server/lib/k8s/kustomize-generator.js +176 -0
- package/server/lib/k8s/kustomize-generator.test.js +58 -0
- package/server/lib/k8s/network-policy.js +114 -0
- package/server/lib/k8s/network-policy.test.js +53 -0
- package/server/lib/k8s/pod-security.js +114 -0
- package/server/lib/k8s/pod-security.test.js +55 -0
- package/server/lib/k8s/rbac-generator.js +132 -0
- package/server/lib/k8s/rbac-generator.test.js +57 -0
- package/server/lib/k8s/resource-manager.js +172 -0
- package/server/lib/k8s/resource-manager.test.js +60 -0
- package/server/lib/k8s/secrets-encryption.js +168 -0
- package/server/lib/k8s/secrets-encryption.test.js +49 -0
- package/server/lib/monitoring/alert-manager.js +238 -0
- package/server/lib/monitoring/alert-manager.test.js +106 -0
- package/server/lib/monitoring/health-check.js +226 -0
- package/server/lib/monitoring/health-check.test.js +176 -0
- package/server/lib/monitoring/incident-manager.js +230 -0
- package/server/lib/monitoring/incident-manager.test.js +98 -0
- package/server/lib/monitoring/log-aggregator.js +147 -0
- package/server/lib/monitoring/log-aggregator.test.js +89 -0
- package/server/lib/monitoring/metrics-collector.js +337 -0
- package/server/lib/monitoring/metrics-collector.test.js +172 -0
- package/server/lib/monitoring/status-page.js +214 -0
- package/server/lib/monitoring/status-page.test.js +105 -0
- package/server/lib/monitoring/uptime-monitor.js +194 -0
- package/server/lib/monitoring/uptime-monitor.test.js +109 -0
- package/server/lib/network/fail2ban-config.js +294 -0
- package/server/lib/network/fail2ban-config.test.js +275 -0
- package/server/lib/network/firewall-manager.js +252 -0
- package/server/lib/network/firewall-manager.test.js +254 -0
- package/server/lib/network/geoip-filter.js +282 -0
- package/server/lib/network/geoip-filter.test.js +264 -0
- package/server/lib/network/rate-limiter.js +229 -0
- package/server/lib/network/rate-limiter.test.js +293 -0
- package/server/lib/network/request-validator.js +351 -0
- package/server/lib/network/request-validator.test.js +345 -0
- package/server/lib/network/security-headers.js +251 -0
- package/server/lib/network/security-headers.test.js +283 -0
- package/server/lib/network/tls-config.js +210 -0
- package/server/lib/network/tls-config.test.js +248 -0
- package/server/lib/security/auth-security.js +369 -0
- package/server/lib/security/auth-security.test.js +448 -0
- package/server/lib/security/cis-benchmark.js +152 -0
- package/server/lib/security/cis-benchmark.test.js +137 -0
- package/server/lib/security/compose-templates.js +312 -0
- package/server/lib/security/compose-templates.test.js +229 -0
- package/server/lib/security/container-runtime.js +456 -0
- package/server/lib/security/container-runtime.test.js +503 -0
- package/server/lib/security/cors-validator.js +278 -0
- package/server/lib/security/cors-validator.test.js +310 -0
- package/server/lib/security/crypto-utils.js +253 -0
- package/server/lib/security/crypto-utils.test.js +409 -0
- package/server/lib/security/dockerfile-linter.js +459 -0
- package/server/lib/security/dockerfile-linter.test.js +483 -0
- package/server/lib/security/dockerfile-templates.js +278 -0
- package/server/lib/security/dockerfile-templates.test.js +164 -0
- package/server/lib/security/error-sanitizer.js +426 -0
- package/server/lib/security/error-sanitizer.test.js +331 -0
- package/server/lib/security/headers-generator.js +368 -0
- package/server/lib/security/headers-generator.test.js +398 -0
- package/server/lib/security/image-scanner.js +83 -0
- package/server/lib/security/image-scanner.test.js +106 -0
- package/server/lib/security/input-validator.js +352 -0
- package/server/lib/security/input-validator.test.js +330 -0
- package/server/lib/security/network-policy.js +174 -0
- package/server/lib/security/network-policy.test.js +164 -0
- package/server/lib/security/output-encoder.js +237 -0
- package/server/lib/security/output-encoder.test.js +276 -0
- package/server/lib/security/path-validator.js +359 -0
- package/server/lib/security/path-validator.test.js +293 -0
- package/server/lib/security/query-builder.js +421 -0
- package/server/lib/security/query-builder.test.js +318 -0
- package/server/lib/security/secret-detector.js +290 -0
- package/server/lib/security/secret-detector.test.js +354 -0
- package/server/lib/security/secrets-validator.js +137 -0
- package/server/lib/security/secrets-validator.test.js +120 -0
- package/server/lib/security-testing/dast-runner.js +154 -0
- package/server/lib/security-testing/dast-runner.test.js +62 -0
- package/server/lib/security-testing/dependency-scanner.js +172 -0
- package/server/lib/security-testing/dependency-scanner.test.js +64 -0
- package/server/lib/security-testing/pentest-runner.js +230 -0
- package/server/lib/security-testing/pentest-runner.test.js +60 -0
- package/server/lib/security-testing/sast-runner.js +136 -0
- package/server/lib/security-testing/sast-runner.test.js +62 -0
- package/server/lib/security-testing/secret-scanner.js +153 -0
- package/server/lib/security-testing/secret-scanner.test.js +66 -0
- package/server/lib/security-testing/security-gate.js +216 -0
- package/server/lib/security-testing/security-gate.test.js +115 -0
- package/server/lib/security-testing/security-reporter.js +303 -0
- package/server/lib/security-testing/security-reporter.test.js +114 -0
- package/server/lib/standards/audit-checker.js +546 -0
- package/server/lib/standards/audit-checker.test.js +415 -0
- package/server/lib/standards/cleanup-executor.js +452 -0
- package/server/lib/standards/cleanup-executor.test.js +293 -0
- package/server/lib/standards/refactor-stepper.js +425 -0
- package/server/lib/standards/refactor-stepper.test.js +298 -0
- package/server/lib/standards/standards-injector.js +167 -0
- package/server/lib/standards/standards-injector.test.js +232 -0
- package/server/lib/user-management.test.js +284 -0
- package/server/lib/vps/backup-manager.js +157 -0
- package/server/lib/vps/backup-manager.test.js +59 -0
- package/server/lib/vps/caddy-config.js +159 -0
- package/server/lib/vps/caddy-config.test.js +48 -0
- package/server/lib/vps/compose-orchestrator.js +219 -0
- package/server/lib/vps/compose-orchestrator.test.js +50 -0
- package/server/lib/vps/database-config.js +208 -0
- package/server/lib/vps/database-config.test.js +47 -0
- package/server/lib/vps/deploy-script.js +211 -0
- package/server/lib/vps/deploy-script.test.js +53 -0
- package/server/lib/vps/secrets-manager.js +148 -0
- package/server/lib/vps/secrets-manager.test.js +58 -0
- package/server/lib/vps/server-hardening.js +174 -0
- package/server/lib/vps/server-hardening.test.js +70 -0
- package/server/package-lock.json +19 -0
- package/server/package.json +1 -0
- package/server/templates/CLAUDE.md +37 -0
- package/server/templates/CODING-STANDARDS.md +408 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORS Validator Module
|
|
3
|
+
*
|
|
4
|
+
* Strict CORS configuration to prevent cross-origin attacks.
|
|
5
|
+
* Addresses OWASP A05: Security Misconfiguration
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Custom error for CORS security violations
|
|
10
|
+
*/
|
|
11
|
+
export class CorsSecurityError extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'CorsSecurityError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Simple headers that don't need explicit CORS allowance
|
|
20
|
+
*/
|
|
21
|
+
const SIMPLE_HEADERS = ['accept', 'accept-language', 'content-language', 'content-type'];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validate an origin against allowed origins
|
|
25
|
+
* @param {string|null} origin - Origin to validate
|
|
26
|
+
* @param {Object} options - Validation options
|
|
27
|
+
* @returns {Object} Validation result
|
|
28
|
+
*/
|
|
29
|
+
export function validateOrigin(origin, options = {}) {
|
|
30
|
+
const {
|
|
31
|
+
allowedOrigins = [],
|
|
32
|
+
production = false,
|
|
33
|
+
allowNull = false,
|
|
34
|
+
} = options;
|
|
35
|
+
|
|
36
|
+
// Check wildcard in production
|
|
37
|
+
if (allowedOrigins.includes('*') && production) {
|
|
38
|
+
throw new CorsSecurityError('Wildcard origin (*) not allowed in production');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Handle null origin
|
|
42
|
+
if (origin === null) {
|
|
43
|
+
return allowNull
|
|
44
|
+
? { allowed: true }
|
|
45
|
+
: { allowed: false, reason: 'Null origin not allowed' };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Wildcard in development
|
|
49
|
+
if (allowedOrigins.includes('*') && !production) {
|
|
50
|
+
return { allowed: true };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Validate origin format - reject credentials in URL
|
|
54
|
+
if (origin && origin.includes('@')) {
|
|
55
|
+
return { allowed: false, reason: 'Origin contains credentials' };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Reject trailing slashes
|
|
59
|
+
if (origin && origin.endsWith('/')) {
|
|
60
|
+
return { allowed: false, reason: 'Origin has trailing slash' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Reject paths
|
|
64
|
+
try {
|
|
65
|
+
const url = new URL(origin);
|
|
66
|
+
if (url.pathname !== '/' && url.pathname !== '') {
|
|
67
|
+
return { allowed: false, reason: 'Origin contains path' };
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
return { allowed: false, reason: 'Invalid origin format' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Normalize for comparison
|
|
74
|
+
const normalizedOrigin = origin.toLowerCase();
|
|
75
|
+
|
|
76
|
+
for (const allowed of allowedOrigins) {
|
|
77
|
+
// Exact match (case insensitive)
|
|
78
|
+
if (allowed.toLowerCase() === normalizedOrigin) {
|
|
79
|
+
return { allowed: true };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Subdomain wildcard pattern
|
|
83
|
+
if (allowed.includes('*')) {
|
|
84
|
+
const pattern = allowed
|
|
85
|
+
.replace(/\./g, '\\.')
|
|
86
|
+
.replace(/\*/g, '[a-z0-9-]+');
|
|
87
|
+
const regex = new RegExp(`^${pattern}$`, 'i');
|
|
88
|
+
|
|
89
|
+
// Limit pattern length to prevent ReDoS
|
|
90
|
+
if (origin.length <= 100 && regex.test(origin)) {
|
|
91
|
+
return { allowed: true };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { allowed: false, reason: 'Origin not in whitelist' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Generate CORS headers for a response
|
|
101
|
+
* @param {Object} options - Header options
|
|
102
|
+
* @returns {Object} CORS headers
|
|
103
|
+
*/
|
|
104
|
+
export function generateCorsHeaders(options = {}) {
|
|
105
|
+
const {
|
|
106
|
+
origin,
|
|
107
|
+
allowedOrigins = [],
|
|
108
|
+
credentials = false,
|
|
109
|
+
exposeHeaders = [],
|
|
110
|
+
production = false,
|
|
111
|
+
} = options;
|
|
112
|
+
|
|
113
|
+
const validation = validateOrigin(origin, { allowedOrigins, production });
|
|
114
|
+
|
|
115
|
+
if (!validation.allowed) {
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const headers = {
|
|
120
|
+
'Access-Control-Allow-Origin': origin,
|
|
121
|
+
'Vary': 'Origin',
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (credentials) {
|
|
125
|
+
headers['Access-Control-Allow-Credentials'] = 'true';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (exposeHeaders.length > 0) {
|
|
129
|
+
headers['Access-Control-Expose-Headers'] = exposeHeaders.join(', ');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return headers;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Handle preflight OPTIONS request
|
|
137
|
+
* @param {Object} options - Preflight options
|
|
138
|
+
* @returns {Object} Preflight response headers
|
|
139
|
+
*/
|
|
140
|
+
export function handlePreflight(options = {}) {
|
|
141
|
+
const {
|
|
142
|
+
origin,
|
|
143
|
+
requestMethod,
|
|
144
|
+
requestHeaders = [],
|
|
145
|
+
allowedOrigins = [],
|
|
146
|
+
allowedMethods = ['GET', 'POST'],
|
|
147
|
+
allowedHeaders = [],
|
|
148
|
+
credentials = false,
|
|
149
|
+
maxAge = 86400,
|
|
150
|
+
production = false,
|
|
151
|
+
} = options;
|
|
152
|
+
|
|
153
|
+
const validation = validateOrigin(origin, { allowedOrigins, production });
|
|
154
|
+
|
|
155
|
+
if (!validation.allowed) {
|
|
156
|
+
return {};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const headers = {
|
|
160
|
+
'Access-Control-Allow-Origin': origin,
|
|
161
|
+
'Vary': 'Origin, Access-Control-Request-Method, Access-Control-Request-Headers',
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Filter allowed methods
|
|
165
|
+
const allowedMethodsList = allowedMethods.filter((m) =>
|
|
166
|
+
requestMethod ? allowedMethods.includes(requestMethod.toUpperCase()) : true
|
|
167
|
+
);
|
|
168
|
+
headers['Access-Control-Allow-Methods'] = allowedMethodsList.join(', ');
|
|
169
|
+
|
|
170
|
+
// Filter allowed headers (always allow simple headers)
|
|
171
|
+
const normalizedAllowedHeaders = [
|
|
172
|
+
...allowedHeaders.map((h) => h.toLowerCase()),
|
|
173
|
+
...SIMPLE_HEADERS,
|
|
174
|
+
];
|
|
175
|
+
const normalizedRequestHeaders = requestHeaders.map((h) => h.toLowerCase());
|
|
176
|
+
|
|
177
|
+
const filteredHeaders = normalizedRequestHeaders.filter((h) =>
|
|
178
|
+
normalizedAllowedHeaders.includes(h) || SIMPLE_HEADERS.includes(h)
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
if (filteredHeaders.length > 0 || allowedHeaders.length > 0) {
|
|
182
|
+
headers['Access-Control-Allow-Headers'] = [...new Set([
|
|
183
|
+
...allowedHeaders,
|
|
184
|
+
...filteredHeaders.filter((h) => SIMPLE_HEADERS.includes(h)),
|
|
185
|
+
])].join(', ') || 'Accept, Content-Type';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (credentials) {
|
|
189
|
+
headers['Access-Control-Allow-Credentials'] = 'true';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (maxAge) {
|
|
193
|
+
headers['Access-Control-Max-Age'] = String(maxAge);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return headers;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Create a reusable CORS validator
|
|
201
|
+
* @param {Object} config - CORS configuration
|
|
202
|
+
* @returns {Object} CORS validator instance
|
|
203
|
+
*/
|
|
204
|
+
export function createCorsValidator(config = {}) {
|
|
205
|
+
const {
|
|
206
|
+
allowedOrigins = [],
|
|
207
|
+
allowedMethods = ['GET', 'POST', 'PUT', 'DELETE'],
|
|
208
|
+
allowedHeaders = ['Content-Type', 'Authorization'],
|
|
209
|
+
credentials = false,
|
|
210
|
+
maxAge = 86400,
|
|
211
|
+
production = false,
|
|
212
|
+
} = config;
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
validate(origin) {
|
|
216
|
+
return validateOrigin(origin, { allowedOrigins, production });
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
isMethodAllowed(method) {
|
|
220
|
+
return allowedMethods.map((m) => m.toUpperCase()).includes(method.toUpperCase());
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
isHeaderAllowed(header) {
|
|
224
|
+
const normalized = header.toLowerCase();
|
|
225
|
+
return (
|
|
226
|
+
SIMPLE_HEADERS.includes(normalized) ||
|
|
227
|
+
allowedHeaders.map((h) => h.toLowerCase()).includes(normalized)
|
|
228
|
+
);
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
getHeaders(origin) {
|
|
232
|
+
return generateCorsHeaders({
|
|
233
|
+
origin,
|
|
234
|
+
allowedOrigins,
|
|
235
|
+
credentials,
|
|
236
|
+
production,
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
handlePreflight(origin, requestMethod, requestHeaders) {
|
|
241
|
+
return handlePreflight({
|
|
242
|
+
origin,
|
|
243
|
+
requestMethod,
|
|
244
|
+
requestHeaders,
|
|
245
|
+
allowedOrigins,
|
|
246
|
+
allowedMethods,
|
|
247
|
+
allowedHeaders,
|
|
248
|
+
credentials,
|
|
249
|
+
maxAge,
|
|
250
|
+
production,
|
|
251
|
+
});
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
middleware() {
|
|
255
|
+
return (req, res, next) => {
|
|
256
|
+
const origin = req.headers.origin;
|
|
257
|
+
const headers = this.getHeaders(origin);
|
|
258
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
259
|
+
res.setHeader(key, value);
|
|
260
|
+
});
|
|
261
|
+
if (req.method === 'OPTIONS') {
|
|
262
|
+
const preflightHeaders = this.handlePreflight(
|
|
263
|
+
origin,
|
|
264
|
+
req.headers['access-control-request-method'],
|
|
265
|
+
(req.headers['access-control-request-headers'] || '').split(',').map((h) => h.trim())
|
|
266
|
+
);
|
|
267
|
+
Object.entries(preflightHeaders).forEach(([key, value]) => {
|
|
268
|
+
res.setHeader(key, value);
|
|
269
|
+
});
|
|
270
|
+
res.statusCode = 204;
|
|
271
|
+
res.end();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
next();
|
|
275
|
+
};
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORS Validator Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for strict CORS configuration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import {
|
|
9
|
+
validateOrigin,
|
|
10
|
+
generateCorsHeaders,
|
|
11
|
+
handlePreflight,
|
|
12
|
+
createCorsValidator,
|
|
13
|
+
CorsSecurityError,
|
|
14
|
+
} from './cors-validator.js';
|
|
15
|
+
|
|
16
|
+
describe('cors-validator', () => {
|
|
17
|
+
describe('validateOrigin', () => {
|
|
18
|
+
it('allows whitelisted origin', () => {
|
|
19
|
+
const result = validateOrigin('https://example.com', {
|
|
20
|
+
allowedOrigins: ['https://example.com', 'https://app.example.com'],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(result.allowed).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('rejects non-whitelisted origin', () => {
|
|
27
|
+
const result = validateOrigin('https://evil.com', {
|
|
28
|
+
allowedOrigins: ['https://example.com'],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(result.allowed).toBe(false);
|
|
32
|
+
expect(result.reason).toContain('not in whitelist');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('rejects wildcard (*) in production mode', () => {
|
|
36
|
+
expect(() => {
|
|
37
|
+
validateOrigin('https://example.com', {
|
|
38
|
+
allowedOrigins: ['*'],
|
|
39
|
+
production: true,
|
|
40
|
+
});
|
|
41
|
+
}).toThrow(CorsSecurityError);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('allows wildcard in development mode', () => {
|
|
45
|
+
const result = validateOrigin('https://anything.com', {
|
|
46
|
+
allowedOrigins: ['*'],
|
|
47
|
+
production: false,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(result.allowed).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('supports pattern matching for subdomains', () => {
|
|
54
|
+
const result = validateOrigin('https://api.example.com', {
|
|
55
|
+
allowedOrigins: ['https://*.example.com'],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(result.allowed).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('rejects null origin by default', () => {
|
|
62
|
+
const result = validateOrigin(null, {
|
|
63
|
+
allowedOrigins: ['https://example.com'],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(result.allowed).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('allows null origin when explicitly configured', () => {
|
|
70
|
+
const result = validateOrigin(null, {
|
|
71
|
+
allowedOrigins: ['https://example.com'],
|
|
72
|
+
allowNull: true,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(result.allowed).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('validates origin protocol', () => {
|
|
79
|
+
const result = validateOrigin('http://example.com', {
|
|
80
|
+
allowedOrigins: ['https://example.com'],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(result.allowed).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('validates origin port', () => {
|
|
87
|
+
const result = validateOrigin('https://example.com:8080', {
|
|
88
|
+
allowedOrigins: ['https://example.com'],
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(result.allowed).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('generateCorsHeaders', () => {
|
|
96
|
+
it('generates Access-Control-Allow-Origin header', () => {
|
|
97
|
+
const headers = generateCorsHeaders({
|
|
98
|
+
origin: 'https://example.com',
|
|
99
|
+
allowedOrigins: ['https://example.com'],
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(headers['Access-Control-Allow-Origin']).toBe('https://example.com');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('generates Vary: Origin header', () => {
|
|
106
|
+
const headers = generateCorsHeaders({
|
|
107
|
+
origin: 'https://example.com',
|
|
108
|
+
allowedOrigins: ['https://example.com'],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(headers['Vary']).toContain('Origin');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('sets Access-Control-Allow-Credentials when configured', () => {
|
|
115
|
+
const headers = generateCorsHeaders({
|
|
116
|
+
origin: 'https://example.com',
|
|
117
|
+
allowedOrigins: ['https://example.com'],
|
|
118
|
+
credentials: true,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(headers['Access-Control-Allow-Credentials']).toBe('true');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('does not set credentials header when not configured', () => {
|
|
125
|
+
const headers = generateCorsHeaders({
|
|
126
|
+
origin: 'https://example.com',
|
|
127
|
+
allowedOrigins: ['https://example.com'],
|
|
128
|
+
credentials: false,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(headers['Access-Control-Allow-Credentials']).toBeUndefined();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('sets Access-Control-Expose-Headers', () => {
|
|
135
|
+
const headers = generateCorsHeaders({
|
|
136
|
+
origin: 'https://example.com',
|
|
137
|
+
allowedOrigins: ['https://example.com'],
|
|
138
|
+
exposeHeaders: ['X-Request-Id', 'X-RateLimit-Remaining'],
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(headers['Access-Control-Expose-Headers']).toContain('X-Request-Id');
|
|
142
|
+
expect(headers['Access-Control-Expose-Headers']).toContain('X-RateLimit-Remaining');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('returns empty object for disallowed origin', () => {
|
|
146
|
+
const headers = generateCorsHeaders({
|
|
147
|
+
origin: 'https://evil.com',
|
|
148
|
+
allowedOrigins: ['https://example.com'],
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(headers['Access-Control-Allow-Origin']).toBeUndefined();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('handlePreflight', () => {
|
|
156
|
+
it('returns correct headers for OPTIONS request', () => {
|
|
157
|
+
const headers = handlePreflight({
|
|
158
|
+
origin: 'https://example.com',
|
|
159
|
+
requestMethod: 'POST',
|
|
160
|
+
requestHeaders: ['Content-Type', 'Authorization'],
|
|
161
|
+
allowedOrigins: ['https://example.com'],
|
|
162
|
+
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
163
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(headers['Access-Control-Allow-Origin']).toBe('https://example.com');
|
|
167
|
+
expect(headers['Access-Control-Allow-Methods']).toContain('POST');
|
|
168
|
+
expect(headers['Access-Control-Allow-Headers']).toContain('Content-Type');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('sets Access-Control-Max-Age', () => {
|
|
172
|
+
const headers = handlePreflight({
|
|
173
|
+
origin: 'https://example.com',
|
|
174
|
+
requestMethod: 'POST',
|
|
175
|
+
allowedOrigins: ['https://example.com'],
|
|
176
|
+
allowedMethods: ['POST'],
|
|
177
|
+
maxAge: 86400,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(headers['Access-Control-Max-Age']).toBe('86400');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('rejects disallowed method', () => {
|
|
184
|
+
const headers = handlePreflight({
|
|
185
|
+
origin: 'https://example.com',
|
|
186
|
+
requestMethod: 'DELETE',
|
|
187
|
+
allowedOrigins: ['https://example.com'],
|
|
188
|
+
allowedMethods: ['GET', 'POST'],
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
expect(headers['Access-Control-Allow-Methods']).not.toContain('DELETE');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('rejects disallowed headers', () => {
|
|
195
|
+
const headers = handlePreflight({
|
|
196
|
+
origin: 'https://example.com',
|
|
197
|
+
requestMethod: 'POST',
|
|
198
|
+
requestHeaders: ['X-Custom-Header'],
|
|
199
|
+
allowedOrigins: ['https://example.com'],
|
|
200
|
+
allowedMethods: ['POST'],
|
|
201
|
+
allowedHeaders: ['Content-Type'],
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(headers['Access-Control-Allow-Headers']).not.toContain('X-Custom-Header');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('always allows simple headers', () => {
|
|
208
|
+
const headers = handlePreflight({
|
|
209
|
+
origin: 'https://example.com',
|
|
210
|
+
requestMethod: 'POST',
|
|
211
|
+
requestHeaders: ['Accept', 'Content-Language'],
|
|
212
|
+
allowedOrigins: ['https://example.com'],
|
|
213
|
+
allowedMethods: ['POST'],
|
|
214
|
+
allowedHeaders: [],
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Simple headers should be implicitly allowed
|
|
218
|
+
expect(headers['Access-Control-Allow-Headers']).toBeDefined();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('createCorsValidator', () => {
|
|
223
|
+
it('creates reusable validator with config', () => {
|
|
224
|
+
const cors = createCorsValidator({
|
|
225
|
+
allowedOrigins: ['https://example.com', 'https://app.example.com'],
|
|
226
|
+
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE'],
|
|
227
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
228
|
+
credentials: true,
|
|
229
|
+
maxAge: 86400,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const result = cors.validate('https://example.com');
|
|
233
|
+
expect(result.allowed).toBe(true);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('validates methods', () => {
|
|
237
|
+
const cors = createCorsValidator({
|
|
238
|
+
allowedOrigins: ['https://example.com'],
|
|
239
|
+
allowedMethods: ['GET', 'POST'],
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
expect(cors.isMethodAllowed('GET')).toBe(true);
|
|
243
|
+
expect(cors.isMethodAllowed('DELETE')).toBe(false);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('validates headers', () => {
|
|
247
|
+
const cors = createCorsValidator({
|
|
248
|
+
allowedOrigins: ['https://example.com'],
|
|
249
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(cors.isHeaderAllowed('Content-Type')).toBe(true);
|
|
253
|
+
expect(cors.isHeaderAllowed('X-Custom')).toBe(false);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('generates middleware function', () => {
|
|
257
|
+
const cors = createCorsValidator({
|
|
258
|
+
allowedOrigins: ['https://example.com'],
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const middleware = cors.middleware();
|
|
262
|
+
expect(typeof middleware).toBe('function');
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('security edge cases', () => {
|
|
267
|
+
it('rejects origins with credentials in URL', () => {
|
|
268
|
+
const result = validateOrigin('https://user:pass@example.com', {
|
|
269
|
+
allowedOrigins: ['https://example.com'],
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
expect(result.allowed).toBe(false);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('handles case-insensitive origin matching', () => {
|
|
276
|
+
const result = validateOrigin('https://EXAMPLE.COM', {
|
|
277
|
+
allowedOrigins: ['https://example.com'],
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
expect(result.allowed).toBe(true);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('rejects origins with trailing slashes', () => {
|
|
284
|
+
const result = validateOrigin('https://example.com/', {
|
|
285
|
+
allowedOrigins: ['https://example.com'],
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Origins should not have trailing slashes
|
|
289
|
+
expect(result.allowed).toBe(false);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('rejects origins with path components', () => {
|
|
293
|
+
const result = validateOrigin('https://example.com/path', {
|
|
294
|
+
allowedOrigins: ['https://example.com'],
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
expect(result.allowed).toBe(false);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('validates against regex patterns safely', () => {
|
|
301
|
+
// Ensure regex patterns don't cause ReDoS
|
|
302
|
+
const result = validateOrigin('https://a'.repeat(1000) + '.example.com', {
|
|
303
|
+
allowedOrigins: ['https://*.example.com'],
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Should complete quickly and reject
|
|
307
|
+
expect(result.allowed).toBe(false);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
});
|