nextjs-secure 0.3.0 → 0.6.0

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.
@@ -0,0 +1,2031 @@
1
+ 'use strict';
2
+
3
+ // src/middleware/validation/utils.ts
4
+ function isZodSchema(schema) {
5
+ return typeof schema === "object" && schema !== null && "safeParse" in schema && typeof schema.safeParse === "function";
6
+ }
7
+ function isCustomSchema(schema) {
8
+ if (typeof schema !== "object" || schema === null) return false;
9
+ if ("safeParse" in schema) return false;
10
+ const entries = Object.entries(schema);
11
+ if (entries.length === 0) return false;
12
+ return entries.every(([_, rule]) => {
13
+ return typeof rule === "object" && rule !== null && "type" in rule;
14
+ });
15
+ }
16
+ var EMAIL_PATTERN = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
17
+ var URL_PATTERN = /^https?:\/\/(?:(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}|localhost|(?:\d{1,3}\.){3}\d{1,3})(?::\d{1,5})?(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/;
18
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
19
+ var DATE_PATTERN = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d{3})?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
20
+ function validateField(value, rule, fieldName) {
21
+ if (value === void 0 || value === null || value === "") {
22
+ if (rule.required) {
23
+ return {
24
+ field: fieldName,
25
+ code: "required",
26
+ message: rule.message || `${fieldName} is required`,
27
+ received: value
28
+ };
29
+ }
30
+ return null;
31
+ }
32
+ switch (rule.type) {
33
+ case "string":
34
+ if (typeof value !== "string") {
35
+ return {
36
+ field: fieldName,
37
+ code: "invalid_type",
38
+ message: rule.message || `${fieldName} must be a string`,
39
+ expected: "string",
40
+ received: typeof value
41
+ };
42
+ }
43
+ if (rule.minLength !== void 0 && value.length < rule.minLength) {
44
+ return {
45
+ field: fieldName,
46
+ code: "too_short",
47
+ message: rule.message || `${fieldName} must be at least ${rule.minLength} characters`,
48
+ received: value.length
49
+ };
50
+ }
51
+ if (rule.maxLength !== void 0 && value.length > rule.maxLength) {
52
+ return {
53
+ field: fieldName,
54
+ code: "too_long",
55
+ message: rule.message || `${fieldName} must be at most ${rule.maxLength} characters`,
56
+ received: value.length
57
+ };
58
+ }
59
+ if (rule.pattern && !rule.pattern.test(value)) {
60
+ return {
61
+ field: fieldName,
62
+ code: "invalid_pattern",
63
+ message: rule.message || `${fieldName} has invalid format`,
64
+ received: value
65
+ };
66
+ }
67
+ break;
68
+ case "number":
69
+ const num = typeof value === "number" ? value : Number(value);
70
+ if (isNaN(num)) {
71
+ return {
72
+ field: fieldName,
73
+ code: "invalid_type",
74
+ message: rule.message || `${fieldName} must be a number`,
75
+ expected: "number",
76
+ received: typeof value
77
+ };
78
+ }
79
+ if (rule.integer && !Number.isInteger(num)) {
80
+ return {
81
+ field: fieldName,
82
+ code: "invalid_integer",
83
+ message: rule.message || `${fieldName} must be an integer`,
84
+ received: num
85
+ };
86
+ }
87
+ if (rule.min !== void 0 && num < rule.min) {
88
+ return {
89
+ field: fieldName,
90
+ code: "too_small",
91
+ message: rule.message || `${fieldName} must be at least ${rule.min}`,
92
+ received: num
93
+ };
94
+ }
95
+ if (rule.max !== void 0 && num > rule.max) {
96
+ return {
97
+ field: fieldName,
98
+ code: "too_large",
99
+ message: rule.message || `${fieldName} must be at most ${rule.max}`,
100
+ received: num
101
+ };
102
+ }
103
+ break;
104
+ case "boolean":
105
+ if (typeof value !== "boolean" && value !== "true" && value !== "false") {
106
+ return {
107
+ field: fieldName,
108
+ code: "invalid_type",
109
+ message: rule.message || `${fieldName} must be a boolean`,
110
+ expected: "boolean",
111
+ received: typeof value
112
+ };
113
+ }
114
+ break;
115
+ case "email":
116
+ if (typeof value !== "string" || !EMAIL_PATTERN.test(value)) {
117
+ return {
118
+ field: fieldName,
119
+ code: "invalid_email",
120
+ message: rule.message || `${fieldName} must be a valid email address`,
121
+ received: value
122
+ };
123
+ }
124
+ break;
125
+ case "url":
126
+ if (typeof value !== "string" || !URL_PATTERN.test(value)) {
127
+ return {
128
+ field: fieldName,
129
+ code: "invalid_url",
130
+ message: rule.message || `${fieldName} must be a valid URL`,
131
+ received: value
132
+ };
133
+ }
134
+ break;
135
+ case "uuid":
136
+ if (typeof value !== "string" || !UUID_PATTERN.test(value)) {
137
+ return {
138
+ field: fieldName,
139
+ code: "invalid_uuid",
140
+ message: rule.message || `${fieldName} must be a valid UUID`,
141
+ received: value
142
+ };
143
+ }
144
+ break;
145
+ case "date":
146
+ if (typeof value !== "string" || !DATE_PATTERN.test(value)) {
147
+ const parsed = new Date(value);
148
+ if (isNaN(parsed.getTime())) {
149
+ return {
150
+ field: fieldName,
151
+ code: "invalid_date",
152
+ message: rule.message || `${fieldName} must be a valid date`,
153
+ received: value
154
+ };
155
+ }
156
+ }
157
+ break;
158
+ case "array":
159
+ if (!Array.isArray(value)) {
160
+ return {
161
+ field: fieldName,
162
+ code: "invalid_type",
163
+ message: rule.message || `${fieldName} must be an array`,
164
+ expected: "array",
165
+ received: typeof value
166
+ };
167
+ }
168
+ if (rule.minItems !== void 0 && value.length < rule.minItems) {
169
+ return {
170
+ field: fieldName,
171
+ code: "too_few_items",
172
+ message: rule.message || `${fieldName} must have at least ${rule.minItems} items`,
173
+ received: value.length
174
+ };
175
+ }
176
+ if (rule.maxItems !== void 0 && value.length > rule.maxItems) {
177
+ return {
178
+ field: fieldName,
179
+ code: "too_many_items",
180
+ message: rule.message || `${fieldName} must have at most ${rule.maxItems} items`,
181
+ received: value.length
182
+ };
183
+ }
184
+ if (rule.items) {
185
+ for (let i = 0; i < value.length; i++) {
186
+ const itemError = validateField(value[i], rule.items, `${fieldName}[${i}]`);
187
+ if (itemError) return itemError;
188
+ }
189
+ }
190
+ break;
191
+ case "object":
192
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
193
+ return {
194
+ field: fieldName,
195
+ code: "invalid_type",
196
+ message: rule.message || `${fieldName} must be an object`,
197
+ expected: "object",
198
+ received: Array.isArray(value) ? "array" : typeof value
199
+ };
200
+ }
201
+ break;
202
+ }
203
+ if (rule.custom) {
204
+ const result = rule.custom(value);
205
+ if (result !== true) {
206
+ return {
207
+ field: fieldName,
208
+ code: "custom_validation",
209
+ message: typeof result === "string" ? result : rule.message || `${fieldName} failed validation`,
210
+ received: value
211
+ };
212
+ }
213
+ }
214
+ return null;
215
+ }
216
+ function validateCustomSchema(data, schema) {
217
+ if (typeof data !== "object" || data === null) {
218
+ return {
219
+ success: false,
220
+ errors: [{
221
+ field: "_root",
222
+ code: "invalid_type",
223
+ message: "Expected an object",
224
+ received: data
225
+ }]
226
+ };
227
+ }
228
+ const errors = [];
229
+ const record = data;
230
+ for (const [fieldName, rule] of Object.entries(schema)) {
231
+ const error = validateField(record[fieldName], rule, fieldName);
232
+ if (error) {
233
+ errors.push(error);
234
+ }
235
+ }
236
+ if (errors.length > 0) {
237
+ return { success: false, errors };
238
+ }
239
+ return { success: true, data };
240
+ }
241
+ function validateZodSchema(data, schema) {
242
+ const result = schema.safeParse(data);
243
+ if (result.success) {
244
+ return { success: true, data: result.data };
245
+ }
246
+ const errors = result.error.issues.map((issue) => ({
247
+ field: issue.path.join(".") || "_root",
248
+ code: issue.code,
249
+ message: issue.message,
250
+ path: issue.path.map(String)
251
+ }));
252
+ return { success: false, errors };
253
+ }
254
+ function getByPath(obj, path) {
255
+ if (typeof obj !== "object" || obj === null) return void 0;
256
+ const parts = path.split(".");
257
+ let current = obj;
258
+ for (const part of parts) {
259
+ if (typeof current !== "object" || current === null) return void 0;
260
+ current = current[part];
261
+ }
262
+ return current;
263
+ }
264
+ function setByPath(obj, path, value) {
265
+ const parts = path.split(".");
266
+ let current = obj;
267
+ for (let i = 0; i < parts.length - 1; i++) {
268
+ const part = parts[i];
269
+ if (!(part in current) || typeof current[part] !== "object") {
270
+ current[part] = {};
271
+ }
272
+ current = current[part];
273
+ }
274
+ current[parts[parts.length - 1]] = value;
275
+ }
276
+ function walkObject(obj, fn, path = "") {
277
+ if (typeof obj === "string") {
278
+ return fn(obj, path);
279
+ }
280
+ if (Array.isArray(obj)) {
281
+ return obj.map((item, i) => walkObject(item, fn, `${path}[${i}]`));
282
+ }
283
+ if (typeof obj === "object" && obj !== null) {
284
+ const result = {};
285
+ for (const [key, value] of Object.entries(obj)) {
286
+ const newPath = path ? `${path}.${key}` : key;
287
+ result[key] = walkObject(value, fn, newPath);
288
+ }
289
+ return result;
290
+ }
291
+ return obj;
292
+ }
293
+ function parseQueryString(url) {
294
+ const result = {};
295
+ try {
296
+ const urlObj = new URL(url);
297
+ for (const [key, value] of urlObj.searchParams.entries()) {
298
+ if (key in result) {
299
+ const existing = result[key];
300
+ if (Array.isArray(existing)) {
301
+ existing.push(value);
302
+ } else {
303
+ result[key] = [existing, value];
304
+ }
305
+ } else {
306
+ result[key] = value;
307
+ }
308
+ }
309
+ } catch {
310
+ }
311
+ return result;
312
+ }
313
+
314
+ // src/middleware/validation/validators/schema.ts
315
+ function validate(data, schema) {
316
+ if (isZodSchema(schema)) {
317
+ return validateZodSchema(data, schema);
318
+ }
319
+ if (isCustomSchema(schema)) {
320
+ return validateCustomSchema(data, schema);
321
+ }
322
+ return {
323
+ success: false,
324
+ errors: [{
325
+ field: "_schema",
326
+ code: "invalid_schema",
327
+ message: "Invalid schema provided"
328
+ }]
329
+ };
330
+ }
331
+ async function validateBody(request, schema) {
332
+ let body;
333
+ try {
334
+ const contentType = request.headers.get("content-type") || "";
335
+ if (contentType.includes("application/json")) {
336
+ body = await request.json();
337
+ } else if (contentType.includes("application/x-www-form-urlencoded")) {
338
+ const text = await request.text();
339
+ body = Object.fromEntries(new URLSearchParams(text));
340
+ } else if (contentType.includes("multipart/form-data")) {
341
+ const formData = await request.formData();
342
+ const obj = {};
343
+ formData.forEach((value, key) => {
344
+ if (typeof value === "string") {
345
+ obj[key] = value;
346
+ }
347
+ });
348
+ body = obj;
349
+ } else {
350
+ try {
351
+ body = await request.json();
352
+ } catch {
353
+ body = {};
354
+ }
355
+ }
356
+ } catch (error) {
357
+ return {
358
+ success: false,
359
+ errors: [{
360
+ field: "_body",
361
+ code: "parse_error",
362
+ message: "Failed to parse request body"
363
+ }]
364
+ };
365
+ }
366
+ return validate(body, schema);
367
+ }
368
+ function validateQuery(request, schema) {
369
+ const query = parseQueryString(request.url);
370
+ return validate(query, schema);
371
+ }
372
+ function validateParams(params, schema) {
373
+ return validate(params, schema);
374
+ }
375
+ async function validateRequest(request, config) {
376
+ const allErrors = [];
377
+ const data = {};
378
+ if (config.body) {
379
+ const bodyResult = await validateBody(request, config.body);
380
+ if (!bodyResult.success) {
381
+ allErrors.push(...(bodyResult.errors || []).map((e) => ({
382
+ ...e,
383
+ field: `body.${e.field}`.replace("body._root", "body")
384
+ })));
385
+ } else {
386
+ data.body = bodyResult.data;
387
+ }
388
+ } else {
389
+ data.body = {};
390
+ }
391
+ if (config.query) {
392
+ const queryResult = validateQuery(request, config.query);
393
+ if (!queryResult.success) {
394
+ allErrors.push(...(queryResult.errors || []).map((e) => ({
395
+ ...e,
396
+ field: `query.${e.field}`.replace("query._root", "query")
397
+ })));
398
+ } else {
399
+ data.query = queryResult.data;
400
+ }
401
+ } else {
402
+ data.query = {};
403
+ }
404
+ if (config.params && config.routeParams) {
405
+ const paramsResult = validateParams(config.routeParams, config.params);
406
+ if (!paramsResult.success) {
407
+ allErrors.push(...(paramsResult.errors || []).map((e) => ({
408
+ ...e,
409
+ field: `params.${e.field}`.replace("params._root", "params")
410
+ })));
411
+ } else {
412
+ data.params = paramsResult.data;
413
+ }
414
+ } else {
415
+ data.params = {};
416
+ }
417
+ if (allErrors.length > 0) {
418
+ return { success: false, errors: allErrors };
419
+ }
420
+ return {
421
+ success: true,
422
+ data
423
+ };
424
+ }
425
+ function defaultValidationErrorResponse(errors) {
426
+ return new Response(
427
+ JSON.stringify({
428
+ error: "validation_error",
429
+ message: "Request validation failed",
430
+ details: errors.map((e) => ({
431
+ field: e.field,
432
+ code: e.code,
433
+ message: e.message
434
+ }))
435
+ }),
436
+ {
437
+ status: 400,
438
+ headers: { "Content-Type": "application/json" }
439
+ }
440
+ );
441
+ }
442
+ function createValidator(schema) {
443
+ return (data) => validate(data, schema);
444
+ }
445
+ function allValid(...results) {
446
+ return results.every((r) => r.success);
447
+ }
448
+ function mergeErrors(...results) {
449
+ const errors = [];
450
+ for (const result of results) {
451
+ if (result.errors) {
452
+ errors.push(...result.errors);
453
+ }
454
+ }
455
+ return errors;
456
+ }
457
+
458
+ // src/middleware/validation/validators/content-type.ts
459
+ var MIME_TYPES = {
460
+ // Text
461
+ TEXT_PLAIN: "text/plain",
462
+ TEXT_HTML: "text/html",
463
+ TEXT_CSS: "text/css",
464
+ TEXT_JAVASCRIPT: "text/javascript",
465
+ // Application
466
+ JSON: "application/json",
467
+ FORM_URLENCODED: "application/x-www-form-urlencoded",
468
+ MULTIPART_FORM: "multipart/form-data",
469
+ XML: "application/xml",
470
+ PDF: "application/pdf",
471
+ ZIP: "application/zip",
472
+ GZIP: "application/gzip",
473
+ OCTET_STREAM: "application/octet-stream",
474
+ // Image
475
+ IMAGE_PNG: "image/png",
476
+ IMAGE_JPEG: "image/jpeg",
477
+ IMAGE_GIF: "image/gif",
478
+ IMAGE_WEBP: "image/webp",
479
+ IMAGE_SVG: "image/svg+xml",
480
+ // Audio
481
+ AUDIO_MP3: "audio/mpeg",
482
+ AUDIO_WAV: "audio/wav",
483
+ AUDIO_OGG: "audio/ogg",
484
+ // Video
485
+ VIDEO_MP4: "video/mp4",
486
+ VIDEO_WEBM: "video/webm"
487
+ };
488
+ function parseContentType(header) {
489
+ if (!header) {
490
+ return {
491
+ type: "",
492
+ subtype: "",
493
+ mediaType: "",
494
+ parameters: {}
495
+ };
496
+ }
497
+ const parts = header.split(";").map((p) => p.trim());
498
+ const mediaType = parts[0].toLowerCase();
499
+ const [type = "", subtype = ""] = mediaType.split("/");
500
+ const parameters = {};
501
+ for (let i = 1; i < parts.length; i++) {
502
+ const [key, value] = parts[i].split("=").map((p) => p.trim());
503
+ if (key && value) {
504
+ parameters[key.toLowerCase()] = value.replace(/^["']|["']$/g, "");
505
+ }
506
+ }
507
+ return {
508
+ type,
509
+ subtype,
510
+ mediaType,
511
+ charset: parameters["charset"],
512
+ boundary: parameters["boundary"],
513
+ parameters
514
+ };
515
+ }
516
+ function isAllowedContentType(contentType, allowedTypes, strict = false) {
517
+ if (!contentType) {
518
+ return !strict;
519
+ }
520
+ const { mediaType } = parseContentType(contentType);
521
+ return allowedTypes.some((allowed) => {
522
+ const normalizedAllowed = allowed.toLowerCase().trim();
523
+ if (mediaType === normalizedAllowed) {
524
+ return true;
525
+ }
526
+ if (normalizedAllowed.endsWith("/*")) {
527
+ const prefix = normalizedAllowed.slice(0, -2);
528
+ return mediaType.startsWith(prefix + "/");
529
+ }
530
+ if (!normalizedAllowed.includes("/")) {
531
+ const { type } = parseContentType(contentType);
532
+ return type === normalizedAllowed;
533
+ }
534
+ return false;
535
+ });
536
+ }
537
+ function validateContentType(request, config) {
538
+ const contentType = request.headers.get("content-type");
539
+ const { allowed, strict = false, charset } = config;
540
+ if (strict && !contentType) {
541
+ return {
542
+ valid: false,
543
+ contentType: null,
544
+ reason: "Content-Type header is required"
545
+ };
546
+ }
547
+ if (contentType && !isAllowedContentType(contentType, allowed, strict)) {
548
+ return {
549
+ valid: false,
550
+ contentType,
551
+ reason: `Content-Type '${contentType}' is not allowed`
552
+ };
553
+ }
554
+ if (charset && contentType) {
555
+ const parsed = parseContentType(contentType);
556
+ if (parsed.charset && parsed.charset.toLowerCase() !== charset.toLowerCase()) {
557
+ return {
558
+ valid: false,
559
+ contentType,
560
+ reason: `Charset '${parsed.charset}' is not allowed, expected '${charset}'`
561
+ };
562
+ }
563
+ }
564
+ return { valid: true, contentType };
565
+ }
566
+ function defaultContentTypeErrorResponse(contentType, reason) {
567
+ return new Response(
568
+ JSON.stringify({
569
+ error: "invalid_content_type",
570
+ message: reason,
571
+ received: contentType
572
+ }),
573
+ {
574
+ status: 415,
575
+ // Unsupported Media Type
576
+ headers: { "Content-Type": "application/json" }
577
+ }
578
+ );
579
+ }
580
+ function isJsonRequest(request) {
581
+ return isAllowedContentType(
582
+ request.headers.get("content-type"),
583
+ [MIME_TYPES.JSON]
584
+ );
585
+ }
586
+ function isFormRequest(request) {
587
+ return isAllowedContentType(
588
+ request.headers.get("content-type"),
589
+ [MIME_TYPES.FORM_URLENCODED, MIME_TYPES.MULTIPART_FORM]
590
+ );
591
+ }
592
+ function isMultipartRequest(request) {
593
+ return isAllowedContentType(
594
+ request.headers.get("content-type"),
595
+ [MIME_TYPES.MULTIPART_FORM]
596
+ );
597
+ }
598
+ function getMultipartBoundary(request) {
599
+ const contentType = request.headers.get("content-type");
600
+ if (!contentType) return null;
601
+ const { boundary } = parseContentType(contentType);
602
+ return boundary || null;
603
+ }
604
+
605
+ // src/middleware/validation/sanitizers/path.ts
606
+ var DANGEROUS_PATTERNS = [
607
+ // Unix path traversal
608
+ /\.\.\//g,
609
+ /\.\./g,
610
+ // Windows path traversal
611
+ /\.\.\\/g,
612
+ // Null byte (can truncate paths in some systems)
613
+ /%00/g,
614
+ /\0/g,
615
+ // URL encoded traversal
616
+ /%2e%2e%2f/gi,
617
+ // ../
618
+ /%2e%2e\//gi,
619
+ // ../
620
+ /%2e%2e%5c/gi,
621
+ // ..\
622
+ /%2e%2e\\/gi,
623
+ // ..\
624
+ // Double URL encoding
625
+ /%252e%252e%252f/gi,
626
+ /%252e%252e%255c/gi,
627
+ // Unicode encoding
628
+ /\.%u002e\//gi,
629
+ /%u002e%u002e%u002f/gi,
630
+ // Overlong UTF-8 encoding
631
+ /%c0%ae%c0%ae%c0%af/gi,
632
+ /%c1%9c/gi
633
+ // Backslash variant
634
+ ];
635
+ var DEFAULT_BLOCKED_EXTENSIONS = [
636
+ ".exe",
637
+ ".dll",
638
+ ".so",
639
+ ".dylib",
640
+ // Executables
641
+ ".sh",
642
+ ".bash",
643
+ ".bat",
644
+ ".cmd",
645
+ ".ps1",
646
+ // Scripts
647
+ ".php",
648
+ ".asp",
649
+ ".aspx",
650
+ ".jsp",
651
+ ".cgi",
652
+ // Server scripts
653
+ ".htaccess",
654
+ ".htpasswd",
655
+ // Apache config
656
+ ".env",
657
+ ".git",
658
+ ".svn"
659
+ // Config/VCS
660
+ ];
661
+ function normalizePathSeparators(path) {
662
+ return path.replace(/\\/g, "/");
663
+ }
664
+ function decodePathComponent(path) {
665
+ let result = path;
666
+ let previous = "";
667
+ while (result !== previous) {
668
+ previous = result;
669
+ try {
670
+ result = decodeURIComponent(result);
671
+ } catch {
672
+ break;
673
+ }
674
+ }
675
+ return result;
676
+ }
677
+ function hasPathTraversal(path) {
678
+ if (!path || typeof path !== "string") return false;
679
+ const normalized = normalizePathSeparators(decodePathComponent(path));
680
+ for (const pattern of DANGEROUS_PATTERNS) {
681
+ pattern.lastIndex = 0;
682
+ if (pattern.test(normalized)) {
683
+ return true;
684
+ }
685
+ }
686
+ if (normalized.includes("..")) {
687
+ return true;
688
+ }
689
+ return false;
690
+ }
691
+ function validatePath(path, config = {}) {
692
+ if (!path || typeof path !== "string") {
693
+ return { valid: false, reason: "Path is empty or not a string" };
694
+ }
695
+ const {
696
+ allowAbsolute = false,
697
+ allowedPrefixes = [],
698
+ allowedExtensions,
699
+ blockedExtensions = DEFAULT_BLOCKED_EXTENSIONS,
700
+ maxDepth = 10,
701
+ maxLength = 255,
702
+ normalize = true
703
+ } = config;
704
+ if (path.length > maxLength) {
705
+ return { valid: false, reason: `Path exceeds maximum length of ${maxLength}` };
706
+ }
707
+ let normalized = decodePathComponent(path);
708
+ if (normalize) {
709
+ normalized = normalizePathSeparators(normalized);
710
+ }
711
+ if (normalized.includes("\0") || path.includes("%00")) {
712
+ return { valid: false, reason: "Path contains null bytes" };
713
+ }
714
+ if (hasPathTraversal(path)) {
715
+ return { valid: false, reason: "Path contains traversal sequences" };
716
+ }
717
+ const isAbsolute = normalized.startsWith("/") || /^[a-zA-Z]:/.test(normalized) || // Windows drive letter
718
+ normalized.startsWith("\\\\");
719
+ if (isAbsolute && !allowAbsolute) {
720
+ return { valid: false, reason: "Absolute paths are not allowed" };
721
+ }
722
+ if (allowedPrefixes.length > 0) {
723
+ const hasValidPrefix = allowedPrefixes.some((prefix) => {
724
+ const normalizedPrefix = normalizePathSeparators(prefix);
725
+ return normalized.startsWith(normalizedPrefix);
726
+ });
727
+ if (!hasValidPrefix) {
728
+ return { valid: false, reason: "Path does not start with an allowed prefix" };
729
+ }
730
+ }
731
+ const segments = normalized.split("/").filter((s) => s && s !== ".");
732
+ if (segments.length > maxDepth) {
733
+ return { valid: false, reason: `Path depth exceeds maximum of ${maxDepth}` };
734
+ }
735
+ const lastSegment = segments[segments.length - 1] || "";
736
+ const dotIndex = lastSegment.lastIndexOf(".");
737
+ const extension = dotIndex > 0 ? lastSegment.slice(dotIndex).toLowerCase() : "";
738
+ if (extension && blockedExtensions.length > 0) {
739
+ if (blockedExtensions.map((e) => e.toLowerCase()).includes(extension)) {
740
+ return { valid: false, reason: `Extension ${extension} is not allowed` };
741
+ }
742
+ }
743
+ if (extension && allowedExtensions && allowedExtensions.length > 0) {
744
+ if (!allowedExtensions.map((e) => e.toLowerCase()).includes(extension)) {
745
+ return { valid: false, reason: `Extension ${extension} is not in allowed list` };
746
+ }
747
+ }
748
+ const sanitized = normalized.replace(/\/+/g, "/");
749
+ return { valid: true, sanitized };
750
+ }
751
+ function sanitizePath(path, config = {}) {
752
+ if (!path || typeof path !== "string") return "";
753
+ const { normalize = true, maxLength = 255 } = config;
754
+ let result = decodePathComponent(path);
755
+ if (normalize) {
756
+ result = normalizePathSeparators(result);
757
+ }
758
+ result = result.replace(/\0/g, "").replace(/%00/g, "");
759
+ result = result.replace(/\.\.\//g, "").replace(/\.\.\\/g, "");
760
+ if (!config.allowAbsolute) {
761
+ result = result.replace(/^\/+/, "");
762
+ result = result.replace(/^[a-zA-Z]:/, "");
763
+ result = result.replace(/^\\\\/, "");
764
+ }
765
+ result = result.replace(/\/+/g, "/");
766
+ result = result.replace(/\/+$/, "");
767
+ if (result.length > maxLength) {
768
+ result = result.slice(0, maxLength);
769
+ }
770
+ return result;
771
+ }
772
+ function isPathContained(path, baseDir) {
773
+ if (!path || !baseDir) return false;
774
+ const normalizedPath = normalizePathSeparators(decodePathComponent(path));
775
+ const normalizedBase = normalizePathSeparators(baseDir);
776
+ const resolvedPath = resolvePath(normalizedPath, normalizedBase);
777
+ return resolvedPath.startsWith(normalizedBase.replace(/\/$/, "") + "/");
778
+ }
779
+ function resolvePath(path, base) {
780
+ let combined;
781
+ if (path.startsWith("/")) {
782
+ combined = path;
783
+ } else {
784
+ combined = `${base.replace(/\/$/, "")}/${path}`;
785
+ }
786
+ const segments = [];
787
+ for (const segment of combined.split("/")) {
788
+ if (segment === "" || segment === ".") {
789
+ continue;
790
+ }
791
+ if (segment === "..") {
792
+ segments.pop();
793
+ } else {
794
+ segments.push(segment);
795
+ }
796
+ }
797
+ return "/" + segments.join("/");
798
+ }
799
+ function getExtension(path) {
800
+ if (!path || typeof path !== "string") return "";
801
+ const normalized = normalizePathSeparators(path);
802
+ const segments = normalized.split("/");
803
+ const filename = segments[segments.length - 1] || "";
804
+ const dotIndex = filename.lastIndexOf(".");
805
+ if (dotIndex <= 0) return "";
806
+ return filename.slice(dotIndex).toLowerCase();
807
+ }
808
+ function getFilename(path) {
809
+ if (!path || typeof path !== "string") return "";
810
+ const normalized = normalizePathSeparators(path);
811
+ const segments = normalized.split("/");
812
+ return segments[segments.length - 1] || "";
813
+ }
814
+ function sanitizeFilename(filename) {
815
+ if (typeof filename !== "string") return "file";
816
+ if (!filename) return "file";
817
+ let result = filename;
818
+ result = result.replace(/[/\\]/g, "");
819
+ result = result.replace(/\0/g, "");
820
+ result = result.replace(/[\x00-\x1f\x7f]/g, "");
821
+ result = result.replace(/[<>:"|?*]/g, "");
822
+ result = result.replace(/^[.\s]+|[.\s]+$/g, "");
823
+ if (result.length > 255) {
824
+ const ext = getExtension(result);
825
+ const name = result.slice(0, 255 - ext.length);
826
+ result = name + ext;
827
+ }
828
+ return result || "file";
829
+ }
830
+ function isHiddenPath(path) {
831
+ if (!path) return false;
832
+ const normalized = normalizePathSeparators(path);
833
+ const segments = normalized.split("/").filter(Boolean);
834
+ return segments.some((segment) => segment.startsWith("."));
835
+ }
836
+
837
+ // src/middleware/validation/validators/file.ts
838
+ var MAGIC_NUMBERS = [
839
+ // Images
840
+ { type: "image/jpeg", extension: ".jpg", signature: [255, 216, 255] },
841
+ { type: "image/png", extension: ".png", signature: [137, 80, 78, 71, 13, 10, 26, 10] },
842
+ { type: "image/gif", extension: ".gif", signature: [71, 73, 70, 56] },
843
+ // GIF87a or GIF89a
844
+ { type: "image/webp", extension: ".webp", signature: [82, 73, 70, 70], offset: 0 },
845
+ // RIFF
846
+ { type: "image/bmp", extension: ".bmp", signature: [66, 77] },
847
+ { type: "image/tiff", extension: ".tiff", signature: [73, 73, 42, 0] },
848
+ // Little endian
849
+ { type: "image/tiff", extension: ".tiff", signature: [77, 77, 0, 42] },
850
+ // Big endian
851
+ { type: "image/x-icon", extension: ".ico", signature: [0, 0, 1, 0] },
852
+ { type: "image/svg+xml", extension: ".svg", signature: [60, 63, 120, 109, 108] },
853
+ // <?xml
854
+ // Documents
855
+ { type: "application/pdf", extension: ".pdf", signature: [37, 80, 68, 70] },
856
+ // %PDF
857
+ { type: "application/zip", extension: ".zip", signature: [80, 75, 3, 4] },
858
+ // PK
859
+ { type: "application/gzip", extension: ".gz", signature: [31, 139] },
860
+ { type: "application/x-rar-compressed", extension: ".rar", signature: [82, 97, 114, 33] },
861
+ { type: "application/x-7z-compressed", extension: ".7z", signature: [55, 122, 188, 175, 39, 28] },
862
+ // Microsoft Office (new format - zip based)
863
+ { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", extension: ".xlsx", signature: [80, 75, 3, 4] },
864
+ { type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", extension: ".docx", signature: [80, 75, 3, 4] },
865
+ { type: "application/vnd.openxmlformats-officedocument.presentationml.presentation", extension: ".pptx", signature: [80, 75, 3, 4] },
866
+ // Microsoft Office (old format)
867
+ { type: "application/msword", extension: ".doc", signature: [208, 207, 17, 224, 161, 177, 26, 225] },
868
+ { type: "application/vnd.ms-excel", extension: ".xls", signature: [208, 207, 17, 224, 161, 177, 26, 225] },
869
+ // Audio
870
+ { type: "audio/mpeg", extension: ".mp3", signature: [255, 251] },
871
+ // MP3 frame sync
872
+ { type: "audio/mpeg", extension: ".mp3", signature: [73, 68, 51] },
873
+ // ID3
874
+ { type: "audio/wav", extension: ".wav", signature: [82, 73, 70, 70] },
875
+ // RIFF
876
+ { type: "audio/ogg", extension: ".ogg", signature: [79, 103, 103, 83] },
877
+ { type: "audio/flac", extension: ".flac", signature: [102, 76, 97, 67] },
878
+ // Video
879
+ { type: "video/mp4", extension: ".mp4", signature: [0, 0, 0], offset: 0 },
880
+ // Partial match
881
+ { type: "video/webm", extension: ".webm", signature: [26, 69, 223, 163] },
882
+ { type: "video/avi", extension: ".avi", signature: [82, 73, 70, 70] },
883
+ // RIFF
884
+ { type: "video/quicktime", extension: ".mov", signature: [0, 0, 0, 20, 102, 116, 121, 112] },
885
+ // Web
886
+ { type: "application/wasm", extension: ".wasm", signature: [0, 97, 115, 109] },
887
+ // \0asm
888
+ // Fonts
889
+ { type: "font/woff", extension: ".woff", signature: [119, 79, 70, 70] },
890
+ { type: "font/woff2", extension: ".woff2", signature: [119, 79, 70, 50] }
891
+ ];
892
+ var DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
893
+ var DEFAULT_MAX_FILES = 10;
894
+ var DANGEROUS_EXTENSIONS = [
895
+ ".exe",
896
+ ".dll",
897
+ ".so",
898
+ ".dylib",
899
+ ".bin",
900
+ ".sh",
901
+ ".bash",
902
+ ".bat",
903
+ ".cmd",
904
+ ".ps1",
905
+ ".vbs",
906
+ ".php",
907
+ ".asp",
908
+ ".aspx",
909
+ ".jsp",
910
+ ".cgi",
911
+ ".pl",
912
+ ".py",
913
+ ".rb",
914
+ ".jar",
915
+ ".class",
916
+ ".msi",
917
+ ".dmg",
918
+ ".pkg",
919
+ ".deb",
920
+ ".rpm",
921
+ ".scr",
922
+ ".pif",
923
+ ".com",
924
+ ".hta"
925
+ ];
926
+ function checkMagicNumber(bytes, magicNumber) {
927
+ const offset = magicNumber.offset || 0;
928
+ const signature = magicNumber.signature;
929
+ if (bytes.length < offset + signature.length) {
930
+ return false;
931
+ }
932
+ for (let i = 0; i < signature.length; i++) {
933
+ if (bytes[offset + i] !== signature[i]) {
934
+ return false;
935
+ }
936
+ }
937
+ return true;
938
+ }
939
+ function detectFileType(bytes) {
940
+ for (const magic of MAGIC_NUMBERS) {
941
+ if (checkMagicNumber(bytes, magic)) {
942
+ return { type: magic.type, extension: magic.extension };
943
+ }
944
+ }
945
+ return null;
946
+ }
947
+ async function validateFile(file, config = {}) {
948
+ const {
949
+ maxSize = DEFAULT_MAX_FILE_SIZE,
950
+ minSize = 0,
951
+ allowedTypes = [],
952
+ blockedTypes = [],
953
+ allowedExtensions = [],
954
+ blockedExtensions = DANGEROUS_EXTENSIONS,
955
+ validateMagicNumbers = true,
956
+ sanitizeFilename: doSanitize = true
957
+ } = config;
958
+ const errors = [];
959
+ const extension = getExtension(file.name);
960
+ const info = {
961
+ filename: doSanitize ? sanitizeFilename(file.name) : file.name,
962
+ size: file.size,
963
+ type: file.type,
964
+ extension
965
+ };
966
+ if (file.size > maxSize) {
967
+ errors.push({
968
+ filename: file.name,
969
+ code: "size_exceeded",
970
+ message: `File size (${formatBytes(file.size)}) exceeds maximum allowed (${formatBytes(maxSize)})`,
971
+ details: { size: file.size, maxSize }
972
+ });
973
+ }
974
+ if (file.size < minSize) {
975
+ errors.push({
976
+ filename: file.name,
977
+ code: "size_too_small",
978
+ message: `File size (${formatBytes(file.size)}) is below minimum required (${formatBytes(minSize)})`,
979
+ details: { size: file.size, minSize }
980
+ });
981
+ }
982
+ if (blockedExtensions.length > 0 && extension) {
983
+ if (blockedExtensions.map((e) => e.toLowerCase()).includes(extension.toLowerCase())) {
984
+ errors.push({
985
+ filename: file.name,
986
+ code: "extension_not_allowed",
987
+ message: `File extension '${extension}' is not allowed`,
988
+ details: { extension, blockedExtensions }
989
+ });
990
+ }
991
+ }
992
+ if (allowedExtensions.length > 0 && extension) {
993
+ if (!allowedExtensions.map((e) => e.toLowerCase()).includes(extension.toLowerCase())) {
994
+ errors.push({
995
+ filename: file.name,
996
+ code: "extension_not_allowed",
997
+ message: `File extension '${extension}' is not in allowed list`,
998
+ details: { extension, allowedExtensions }
999
+ });
1000
+ }
1001
+ }
1002
+ if (blockedTypes.length > 0 && file.type) {
1003
+ if (blockedTypes.includes(file.type)) {
1004
+ errors.push({
1005
+ filename: file.name,
1006
+ code: "type_not_allowed",
1007
+ message: `File type '${file.type}' is not allowed`,
1008
+ details: { type: file.type, blockedTypes }
1009
+ });
1010
+ }
1011
+ }
1012
+ if (allowedTypes.length > 0) {
1013
+ if (!allowedTypes.includes(file.type)) {
1014
+ errors.push({
1015
+ filename: file.name,
1016
+ code: "type_not_allowed",
1017
+ message: `File type '${file.type}' is not in allowed list`,
1018
+ details: { type: file.type, allowedTypes }
1019
+ });
1020
+ }
1021
+ }
1022
+ if (validateMagicNumbers && errors.length === 0) {
1023
+ try {
1024
+ const buffer = await file.arrayBuffer();
1025
+ const bytes = new Uint8Array(buffer.slice(0, 32));
1026
+ const detected = detectFileType(bytes);
1027
+ if (detected) {
1028
+ if (file.type && detected.type !== file.type) {
1029
+ const isSimilar = detected.type.startsWith("image/") && file.type.startsWith("image/") || detected.type.startsWith("audio/") && file.type.startsWith("audio/") || detected.type.startsWith("video/") && file.type.startsWith("video/");
1030
+ if (!isSimilar) {
1031
+ errors.push({
1032
+ filename: file.name,
1033
+ code: "invalid_content",
1034
+ message: `File content doesn't match declared type (claimed: ${file.type}, detected: ${detected.type})`,
1035
+ details: { claimed: file.type, detected: detected.type }
1036
+ });
1037
+ }
1038
+ }
1039
+ }
1040
+ } catch {
1041
+ }
1042
+ }
1043
+ return {
1044
+ valid: errors.length === 0,
1045
+ info,
1046
+ errors
1047
+ };
1048
+ }
1049
+ async function validateFiles(files, config = {}) {
1050
+ const { maxFiles = DEFAULT_MAX_FILES } = config;
1051
+ const allErrors = [];
1052
+ const infos = [];
1053
+ if (files.length > maxFiles) {
1054
+ allErrors.push({
1055
+ filename: "",
1056
+ code: "too_many_files",
1057
+ message: `Too many files (${files.length}), maximum allowed is ${maxFiles}`,
1058
+ details: { count: files.length, maxFiles }
1059
+ });
1060
+ }
1061
+ for (const file of files) {
1062
+ const result = await validateFile(file, config);
1063
+ infos.push(result.info);
1064
+ allErrors.push(...result.errors);
1065
+ }
1066
+ return {
1067
+ valid: allErrors.length === 0,
1068
+ infos,
1069
+ errors: allErrors
1070
+ };
1071
+ }
1072
+ function extractFilesFromFormData(formData) {
1073
+ const files = /* @__PURE__ */ new Map();
1074
+ formData.forEach((value, key) => {
1075
+ if (value instanceof File) {
1076
+ const existing = files.get(key) || [];
1077
+ existing.push(value);
1078
+ files.set(key, existing);
1079
+ }
1080
+ });
1081
+ return files;
1082
+ }
1083
+ async function validateFilesFromRequest(request, config = {}) {
1084
+ const contentType = request.headers.get("content-type") || "";
1085
+ if (!contentType.includes("multipart/form-data")) {
1086
+ return { valid: true, files: /* @__PURE__ */ new Map(), errors: [] };
1087
+ }
1088
+ try {
1089
+ const formData = await request.formData();
1090
+ const fileMap = extractFilesFromFormData(formData);
1091
+ const allInfos = /* @__PURE__ */ new Map();
1092
+ const allErrors = [];
1093
+ let totalFileCount = 0;
1094
+ for (const [field, files] of fileMap.entries()) {
1095
+ totalFileCount += files.length;
1096
+ const result = await validateFiles(files, { ...config, maxFiles: Infinity });
1097
+ allInfos.set(field, result.infos);
1098
+ allErrors.push(...result.errors.map((e) => ({ ...e, field })));
1099
+ }
1100
+ const maxFiles = config.maxFiles ?? DEFAULT_MAX_FILES;
1101
+ if (totalFileCount > maxFiles) {
1102
+ allErrors.push({
1103
+ filename: "",
1104
+ code: "too_many_files",
1105
+ message: `Total file count (${totalFileCount}) exceeds maximum (${maxFiles})`,
1106
+ details: { count: totalFileCount, maxFiles }
1107
+ });
1108
+ }
1109
+ return {
1110
+ valid: allErrors.length === 0,
1111
+ files: allInfos,
1112
+ errors: allErrors
1113
+ };
1114
+ } catch {
1115
+ return {
1116
+ valid: false,
1117
+ files: /* @__PURE__ */ new Map(),
1118
+ errors: [{
1119
+ filename: "",
1120
+ code: "invalid_content",
1121
+ message: "Failed to parse multipart form data"
1122
+ }]
1123
+ };
1124
+ }
1125
+ }
1126
+ function defaultFileErrorResponse(errors) {
1127
+ return new Response(
1128
+ JSON.stringify({
1129
+ error: "file_validation_error",
1130
+ message: "File validation failed",
1131
+ details: errors.map((e) => ({
1132
+ filename: e.filename,
1133
+ field: e.field,
1134
+ code: e.code,
1135
+ message: e.message
1136
+ }))
1137
+ }),
1138
+ {
1139
+ status: 400,
1140
+ headers: { "Content-Type": "application/json" }
1141
+ }
1142
+ );
1143
+ }
1144
+ function formatBytes(bytes) {
1145
+ if (bytes === 0) return "0 B";
1146
+ const units = ["B", "KB", "MB", "GB"];
1147
+ const k = 1024;
1148
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1149
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${units[i]}`;
1150
+ }
1151
+
1152
+ // src/middleware/validation/sanitizers/xss.ts
1153
+ var DEFAULT_ALLOWED_TAGS = [
1154
+ "a",
1155
+ "abbr",
1156
+ "b",
1157
+ "blockquote",
1158
+ "br",
1159
+ "code",
1160
+ "del",
1161
+ "em",
1162
+ "h1",
1163
+ "h2",
1164
+ "h3",
1165
+ "h4",
1166
+ "h5",
1167
+ "h6",
1168
+ "hr",
1169
+ "i",
1170
+ "ins",
1171
+ "li",
1172
+ "mark",
1173
+ "ol",
1174
+ "p",
1175
+ "pre",
1176
+ "q",
1177
+ "s",
1178
+ "small",
1179
+ "span",
1180
+ "strong",
1181
+ "sub",
1182
+ "sup",
1183
+ "u",
1184
+ "ul"
1185
+ ];
1186
+ var DEFAULT_ALLOWED_ATTRIBUTES = {
1187
+ a: ["href", "title", "target", "rel"],
1188
+ img: ["src", "alt", "title", "width", "height"],
1189
+ abbr: ["title"],
1190
+ q: ["cite"],
1191
+ blockquote: ["cite"]
1192
+ };
1193
+ var DEFAULT_SAFE_PROTOCOLS = ["http:", "https:", "mailto:", "tel:"];
1194
+ var DANGEROUS_PATTERNS2 = [
1195
+ // Event handlers
1196
+ /\bon\w+\s*=/gi,
1197
+ // JavaScript protocol
1198
+ /javascript\s*:/gi,
1199
+ // VBScript protocol
1200
+ /vbscript\s*:/gi,
1201
+ // Data URI with scripts
1202
+ /data\s*:[^,]*(?:text\/html|application\/javascript|text\/javascript)/gi,
1203
+ // Expression in CSS
1204
+ /expression\s*\(/gi,
1205
+ // Binding in CSS (Firefox)
1206
+ /-moz-binding\s*:/gi,
1207
+ // Behavior in CSS (IE)
1208
+ /behavior\s*:/gi,
1209
+ // Import in CSS
1210
+ /@import/gi,
1211
+ // Script tags
1212
+ /<\s*script/gi,
1213
+ // Style tags with expressions
1214
+ /<\s*style[^>]*>[^<]*expression/gi,
1215
+ // SVG with scripts
1216
+ /<\s*svg[^>]*onload/gi,
1217
+ // Object/embed/applet tags
1218
+ /<\s*(object|embed|applet)/gi,
1219
+ // Base tag (can redirect resources)
1220
+ /<\s*base/gi,
1221
+ // Meta refresh
1222
+ /<\s*meta[^>]*http-equiv\s*=\s*["']?refresh/gi,
1223
+ // Form action hijacking
1224
+ /<\s*form[^>]*action\s*=\s*["']?javascript/gi,
1225
+ // Link tag with import
1226
+ /<\s*link[^>]*rel\s*=\s*["']?import/gi
1227
+ ];
1228
+ var HTML_ENTITIES = {
1229
+ "&": "&amp;",
1230
+ "<": "&lt;",
1231
+ ">": "&gt;",
1232
+ '"': "&quot;",
1233
+ "'": "&#x27;",
1234
+ "/": "&#x2F;",
1235
+ "`": "&#x60;",
1236
+ "=": "&#x3D;"
1237
+ };
1238
+ function escapeHtml(str) {
1239
+ return str.replace(/[&<>"'`=/]/g, (char) => HTML_ENTITIES[char] || char);
1240
+ }
1241
+ function unescapeHtml(str) {
1242
+ const entityMap = {
1243
+ "&amp;": "&",
1244
+ "&lt;": "<",
1245
+ "&gt;": ">",
1246
+ "&quot;": '"',
1247
+ "&#x27;": "'",
1248
+ "&#x2F;": "/",
1249
+ "&#x60;": "`",
1250
+ "&#x3D;": "=",
1251
+ "&#39;": "'",
1252
+ "&#47;": "/"
1253
+ };
1254
+ return str.replace(/&(?:amp|lt|gt|quot|#x27|#x2F|#x60|#x3D|#39|#47);/gi, (entity) => {
1255
+ return entityMap[entity.toLowerCase()] || entity;
1256
+ });
1257
+ }
1258
+ function stripHtml(str) {
1259
+ let result = str.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
1260
+ result = result.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
1261
+ result = result.replace(/<[^>]*>/g, "");
1262
+ result = unescapeHtml(result);
1263
+ result = result.replace(/\0/g, "");
1264
+ return result.trim();
1265
+ }
1266
+ function isSafeUrl(url, allowedProtocols = DEFAULT_SAFE_PROTOCOLS) {
1267
+ if (!url) return true;
1268
+ const trimmed = url.trim().toLowerCase();
1269
+ if (trimmed.startsWith("javascript:")) return false;
1270
+ if (trimmed.startsWith("vbscript:")) return false;
1271
+ if (trimmed.startsWith("data:image/")) return true;
1272
+ if (trimmed.startsWith("data:")) return false;
1273
+ try {
1274
+ const parsed = new URL(url, "https://example.com");
1275
+ if (parsed.protocol && !allowedProtocols.includes(parsed.protocol)) {
1276
+ if (!url.includes(":")) return true;
1277
+ return false;
1278
+ }
1279
+ } catch {
1280
+ return true;
1281
+ }
1282
+ return true;
1283
+ }
1284
+ function sanitizeHtml(str, allowedTags = DEFAULT_ALLOWED_TAGS, allowedAttributes = DEFAULT_ALLOWED_ATTRIBUTES, allowedProtocols = DEFAULT_SAFE_PROTOCOLS) {
1285
+ let result = str.replace(/\0/g, "");
1286
+ result = result.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
1287
+ result = result.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
1288
+ result = result.replace(/<!--[\s\S]*?-->/g, "");
1289
+ result = result.replace(/<\/?([a-z][a-z0-9]*)\b([^>]*)>/gi, (match, tagName, attributes) => {
1290
+ const lowerTag = tagName.toLowerCase();
1291
+ const isClosing = match.startsWith("</");
1292
+ if (!allowedTags.includes(lowerTag)) {
1293
+ return "";
1294
+ }
1295
+ if (isClosing) {
1296
+ return `</${lowerTag}>`;
1297
+ }
1298
+ const allowedAttrs = allowedAttributes[lowerTag] || [];
1299
+ const safeAttrs = [];
1300
+ const attrRegex = /([a-z][a-z0-9-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]*))/gi;
1301
+ let attrMatch;
1302
+ while ((attrMatch = attrRegex.exec(attributes)) !== null) {
1303
+ const attrName = attrMatch[1].toLowerCase();
1304
+ const attrValue = attrMatch[2] || attrMatch[3] || attrMatch[4] || "";
1305
+ if (!allowedAttrs.includes(attrName)) continue;
1306
+ if (DANGEROUS_PATTERNS2.some((pattern) => pattern.test(attrValue))) continue;
1307
+ if (["href", "src", "action", "formaction"].includes(attrName)) {
1308
+ if (!isSafeUrl(attrValue, allowedProtocols)) continue;
1309
+ }
1310
+ const safeValue = escapeHtml(attrValue);
1311
+ safeAttrs.push(`${attrName}="${safeValue}"`);
1312
+ }
1313
+ const attrStr = safeAttrs.length > 0 ? " " + safeAttrs.join(" ") : "";
1314
+ return `<${lowerTag}${attrStr}>`;
1315
+ });
1316
+ for (const pattern of DANGEROUS_PATTERNS2) {
1317
+ result = result.replace(pattern, "");
1318
+ }
1319
+ return result;
1320
+ }
1321
+ function detectXSS(str) {
1322
+ if (!str || typeof str !== "string") return false;
1323
+ const normalized = str.replace(/\\x([0-9a-f]{2})/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/\\u([0-9a-f]{4})/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/&#(\d+);?/gi, (_, dec) => String.fromCharCode(parseInt(dec, 10)));
1324
+ for (const pattern of DANGEROUS_PATTERNS2) {
1325
+ pattern.lastIndex = 0;
1326
+ if (pattern.test(normalized)) {
1327
+ return true;
1328
+ }
1329
+ }
1330
+ return false;
1331
+ }
1332
+ function sanitize(input, config = {}) {
1333
+ if (!input || typeof input !== "string") return "";
1334
+ const {
1335
+ mode = "escape",
1336
+ allowedTags = DEFAULT_ALLOWED_TAGS,
1337
+ allowedAttributes = DEFAULT_ALLOWED_ATTRIBUTES,
1338
+ allowedProtocols = DEFAULT_SAFE_PROTOCOLS,
1339
+ maxLength,
1340
+ stripNull = true
1341
+ } = config;
1342
+ let result = input;
1343
+ if (stripNull) {
1344
+ result = result.replace(/\0/g, "");
1345
+ }
1346
+ switch (mode) {
1347
+ case "escape":
1348
+ result = escapeHtml(result);
1349
+ break;
1350
+ case "strip":
1351
+ result = stripHtml(result);
1352
+ break;
1353
+ case "allow-safe":
1354
+ result = sanitizeHtml(result, allowedTags, allowedAttributes, allowedProtocols);
1355
+ break;
1356
+ }
1357
+ if (maxLength !== void 0 && result.length > maxLength) {
1358
+ result = result.slice(0, maxLength);
1359
+ }
1360
+ return result;
1361
+ }
1362
+ function sanitizeObject(obj, config = {}) {
1363
+ if (typeof obj === "string") {
1364
+ return sanitize(obj, config);
1365
+ }
1366
+ if (Array.isArray(obj)) {
1367
+ return obj.map((item) => sanitizeObject(item, config));
1368
+ }
1369
+ if (typeof obj === "object" && obj !== null) {
1370
+ const result = {};
1371
+ for (const [key, value] of Object.entries(obj)) {
1372
+ result[key] = sanitizeObject(value, config);
1373
+ }
1374
+ return result;
1375
+ }
1376
+ return obj;
1377
+ }
1378
+ function sanitizeFields(obj, fields, config = {}) {
1379
+ const result = { ...obj };
1380
+ for (const field of fields) {
1381
+ if (field in result && typeof result[field] === "string") {
1382
+ result[field] = sanitize(result[field], config);
1383
+ }
1384
+ }
1385
+ return result;
1386
+ }
1387
+
1388
+ // src/middleware/validation/sanitizers/sql.ts
1389
+ var SQL_PATTERNS = [
1390
+ // High severity - Definite attacks
1391
+ {
1392
+ pattern: /'\s*OR\s+'?\d+'?\s*=\s*'?\d+'?/gi,
1393
+ name: "OR '1'='1' attack",
1394
+ severity: "high"
1395
+ },
1396
+ {
1397
+ pattern: /'\s*OR\s+'[^']*'\s*=\s*'[^']*'/gi,
1398
+ name: "OR 'x'='x' attack",
1399
+ severity: "high"
1400
+ },
1401
+ {
1402
+ pattern: /;\s*DROP\s+(TABLE|DATABASE|INDEX|VIEW)/gi,
1403
+ name: "DROP statement",
1404
+ severity: "high"
1405
+ },
1406
+ {
1407
+ pattern: /;\s*DELETE\s+FROM/gi,
1408
+ name: "DELETE statement",
1409
+ severity: "high"
1410
+ },
1411
+ {
1412
+ pattern: /;\s*TRUNCATE\s+/gi,
1413
+ name: "TRUNCATE statement",
1414
+ severity: "high"
1415
+ },
1416
+ {
1417
+ pattern: /;\s*INSERT\s+INTO/gi,
1418
+ name: "INSERT statement",
1419
+ severity: "high"
1420
+ },
1421
+ {
1422
+ pattern: /;\s*UPDATE\s+\w+\s+SET/gi,
1423
+ name: "UPDATE statement",
1424
+ severity: "high"
1425
+ },
1426
+ {
1427
+ pattern: /UNION\s+(ALL\s+)?SELECT/gi,
1428
+ name: "UNION SELECT attack",
1429
+ severity: "high"
1430
+ },
1431
+ {
1432
+ pattern: /EXEC(\s+|\()+(sp_|xp_)/gi,
1433
+ name: "SQL Server stored procedure",
1434
+ severity: "high"
1435
+ },
1436
+ {
1437
+ pattern: /EXECUTE\s+IMMEDIATE/gi,
1438
+ name: "Oracle EXECUTE IMMEDIATE",
1439
+ severity: "high"
1440
+ },
1441
+ {
1442
+ pattern: /INTO\s+(OUT|DUMP)FILE/gi,
1443
+ name: "MySQL file write",
1444
+ severity: "high"
1445
+ },
1446
+ {
1447
+ pattern: /LOAD_FILE\s*\(/gi,
1448
+ name: "MySQL file read",
1449
+ severity: "high"
1450
+ },
1451
+ {
1452
+ pattern: /BENCHMARK\s*\(\s*\d+\s*,/gi,
1453
+ name: "MySQL BENCHMARK DoS",
1454
+ severity: "high"
1455
+ },
1456
+ {
1457
+ pattern: /SLEEP\s*\(\s*\d+\s*\)/gi,
1458
+ name: "SQL SLEEP time-based attack",
1459
+ severity: "high"
1460
+ },
1461
+ {
1462
+ pattern: /WAITFOR\s+DELAY/gi,
1463
+ name: "SQL Server WAITFOR DELAY",
1464
+ severity: "high"
1465
+ },
1466
+ {
1467
+ pattern: /PG_SLEEP\s*\(/gi,
1468
+ name: "PostgreSQL pg_sleep",
1469
+ severity: "high"
1470
+ },
1471
+ // Medium severity - Likely attacks
1472
+ {
1473
+ pattern: /'\s*--/g,
1474
+ name: "SQL comment injection",
1475
+ severity: "medium"
1476
+ },
1477
+ {
1478
+ pattern: /'\s*#/g,
1479
+ name: "MySQL comment injection",
1480
+ severity: "medium"
1481
+ },
1482
+ {
1483
+ pattern: /\/\*[\s\S]*?\*\//g,
1484
+ name: "Block comment",
1485
+ severity: "medium"
1486
+ },
1487
+ {
1488
+ pattern: /'\s*;\s*$/g,
1489
+ name: "Statement terminator",
1490
+ severity: "medium"
1491
+ },
1492
+ {
1493
+ pattern: /HAVING\s+\d+\s*=\s*\d+/gi,
1494
+ name: "HAVING clause injection",
1495
+ severity: "medium"
1496
+ },
1497
+ {
1498
+ pattern: /GROUP\s+BY\s+\d+/gi,
1499
+ name: "GROUP BY injection",
1500
+ severity: "medium"
1501
+ },
1502
+ {
1503
+ pattern: /ORDER\s+BY\s+\d+/gi,
1504
+ name: "ORDER BY injection",
1505
+ severity: "medium"
1506
+ },
1507
+ {
1508
+ pattern: /CONCAT\s*\(/gi,
1509
+ name: "CONCAT function",
1510
+ severity: "medium"
1511
+ },
1512
+ {
1513
+ pattern: /CHAR\s*\(\s*\d+\s*\)/gi,
1514
+ name: "CHAR function bypass",
1515
+ severity: "medium"
1516
+ },
1517
+ {
1518
+ pattern: /0x[0-9a-f]{2,}/gi,
1519
+ name: "Hex encoded value",
1520
+ severity: "medium"
1521
+ },
1522
+ {
1523
+ pattern: /CONVERT\s*\(/gi,
1524
+ name: "CONVERT function",
1525
+ severity: "medium"
1526
+ },
1527
+ {
1528
+ pattern: /CAST\s*\(/gi,
1529
+ name: "CAST function",
1530
+ severity: "medium"
1531
+ },
1532
+ // Low severity - Suspicious but may be false positives
1533
+ {
1534
+ pattern: /'\s*AND\s+'?\d+'?\s*=\s*'?\d+'?/gi,
1535
+ name: "AND '1'='1' pattern",
1536
+ severity: "low"
1537
+ },
1538
+ {
1539
+ pattern: /'\s*AND\s+'[^']*'\s*=\s*'[^']*'/gi,
1540
+ name: "AND 'x'='x' pattern",
1541
+ severity: "low"
1542
+ },
1543
+ {
1544
+ pattern: /SELECT\s+[\w\s,*]+\s+FROM/gi,
1545
+ name: "SELECT statement",
1546
+ severity: "low"
1547
+ },
1548
+ {
1549
+ pattern: /'\s*\+\s*'/g,
1550
+ name: "String concatenation",
1551
+ severity: "low"
1552
+ },
1553
+ {
1554
+ pattern: /'\s*\|\|\s*'/g,
1555
+ name: "Oracle string concatenation",
1556
+ severity: "low"
1557
+ }
1558
+ ];
1559
+ var ENCODED_PATTERNS = [
1560
+ {
1561
+ pattern: /%27\s*%4f%52\s*%27/gi,
1562
+ // URL encoded ' OR '
1563
+ name: "URL encoded OR injection",
1564
+ severity: "high"
1565
+ },
1566
+ {
1567
+ pattern: /%27\s*%2d%2d/gi,
1568
+ // URL encoded ' --
1569
+ name: "URL encoded comment injection",
1570
+ severity: "medium"
1571
+ },
1572
+ {
1573
+ pattern: /\0|%00/g,
1574
+ // Null byte (decoded or encoded)
1575
+ name: "Null byte injection",
1576
+ severity: "high"
1577
+ },
1578
+ {
1579
+ pattern: /\\x27/gi,
1580
+ // Hex escape
1581
+ name: "Hex escaped quote",
1582
+ severity: "medium"
1583
+ },
1584
+ {
1585
+ pattern: /\\u0027/gi,
1586
+ // Unicode escape
1587
+ name: "Unicode escaped quote",
1588
+ severity: "medium"
1589
+ }
1590
+ ];
1591
+ function normalizeInput(input) {
1592
+ let result = input;
1593
+ try {
1594
+ result = decodeURIComponent(result);
1595
+ } catch {
1596
+ }
1597
+ result = result.replace(/&#x([0-9a-f]+);?/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/&#(\d+);?/gi, (_, dec) => String.fromCharCode(parseInt(dec, 10))).replace(/&quot;/gi, '"').replace(/&apos;/gi, "'").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&amp;/gi, "&");
1598
+ result = result.replace(
1599
+ /\\x([0-9a-f]{2})/gi,
1600
+ (_, hex) => String.fromCharCode(parseInt(hex, 16))
1601
+ );
1602
+ result = result.replace(
1603
+ /\\u([0-9a-f]{4})/gi,
1604
+ (_, hex) => String.fromCharCode(parseInt(hex, 16))
1605
+ );
1606
+ return result;
1607
+ }
1608
+ function detectSQLInjection(input, options = {}) {
1609
+ if (!input || typeof input !== "string") return [];
1610
+ const {
1611
+ customPatterns = [],
1612
+ checkEncoded = true,
1613
+ minSeverity = "low"
1614
+ } = options;
1615
+ const severityOrder = { low: 0, medium: 1, high: 2 };
1616
+ const minSeverityLevel = severityOrder[minSeverity];
1617
+ const detections = [];
1618
+ const seenPatterns = /* @__PURE__ */ new Set();
1619
+ const normalizedInput = checkEncoded ? normalizeInput(input) : input;
1620
+ const allPatterns = [
1621
+ ...SQL_PATTERNS,
1622
+ ...checkEncoded ? ENCODED_PATTERNS : [],
1623
+ ...customPatterns.map((p) => ({ pattern: p, name: "Custom pattern", severity: "high" }))
1624
+ ];
1625
+ for (const { pattern, name, severity } of allPatterns) {
1626
+ if (severityOrder[severity] < minSeverityLevel) continue;
1627
+ pattern.lastIndex = 0;
1628
+ const testInput = checkEncoded ? normalizedInput : input;
1629
+ if (pattern.test(testInput)) {
1630
+ const key = `${name}:${severity}`;
1631
+ if (!seenPatterns.has(key)) {
1632
+ seenPatterns.add(key);
1633
+ detections.push({
1634
+ field: "",
1635
+ // Will be set by caller
1636
+ value: input,
1637
+ pattern: name,
1638
+ severity
1639
+ });
1640
+ }
1641
+ }
1642
+ }
1643
+ return detections;
1644
+ }
1645
+ function hasSQLInjection(input, minSeverity = "medium") {
1646
+ return detectSQLInjection(input, { minSeverity }).length > 0;
1647
+ }
1648
+ function sanitizeSQLInput(input) {
1649
+ if (!input || typeof input !== "string") return "";
1650
+ let result = input;
1651
+ result = result.replace(/\0/g, "");
1652
+ result = result.replace(/'/g, "''");
1653
+ result = result.replace(/;/g, "");
1654
+ result = result.replace(/--/g, "");
1655
+ result = result.replace(/\/\*/g, "");
1656
+ result = result.replace(/\*\//g, "");
1657
+ result = result.replace(/0x[0-9a-f]+/gi, "");
1658
+ return result;
1659
+ }
1660
+ function detectSQLInjectionInObject(obj, options = {}) {
1661
+ const { fields, deep = true, customPatterns, minSeverity } = options;
1662
+ const detections = [];
1663
+ function walk(value, path) {
1664
+ if (typeof value === "string") {
1665
+ if (fields && fields.length > 0) {
1666
+ const fieldName = path.split(".").pop() || path;
1667
+ if (!fields.includes(fieldName)) return;
1668
+ }
1669
+ const detected = detectSQLInjection(value, { customPatterns, minSeverity });
1670
+ for (const d of detected) {
1671
+ detections.push({ ...d, field: path });
1672
+ }
1673
+ } else if (deep && Array.isArray(value)) {
1674
+ value.forEach((item, i) => walk(item, `${path}[${i}]`));
1675
+ } else if (deep && typeof value === "object" && value !== null) {
1676
+ for (const [key, val] of Object.entries(value)) {
1677
+ walk(val, path ? `${path}.${key}` : key);
1678
+ }
1679
+ }
1680
+ }
1681
+ walk(obj, "");
1682
+ return detections;
1683
+ }
1684
+ function isAllowedValue(value, allowList) {
1685
+ if (!allowList || allowList.length === 0) return false;
1686
+ return allowList.includes(value);
1687
+ }
1688
+
1689
+ // src/middleware/validation/middleware.ts
1690
+ function withValidation(handler, config) {
1691
+ const onError = config.onError || ((_, errors) => defaultValidationErrorResponse(errors));
1692
+ return async (req) => {
1693
+ const result = await validateRequest(req, {
1694
+ body: config.body,
1695
+ query: config.query,
1696
+ params: config.params,
1697
+ routeParams: config.routeParams
1698
+ });
1699
+ if (!result.success) {
1700
+ return onError(req, result.errors || []);
1701
+ }
1702
+ return handler(req, { validated: result.data });
1703
+ };
1704
+ }
1705
+ function withSanitization(handler, config = {}) {
1706
+ const {
1707
+ fields,
1708
+ mode = "escape",
1709
+ allowedTags,
1710
+ skip,
1711
+ onSanitized
1712
+ } = config;
1713
+ return async (req) => {
1714
+ if (skip && await skip(req)) {
1715
+ return handler(req, { sanitized: null, changes: [] });
1716
+ }
1717
+ let body;
1718
+ try {
1719
+ body = await req.json();
1720
+ } catch {
1721
+ return handler(req, { sanitized: null, changes: [] });
1722
+ }
1723
+ const changes = [];
1724
+ const sanitized = walkObject(body, (value, path) => {
1725
+ if (fields && fields.length > 0) {
1726
+ const fieldName = path.split(".").pop() || path;
1727
+ if (!fields.includes(fieldName)) {
1728
+ return value;
1729
+ }
1730
+ }
1731
+ const cleaned = sanitize(value, { mode, allowedTags });
1732
+ if (cleaned !== value) {
1733
+ changes.push({
1734
+ field: path,
1735
+ original: value,
1736
+ sanitized: cleaned
1737
+ });
1738
+ }
1739
+ return cleaned;
1740
+ }, "");
1741
+ if (onSanitized && changes.length > 0) {
1742
+ onSanitized(req, changes);
1743
+ }
1744
+ return handler(req, { sanitized, changes });
1745
+ };
1746
+ }
1747
+ function withXSSProtection(handler, config = {}) {
1748
+ const { fields, onDetection, checkQuery = true } = config;
1749
+ return async (req) => {
1750
+ const detections = [];
1751
+ if (checkQuery) {
1752
+ const url = new URL(req.url);
1753
+ for (const [key, value] of url.searchParams.entries()) {
1754
+ if (detectXSS(value)) {
1755
+ detections.push({ field: `query.${key}`, value });
1756
+ }
1757
+ }
1758
+ }
1759
+ let body;
1760
+ try {
1761
+ body = await req.json();
1762
+ } catch {
1763
+ body = null;
1764
+ }
1765
+ if (body) {
1766
+ walkObject(body, (value, path) => {
1767
+ if (fields && fields.length > 0) {
1768
+ const fieldName = path.split(".").pop() || path;
1769
+ if (!fields.includes(fieldName)) {
1770
+ return value;
1771
+ }
1772
+ }
1773
+ if (detectXSS(value)) {
1774
+ detections.push({ field: path, value });
1775
+ }
1776
+ return value;
1777
+ }, "");
1778
+ }
1779
+ if (detections.length > 0) {
1780
+ if (onDetection) {
1781
+ for (const { field, value } of detections) {
1782
+ const result = await onDetection(req, field, value);
1783
+ if (result instanceof Response) {
1784
+ return result;
1785
+ }
1786
+ }
1787
+ }
1788
+ return new Response(
1789
+ JSON.stringify({
1790
+ error: "xss_detected",
1791
+ message: "Potentially malicious content detected",
1792
+ fields: detections.map((d) => d.field)
1793
+ }),
1794
+ {
1795
+ status: 400,
1796
+ headers: { "Content-Type": "application/json" }
1797
+ }
1798
+ );
1799
+ }
1800
+ return handler(req);
1801
+ };
1802
+ }
1803
+ function withSQLProtection(handler, config = {}) {
1804
+ const {
1805
+ fields,
1806
+ deep = true,
1807
+ mode = "block",
1808
+ customPatterns,
1809
+ allowList = [],
1810
+ onDetection
1811
+ } = config;
1812
+ return async (req) => {
1813
+ let body;
1814
+ try {
1815
+ body = await req.json();
1816
+ } catch {
1817
+ return handler(req);
1818
+ }
1819
+ const detections = detectSQLInjectionInObject(body, {
1820
+ fields,
1821
+ deep,
1822
+ customPatterns,
1823
+ minSeverity: mode === "detect" ? "low" : "medium"
1824
+ });
1825
+ const filtered = detections.filter((d) => !allowList.includes(d.value));
1826
+ if (filtered.length > 0) {
1827
+ if (onDetection) {
1828
+ const result = await onDetection(req, filtered);
1829
+ if (result instanceof Response) {
1830
+ return result;
1831
+ }
1832
+ }
1833
+ if (mode === "block") {
1834
+ return new Response(
1835
+ JSON.stringify({
1836
+ error: "sql_injection_detected",
1837
+ message: "Potentially malicious SQL detected",
1838
+ detections: filtered.map((d) => ({
1839
+ field: d.field,
1840
+ pattern: d.pattern,
1841
+ severity: d.severity
1842
+ }))
1843
+ }),
1844
+ {
1845
+ status: 400,
1846
+ headers: { "Content-Type": "application/json" }
1847
+ }
1848
+ );
1849
+ }
1850
+ }
1851
+ return handler(req);
1852
+ };
1853
+ }
1854
+ function withContentType(handler, config) {
1855
+ const onInvalid = config.onInvalid || ((_, contentType) => defaultContentTypeErrorResponse(contentType, `Content-Type '${contentType}' is not allowed`));
1856
+ return async (req) => {
1857
+ const result = validateContentType(req, config);
1858
+ if (!result.valid) {
1859
+ return onInvalid(req, result.contentType);
1860
+ }
1861
+ return handler(req);
1862
+ };
1863
+ }
1864
+ function withFileValidation(handler, config = {}) {
1865
+ const onInvalid = config.onInvalid || ((_, errors) => defaultFileErrorResponse(errors));
1866
+ return async (req) => {
1867
+ const result = await validateFilesFromRequest(req, config);
1868
+ if (!result.valid) {
1869
+ return onInvalid(req, result.errors);
1870
+ }
1871
+ return handler(req, { files: result.files });
1872
+ };
1873
+ }
1874
+ function withSecureValidation(handler, config) {
1875
+ return async (req) => {
1876
+ const allErrors = [];
1877
+ if (config.contentType) {
1878
+ const ctResult = validateContentType(req, config.contentType);
1879
+ if (!ctResult.valid) {
1880
+ allErrors.push({
1881
+ field: "Content-Type",
1882
+ code: "invalid_content_type",
1883
+ message: ctResult.reason || "Invalid Content-Type"
1884
+ });
1885
+ }
1886
+ }
1887
+ let files;
1888
+ if (config.files) {
1889
+ const fileResult = await validateFilesFromRequest(req, config.files);
1890
+ if (!fileResult.valid) {
1891
+ allErrors.push(...fileResult.errors.map((e) => ({
1892
+ field: e.field || e.filename,
1893
+ code: e.code,
1894
+ message: e.message
1895
+ })));
1896
+ } else {
1897
+ files = fileResult.files;
1898
+ }
1899
+ }
1900
+ if (allErrors.length > 0) {
1901
+ const onError = config.onError || ((_, errors) => defaultValidationErrorResponse(errors));
1902
+ return onError(req, allErrors);
1903
+ }
1904
+ let validated;
1905
+ if (config.schema) {
1906
+ const schemaResult = await validateRequest(req, {
1907
+ body: config.schema.body,
1908
+ query: config.schema.query,
1909
+ params: config.schema.params,
1910
+ routeParams: config.routeParams
1911
+ });
1912
+ if (!schemaResult.success) {
1913
+ allErrors.push(...schemaResult.errors || []);
1914
+ } else {
1915
+ validated = schemaResult.data;
1916
+ }
1917
+ } else {
1918
+ validated = {
1919
+ body: {},
1920
+ query: {},
1921
+ params: {}
1922
+ };
1923
+ }
1924
+ if (config.sql && validated?.body) {
1925
+ const sqlDetections = detectSQLInjectionInObject(validated.body, {
1926
+ fields: config.sql.fields,
1927
+ deep: config.sql.deep,
1928
+ customPatterns: config.sql.customPatterns
1929
+ });
1930
+ if (sqlDetections.length > 0 && config.sql.mode !== "detect") {
1931
+ allErrors.push(...sqlDetections.map((d) => ({
1932
+ field: d.field,
1933
+ code: "sql_injection",
1934
+ message: `Potential SQL injection detected: ${d.pattern}`
1935
+ })));
1936
+ }
1937
+ }
1938
+ if (config.xss?.enabled && validated?.body) {
1939
+ walkObject(validated.body, (value, path) => {
1940
+ if (config.xss?.fields && config.xss.fields.length > 0) {
1941
+ const fieldName = path.split(".").pop() || path;
1942
+ if (!config.xss.fields.includes(fieldName)) {
1943
+ return value;
1944
+ }
1945
+ }
1946
+ if (detectXSS(value)) {
1947
+ allErrors.push({
1948
+ field: path,
1949
+ code: "xss_detected",
1950
+ message: "Potentially malicious content detected"
1951
+ });
1952
+ }
1953
+ return value;
1954
+ }, "");
1955
+ }
1956
+ if (allErrors.length > 0) {
1957
+ const onError = config.onError || ((_, errors) => defaultValidationErrorResponse(errors));
1958
+ return onError(req, allErrors);
1959
+ }
1960
+ return handler(req, { validated, files });
1961
+ };
1962
+ }
1963
+
1964
+ exports.DANGEROUS_EXTENSIONS = DANGEROUS_EXTENSIONS;
1965
+ exports.DEFAULT_MAX_FILES = DEFAULT_MAX_FILES;
1966
+ exports.DEFAULT_MAX_FILE_SIZE = DEFAULT_MAX_FILE_SIZE;
1967
+ exports.MIME_TYPES = MIME_TYPES;
1968
+ exports.allValid = allValid;
1969
+ exports.checkMagicNumber = checkMagicNumber;
1970
+ exports.createValidator = createValidator;
1971
+ exports.defaultContentTypeErrorResponse = defaultContentTypeErrorResponse;
1972
+ exports.defaultFileErrorResponse = defaultFileErrorResponse;
1973
+ exports.defaultValidationErrorResponse = defaultValidationErrorResponse;
1974
+ exports.detectFileType = detectFileType;
1975
+ exports.detectSQLInjection = detectSQLInjection;
1976
+ exports.detectSQLInjectionInObject = detectSQLInjectionInObject;
1977
+ exports.detectXSS = detectXSS;
1978
+ exports.escapeHtml = escapeHtml;
1979
+ exports.extractFilesFromFormData = extractFilesFromFormData;
1980
+ exports.getByPath = getByPath;
1981
+ exports.getExtension = getExtension;
1982
+ exports.getFilename = getFilename;
1983
+ exports.getMultipartBoundary = getMultipartBoundary;
1984
+ exports.hasPathTraversal = hasPathTraversal;
1985
+ exports.hasSQLInjection = hasSQLInjection;
1986
+ exports.isAllowedContentType = isAllowedContentType;
1987
+ exports.isAllowedValue = isAllowedValue;
1988
+ exports.isCustomSchema = isCustomSchema;
1989
+ exports.isFormRequest = isFormRequest;
1990
+ exports.isHiddenPath = isHiddenPath;
1991
+ exports.isJsonRequest = isJsonRequest;
1992
+ exports.isMultipartRequest = isMultipartRequest;
1993
+ exports.isPathContained = isPathContained;
1994
+ exports.isSafeUrl = isSafeUrl;
1995
+ exports.isZodSchema = isZodSchema;
1996
+ exports.mergeErrors = mergeErrors;
1997
+ exports.parseContentType = parseContentType;
1998
+ exports.parseQueryString = parseQueryString;
1999
+ exports.sanitize = sanitize;
2000
+ exports.sanitizeFields = sanitizeFields;
2001
+ exports.sanitizeFilename = sanitizeFilename;
2002
+ exports.sanitizeHtml = sanitizeHtml;
2003
+ exports.sanitizeObject = sanitizeObject;
2004
+ exports.sanitizePath = sanitizePath;
2005
+ exports.sanitizeSQLInput = sanitizeSQLInput;
2006
+ exports.setByPath = setByPath;
2007
+ exports.stripHtml = stripHtml;
2008
+ exports.unescapeHtml = unescapeHtml;
2009
+ exports.validate = validate;
2010
+ exports.validateBody = validateBody;
2011
+ exports.validateContentType = validateContentType;
2012
+ exports.validateCustomSchema = validateCustomSchema;
2013
+ exports.validateField = validateField;
2014
+ exports.validateFile = validateFile;
2015
+ exports.validateFiles = validateFiles;
2016
+ exports.validateFilesFromRequest = validateFilesFromRequest;
2017
+ exports.validateParams = validateParams;
2018
+ exports.validatePath = validatePath;
2019
+ exports.validateQuery = validateQuery;
2020
+ exports.validateRequest = validateRequest;
2021
+ exports.validateZodSchema = validateZodSchema;
2022
+ exports.walkObject = walkObject;
2023
+ exports.withContentType = withContentType;
2024
+ exports.withFileValidation = withFileValidation;
2025
+ exports.withSQLProtection = withSQLProtection;
2026
+ exports.withSanitization = withSanitization;
2027
+ exports.withSecureValidation = withSecureValidation;
2028
+ exports.withValidation = withValidation;
2029
+ exports.withXSSProtection = withXSSProtection;
2030
+ //# sourceMappingURL=validation.cjs.map
2031
+ //# sourceMappingURL=validation.cjs.map