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,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request Validator
|
|
3
|
+
* Validates request size, content type, JSON depth, and prevents path traversal
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const REQUEST_LIMITS = {
|
|
7
|
+
MAX_BODY_SIZE: 10 * 1024 * 1024, // 10MB
|
|
8
|
+
MAX_JSON_DEPTH: 10,
|
|
9
|
+
MAX_QUERY_LENGTH: 2048,
|
|
10
|
+
MAX_HEADER_SIZE: 8192,
|
|
11
|
+
MAX_PARAMS: 100,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validate request size against limit
|
|
16
|
+
*/
|
|
17
|
+
export function validateRequestSize(options) {
|
|
18
|
+
const { contentLength, maxSize = REQUEST_LIMITS.MAX_BODY_SIZE } = options;
|
|
19
|
+
|
|
20
|
+
if (contentLength === undefined || contentLength === null) {
|
|
21
|
+
return {
|
|
22
|
+
valid: true,
|
|
23
|
+
warning: 'Content-Length header not provided',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (contentLength > maxSize) {
|
|
28
|
+
return {
|
|
29
|
+
valid: false,
|
|
30
|
+
error: `Request size ${contentLength} exceeds maximum allowed size of ${maxSize}`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { valid: true };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Validate content type against allowed types
|
|
39
|
+
*/
|
|
40
|
+
export function validateContentType(options) {
|
|
41
|
+
const { contentType, allowedTypes } = options;
|
|
42
|
+
|
|
43
|
+
if (!contentType) {
|
|
44
|
+
return {
|
|
45
|
+
valid: false,
|
|
46
|
+
error: 'Content-Type header is required',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Extract base content type (without charset, etc.)
|
|
51
|
+
const baseType = contentType.split(';')[0].trim().toLowerCase();
|
|
52
|
+
|
|
53
|
+
const isAllowed = allowedTypes.some((allowed) => {
|
|
54
|
+
const allowedLower = allowed.toLowerCase();
|
|
55
|
+
|
|
56
|
+
// Handle wildcard types (e.g., image/*)
|
|
57
|
+
if (allowedLower.endsWith('/*')) {
|
|
58
|
+
const prefix = allowedLower.slice(0, -1); // Remove *
|
|
59
|
+
return baseType.startsWith(prefix);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return baseType === allowedLower;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!isAllowed) {
|
|
66
|
+
return {
|
|
67
|
+
valid: false,
|
|
68
|
+
error: `Content-Type '${baseType}' is not allowed. Allowed types: ${allowedTypes.join(', ')}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { valid: true };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Calculate depth of a JSON object/array
|
|
77
|
+
*/
|
|
78
|
+
function calculateDepth(obj, currentDepth = 0) {
|
|
79
|
+
if (obj === null || typeof obj !== 'object') {
|
|
80
|
+
return currentDepth;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
currentDepth++;
|
|
84
|
+
|
|
85
|
+
if (Array.isArray(obj)) {
|
|
86
|
+
if (obj.length === 0) return currentDepth;
|
|
87
|
+
return Math.max(...obj.map((item) => calculateDepth(item, currentDepth)));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const values = Object.values(obj);
|
|
91
|
+
if (values.length === 0) return currentDepth;
|
|
92
|
+
return Math.max(...values.map((value) => calculateDepth(value, currentDepth)));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Validate JSON depth against limit
|
|
97
|
+
*/
|
|
98
|
+
export function validateJsonDepth(options) {
|
|
99
|
+
const { json, maxDepth = REQUEST_LIMITS.MAX_JSON_DEPTH } = options;
|
|
100
|
+
|
|
101
|
+
const depth = calculateDepth(json);
|
|
102
|
+
|
|
103
|
+
if (depth > maxDepth) {
|
|
104
|
+
return {
|
|
105
|
+
valid: false,
|
|
106
|
+
error: `JSON depth ${depth} exceeds maximum allowed depth of ${maxDepth}`,
|
|
107
|
+
depth,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { valid: true, depth };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Validate query string length and parameters
|
|
116
|
+
*/
|
|
117
|
+
export function validateQueryString(options) {
|
|
118
|
+
const {
|
|
119
|
+
queryString,
|
|
120
|
+
maxLength = REQUEST_LIMITS.MAX_QUERY_LENGTH,
|
|
121
|
+
maxParams = REQUEST_LIMITS.MAX_PARAMS,
|
|
122
|
+
allowDuplicates = true,
|
|
123
|
+
} = options;
|
|
124
|
+
|
|
125
|
+
if (!queryString) {
|
|
126
|
+
return { valid: true };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check length
|
|
130
|
+
if (queryString.length > maxLength) {
|
|
131
|
+
return {
|
|
132
|
+
valid: false,
|
|
133
|
+
error: `query string length ${queryString.length} exceeds maximum of ${maxLength}`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Parse parameters
|
|
138
|
+
const params = queryString.split('&').filter((p) => p.length > 0);
|
|
139
|
+
|
|
140
|
+
// Check parameter count
|
|
141
|
+
if (params.length > maxParams) {
|
|
142
|
+
return {
|
|
143
|
+
valid: false,
|
|
144
|
+
error: `Query string has ${params.length} parameters, exceeds maximum of ${maxParams}`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check for duplicates
|
|
149
|
+
if (!allowDuplicates) {
|
|
150
|
+
const names = params.map((p) => p.split('=')[0]);
|
|
151
|
+
const uniqueNames = new Set(names);
|
|
152
|
+
if (uniqueNames.size !== names.length) {
|
|
153
|
+
return {
|
|
154
|
+
valid: false,
|
|
155
|
+
error: 'Query string contains duplicate parameter names',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { valid: true };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Validate headers size and format
|
|
165
|
+
*/
|
|
166
|
+
export function validateHeaders(options) {
|
|
167
|
+
const {
|
|
168
|
+
headers,
|
|
169
|
+
maxSize = REQUEST_LIMITS.MAX_HEADER_SIZE,
|
|
170
|
+
maxHeaderSize,
|
|
171
|
+
validateNames = false,
|
|
172
|
+
} = options;
|
|
173
|
+
|
|
174
|
+
// Calculate total headers size
|
|
175
|
+
let totalSize = 0;
|
|
176
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
177
|
+
const headerSize = name.length + String(value).length;
|
|
178
|
+
totalSize += headerSize;
|
|
179
|
+
|
|
180
|
+
// Check individual header size
|
|
181
|
+
if (maxHeaderSize && headerSize > maxHeaderSize) {
|
|
182
|
+
return {
|
|
183
|
+
valid: false,
|
|
184
|
+
error: `Header '${name}' size ${headerSize} exceeds maximum of ${maxHeaderSize}`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Validate header name format (RFC 7230)
|
|
189
|
+
if (validateNames) {
|
|
190
|
+
const validHeaderName = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
191
|
+
if (!validHeaderName.test(name)) {
|
|
192
|
+
return {
|
|
193
|
+
valid: false,
|
|
194
|
+
error: `Header name '${name}' contains invalid characters`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (totalSize > maxSize) {
|
|
201
|
+
return {
|
|
202
|
+
valid: false,
|
|
203
|
+
error: `Total header size ${totalSize} exceeds maximum of ${maxSize}`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { valid: true };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Validate path for security issues
|
|
212
|
+
*/
|
|
213
|
+
export function validatePath(options) {
|
|
214
|
+
const { path, allowedPaths } = options;
|
|
215
|
+
|
|
216
|
+
// Decode the path to catch encoded attacks
|
|
217
|
+
let decodedPath;
|
|
218
|
+
try {
|
|
219
|
+
// Double decode to catch double-encoding attacks
|
|
220
|
+
decodedPath = decodeURIComponent(decodeURIComponent(path));
|
|
221
|
+
} catch {
|
|
222
|
+
// If decoding fails, use single decode or original
|
|
223
|
+
try {
|
|
224
|
+
decodedPath = decodeURIComponent(path);
|
|
225
|
+
} catch {
|
|
226
|
+
decodedPath = path;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check for null bytes
|
|
231
|
+
if (path.includes('%00') || path.includes('\0') || decodedPath.includes('\0')) {
|
|
232
|
+
return {
|
|
233
|
+
valid: false,
|
|
234
|
+
error: 'Path contains null byte injection attempt',
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check for path traversal
|
|
239
|
+
if (
|
|
240
|
+
decodedPath.includes('..') ||
|
|
241
|
+
decodedPath.includes('..\\') ||
|
|
242
|
+
path.includes('%2e%2e') ||
|
|
243
|
+
path.includes('%252e')
|
|
244
|
+
) {
|
|
245
|
+
return {
|
|
246
|
+
valid: false,
|
|
247
|
+
error: 'Path contains traversal attempt',
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Check against allowed paths
|
|
252
|
+
if (allowedPaths && allowedPaths.length > 0) {
|
|
253
|
+
const isAllowed = allowedPaths.some((allowed) => {
|
|
254
|
+
if (allowed.endsWith('/*')) {
|
|
255
|
+
const prefix = allowed.slice(0, -1);
|
|
256
|
+
return path.startsWith(prefix);
|
|
257
|
+
}
|
|
258
|
+
return path === allowed || path.startsWith(allowed + '/');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
if (!isAllowed) {
|
|
262
|
+
return {
|
|
263
|
+
valid: false,
|
|
264
|
+
error: `Path '${path}' is not in allowed paths`,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return { valid: true };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Create a request validator instance
|
|
274
|
+
*/
|
|
275
|
+
export function createRequestValidator(config = {}) {
|
|
276
|
+
const {
|
|
277
|
+
maxBodySize = REQUEST_LIMITS.MAX_BODY_SIZE,
|
|
278
|
+
maxJsonDepth = REQUEST_LIMITS.MAX_JSON_DEPTH,
|
|
279
|
+
allowedContentTypes = ['application/json'],
|
|
280
|
+
allowedPaths,
|
|
281
|
+
} = config;
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
validate(request) {
|
|
285
|
+
const errors = [];
|
|
286
|
+
|
|
287
|
+
// Validate size
|
|
288
|
+
const sizeResult = validateRequestSize({
|
|
289
|
+
contentLength: request.contentLength,
|
|
290
|
+
maxSize: maxBodySize,
|
|
291
|
+
});
|
|
292
|
+
if (!sizeResult.valid) {
|
|
293
|
+
errors.push(sizeResult.error);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Validate content type
|
|
297
|
+
if (request.contentType) {
|
|
298
|
+
const contentTypeResult = validateContentType({
|
|
299
|
+
contentType: request.contentType,
|
|
300
|
+
allowedTypes: allowedContentTypes,
|
|
301
|
+
});
|
|
302
|
+
if (!contentTypeResult.valid) {
|
|
303
|
+
errors.push(contentTypeResult.error);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Validate path
|
|
308
|
+
if (request.path) {
|
|
309
|
+
const pathResult = validatePath({
|
|
310
|
+
path: request.path,
|
|
311
|
+
allowedPaths,
|
|
312
|
+
});
|
|
313
|
+
if (!pathResult.valid) {
|
|
314
|
+
errors.push(pathResult.error);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Validate JSON depth
|
|
319
|
+
if (request.body && typeof request.body === 'object') {
|
|
320
|
+
const jsonResult = validateJsonDepth({
|
|
321
|
+
json: request.body,
|
|
322
|
+
maxDepth: maxJsonDepth,
|
|
323
|
+
});
|
|
324
|
+
if (!jsonResult.valid) {
|
|
325
|
+
errors.push(jsonResult.error);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
valid: errors.length === 0,
|
|
331
|
+
errors,
|
|
332
|
+
};
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
validateSize(options) {
|
|
336
|
+
return validateRequestSize({ ...options, maxSize: maxBodySize });
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
validateContentType(options) {
|
|
340
|
+
return validateContentType({ ...options, allowedTypes: allowedContentTypes });
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
validateJson(json) {
|
|
344
|
+
return validateJsonDepth({ json, maxDepth: maxJsonDepth });
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
validatePath(path) {
|
|
348
|
+
return validatePath({ path, allowedPaths });
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request Validator Tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
validateRequestSize,
|
|
7
|
+
validateContentType,
|
|
8
|
+
validateJsonDepth,
|
|
9
|
+
validateQueryString,
|
|
10
|
+
validateHeaders,
|
|
11
|
+
validatePath,
|
|
12
|
+
REQUEST_LIMITS,
|
|
13
|
+
createRequestValidator,
|
|
14
|
+
} from './request-validator.js';
|
|
15
|
+
|
|
16
|
+
describe('request-validator', () => {
|
|
17
|
+
describe('REQUEST_LIMITS', () => {
|
|
18
|
+
it('defines default limits', () => {
|
|
19
|
+
expect(REQUEST_LIMITS.MAX_BODY_SIZE).toBeDefined();
|
|
20
|
+
expect(REQUEST_LIMITS.MAX_JSON_DEPTH).toBeDefined();
|
|
21
|
+
expect(REQUEST_LIMITS.MAX_QUERY_LENGTH).toBeDefined();
|
|
22
|
+
expect(REQUEST_LIMITS.MAX_HEADER_SIZE).toBeDefined();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('validateRequestSize', () => {
|
|
27
|
+
it('accepts requests under limit', () => {
|
|
28
|
+
const result = validateRequestSize({
|
|
29
|
+
contentLength: 1000,
|
|
30
|
+
maxSize: 10000,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(result.valid).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('rejects oversized requests', () => {
|
|
37
|
+
const result = validateRequestSize({
|
|
38
|
+
contentLength: 20000,
|
|
39
|
+
maxSize: 10000,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(result.valid).toBe(false);
|
|
43
|
+
expect(result.error).toContain('size');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('uses default limit when not specified', () => {
|
|
47
|
+
const result = validateRequestSize({
|
|
48
|
+
contentLength: 1000,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(result.valid).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('handles missing content-length', () => {
|
|
55
|
+
const result = validateRequestSize({
|
|
56
|
+
maxSize: 10000,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(result.valid).toBe(true);
|
|
60
|
+
expect(result.warning).toContain('Content-Length');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('validateContentType', () => {
|
|
65
|
+
it('accepts valid JSON content type', () => {
|
|
66
|
+
const result = validateContentType({
|
|
67
|
+
contentType: 'application/json',
|
|
68
|
+
allowedTypes: ['application/json'],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(result.valid).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('accepts content type with charset', () => {
|
|
75
|
+
const result = validateContentType({
|
|
76
|
+
contentType: 'application/json; charset=utf-8',
|
|
77
|
+
allowedTypes: ['application/json'],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(result.valid).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('rejects invalid content type', () => {
|
|
84
|
+
const result = validateContentType({
|
|
85
|
+
contentType: 'text/plain',
|
|
86
|
+
allowedTypes: ['application/json'],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(result.valid).toBe(false);
|
|
90
|
+
expect(result.error).toContain('Content-Type');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('supports multiple allowed types', () => {
|
|
94
|
+
const result = validateContentType({
|
|
95
|
+
contentType: 'application/xml',
|
|
96
|
+
allowedTypes: ['application/json', 'application/xml'],
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(result.valid).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('handles wildcard types', () => {
|
|
103
|
+
const result = validateContentType({
|
|
104
|
+
contentType: 'image/png',
|
|
105
|
+
allowedTypes: ['image/*'],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(result.valid).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('validateJsonDepth', () => {
|
|
113
|
+
it('accepts JSON within depth limit', () => {
|
|
114
|
+
const json = { a: { b: { c: 'value' } } };
|
|
115
|
+
const result = validateJsonDepth({
|
|
116
|
+
json,
|
|
117
|
+
maxDepth: 5,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(result.valid).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('rejects deeply nested JSON', () => {
|
|
124
|
+
const json = { a: { b: { c: { d: { e: { f: 'value' } } } } } };
|
|
125
|
+
const result = validateJsonDepth({
|
|
126
|
+
json,
|
|
127
|
+
maxDepth: 3,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(result.valid).toBe(false);
|
|
131
|
+
expect(result.error).toContain('depth');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('handles arrays in depth calculation', () => {
|
|
135
|
+
const json = { a: [{ b: [{ c: 'value' }] }] };
|
|
136
|
+
const result = validateJsonDepth({
|
|
137
|
+
json,
|
|
138
|
+
maxDepth: 5,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(result.valid).toBe(true);
|
|
142
|
+
expect(result.depth).toBe(5);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('returns actual depth', () => {
|
|
146
|
+
const json = { a: { b: 'value' } };
|
|
147
|
+
const result = validateJsonDepth({
|
|
148
|
+
json,
|
|
149
|
+
maxDepth: 10,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(result.depth).toBe(2);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('validateQueryString', () => {
|
|
157
|
+
it('accepts query string under length limit', () => {
|
|
158
|
+
const result = validateQueryString({
|
|
159
|
+
queryString: 'foo=bar&baz=qux',
|
|
160
|
+
maxLength: 1000,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(result.valid).toBe(true);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('rejects query string over length limit', () => {
|
|
167
|
+
const result = validateQueryString({
|
|
168
|
+
queryString: 'a'.repeat(2000),
|
|
169
|
+
maxLength: 1000,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(result.valid).toBe(false);
|
|
173
|
+
expect(result.error).toContain('query');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('limits parameter count', () => {
|
|
177
|
+
const params = Array.from({ length: 200 }, (_, i) => `p${i}=v${i}`).join('&');
|
|
178
|
+
const result = validateQueryString({
|
|
179
|
+
queryString: params,
|
|
180
|
+
maxParams: 100,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(result.valid).toBe(false);
|
|
184
|
+
expect(result.error).toContain('parameters');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('detects duplicate parameters', () => {
|
|
188
|
+
const result = validateQueryString({
|
|
189
|
+
queryString: 'foo=bar&foo=baz',
|
|
190
|
+
allowDuplicates: false,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect(result.valid).toBe(false);
|
|
194
|
+
expect(result.error).toContain('duplicate');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('validateHeaders', () => {
|
|
199
|
+
it('accepts headers under size limit', () => {
|
|
200
|
+
const result = validateHeaders({
|
|
201
|
+
headers: {
|
|
202
|
+
'Content-Type': 'application/json',
|
|
203
|
+
'Authorization': 'Bearer token123',
|
|
204
|
+
},
|
|
205
|
+
maxSize: 8000,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(result.valid).toBe(true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('rejects headers over size limit', () => {
|
|
212
|
+
const result = validateHeaders({
|
|
213
|
+
headers: {
|
|
214
|
+
'X-Large-Header': 'a'.repeat(10000),
|
|
215
|
+
},
|
|
216
|
+
maxSize: 8000,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(result.valid).toBe(false);
|
|
220
|
+
expect(result.error).toContain('header');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('limits individual header size', () => {
|
|
224
|
+
const result = validateHeaders({
|
|
225
|
+
headers: {
|
|
226
|
+
'X-Large-Header': 'a'.repeat(5000),
|
|
227
|
+
},
|
|
228
|
+
maxHeaderSize: 4000,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(result.valid).toBe(false);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('validates header name format', () => {
|
|
235
|
+
const result = validateHeaders({
|
|
236
|
+
headers: {
|
|
237
|
+
'Invalid Header Name': 'value',
|
|
238
|
+
},
|
|
239
|
+
validateNames: true,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
expect(result.valid).toBe(false);
|
|
243
|
+
expect(result.error).toContain('invalid');
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
describe('validatePath', () => {
|
|
248
|
+
it('accepts valid paths', () => {
|
|
249
|
+
const result = validatePath({
|
|
250
|
+
path: '/api/users/123',
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(result.valid).toBe(true);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('blocks path traversal attempts', () => {
|
|
257
|
+
const result = validatePath({
|
|
258
|
+
path: '/api/files/../../../etc/passwd',
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
expect(result.valid).toBe(false);
|
|
262
|
+
expect(result.error).toContain('traversal');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('blocks encoded path traversal', () => {
|
|
266
|
+
const result = validatePath({
|
|
267
|
+
path: '/api/files/%2e%2e%2f%2e%2e%2fetc/passwd',
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
expect(result.valid).toBe(false);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('blocks double-encoded traversal', () => {
|
|
274
|
+
const result = validatePath({
|
|
275
|
+
path: '/api/files/%252e%252e%252f',
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
expect(result.valid).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('blocks null bytes', () => {
|
|
282
|
+
const result = validatePath({
|
|
283
|
+
path: '/api/files/test%00.txt',
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(result.valid).toBe(false);
|
|
287
|
+
expect(result.error).toContain('null');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('validates against allowed paths', () => {
|
|
291
|
+
const result = validatePath({
|
|
292
|
+
path: '/admin/secret',
|
|
293
|
+
allowedPaths: ['/api/*', '/public/*'],
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
expect(result.valid).toBe(false);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe('createRequestValidator', () => {
|
|
301
|
+
it('creates validator with methods', () => {
|
|
302
|
+
const validator = createRequestValidator();
|
|
303
|
+
|
|
304
|
+
expect(validator.validate).toBeDefined();
|
|
305
|
+
expect(validator.validateSize).toBeDefined();
|
|
306
|
+
expect(validator.validateContentType).toBeDefined();
|
|
307
|
+
expect(validator.validateJson).toBeDefined();
|
|
308
|
+
expect(validator.validatePath).toBeDefined();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('validates full request', () => {
|
|
312
|
+
const validator = createRequestValidator({
|
|
313
|
+
maxBodySize: 10000,
|
|
314
|
+
maxJsonDepth: 5,
|
|
315
|
+
allowedContentTypes: ['application/json'],
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const result = validator.validate({
|
|
319
|
+
contentLength: 1000,
|
|
320
|
+
contentType: 'application/json',
|
|
321
|
+
path: '/api/users',
|
|
322
|
+
body: { name: 'test' },
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
expect(result.valid).toBe(true);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('returns all validation errors', () => {
|
|
329
|
+
const validator = createRequestValidator({
|
|
330
|
+
maxBodySize: 100,
|
|
331
|
+
maxJsonDepth: 2,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const result = validator.validate({
|
|
335
|
+
contentLength: 10000,
|
|
336
|
+
contentType: 'application/json',
|
|
337
|
+
path: '/api/../etc/passwd',
|
|
338
|
+
body: { a: { b: { c: { d: 'value' } } } },
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
expect(result.valid).toBe(false);
|
|
342
|
+
expect(result.errors.length).toBeGreaterThan(1);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
});
|