-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathinputValidator.ts
More file actions
211 lines (178 loc) · 6.34 KB
/
Copy pathinputValidator.ts
File metadata and controls
211 lines (178 loc) · 6.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
/**
* Input Validation Middleware for Cloudflare Workers
* Uses Zod for schema validation and sanitization
*/
import { z } from 'zod';
import { SecurityError, SecurityErrorType } from 'shared/types/errors';
import { createLogger } from '../logger';
import { validatePassword, validateEmail, validateUsername } from './validationUtils';
const logger = createLogger('InputValidator');
/**
* Input validation middleware using Zod schemas
*
* @param request - The incoming request
* @param schema - Zod schema for validation
* @returns Validated data or throws SecurityError
*/
export async function validateInput<T extends z.ZodSchema>(
request: Request,
schema: T
): Promise<z.infer<T>> {
try {
// Handle different content types
const contentType = request.headers.get('content-type');
let data: unknown;
if (contentType?.includes('application/json')) {
data = await parseJSON(request);
} else if (contentType?.includes('application/x-www-form-urlencoded')) {
data = await parseFormData(request);
} else if (contentType?.includes('multipart/form-data')) {
data = await parseMultipartFormData(request);
} else if (request.method === 'GET' || request.method === 'DELETE') {
data = parseQueryParams(request);
} else {
throw new SecurityError(
SecurityErrorType.INVALID_INPUT,
'Unsupported content type',
400
);
}
// Validate with Zod
const result = schema.safeParse(data);
if (!result.success) {
logger.warn('Validation failed', {
errors: result.error.issues,
path: new URL(request.url).pathname
});
throw new SecurityError(
SecurityErrorType.INVALID_INPUT,
formatValidationErrors(result.error),
400
);
}
logger.debug('Input validated successfully', {
path: new URL(request.url).pathname
});
return result.data;
} catch (error) {
if (error instanceof SecurityError) {
throw error;
}
logger.error('Input validation error', error);
throw new SecurityError(
SecurityErrorType.INVALID_INPUT,
'Invalid request data',
400
);
}
}
/**
* Parse JSON body with size limit
*/
async function parseJSON(request: Request): Promise<unknown> {
const text = await request.text();
// Check size limit (1MB)
if (text.length > 1024 * 1024) {
throw new SecurityError(
SecurityErrorType.INVALID_INPUT,
'Request body too large',
413
);
}
try {
return JSON.parse(text);
} catch {
throw new SecurityError(
SecurityErrorType.INVALID_INPUT,
'Invalid JSON',
400
);
}
}
/**
* Parse URL-encoded form data
*/
async function parseFormData(request: Request): Promise<Record<string, string>> {
const text = await request.text();
const params = new URLSearchParams(text);
const data: Record<string, string> = {};
for (const [key, value] of params) {
data[key] = value;
}
return data;
}
/**
* Parse multipart form data
*/
async function parseMultipartFormData(request: Request): Promise<Record<string, string | File>> {
const formData = await request.formData();
const data: Record<string, string | File> = {};
formData.forEach((value, key) => {
data[key] = value;
});
return data;
}
/**
* Parse query parameters
*/
function parseQueryParams(request: Request): Record<string, string> {
const url = new URL(request.url);
const data: Record<string, string> = {};
for (const [key, value] of url.searchParams) {
data[key] = value;
}
return data;
}
/**
* Format Zod validation errors for user-friendly response
*/
function formatValidationErrors(error: z.ZodError): string {
const messages = error.issues.map(err => {
const path = err.path.join('.');
return path ? `${path}: ${err.message}` : err.message;
});
return messages.join(', ');
}
/**
* Common validation schemas using centralized validation functions
*/
export const commonSchemas = {
// Email validation using centralized function
email: z.string().refine(
(email) => validateEmail(email).valid,
{ error: (issue) => validateEmail(issue.input as string).error || 'Invalid email format' }
).transform((email) => email.toLowerCase()),
// Password validation using centralized comprehensive validation
password: z.string().refine(
(password) => validatePassword(password).valid,
{ error: (issue) => validatePassword(issue.input as string).errors?.[0] || 'Password does not meet requirements' }
),
// Password validation with user context (for preventing personal info in passwords)
passwordWithUserInfo: (userInfo?: { email?: string; username?: string; name?: string }) =>
z.string().refine(
(password) => validatePassword(password, undefined, userInfo).valid,
{ error: (issue) => validatePassword(issue.input as string, undefined, userInfo).errors?.[0] || 'Password does not meet requirements' }
),
// Username validation using centralized function
username: z.string().refine(
(username) => validateUsername(username).valid,
{ error: (issue) => validateUsername(issue.input as string).error || 'Invalid username format' }
),
// UUID validation
uuid: z.string().uuid(),
// Pagination
pagination: z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
sortBy: z.string().optional(),
sortOrder: z.enum(['asc', 'desc']).default('desc')
}),
// Safe string (no special chars that could be used for injection)
safeString: z.string()
.regex(/^[a-zA-Z0-9\s\-_]+$/, 'Only alphanumeric characters, spaces, hyphens, and underscores allowed')
.transform(val => val.trim()),
// URL validation
url: z.string().url(),
// Date validation
date: z.string().datetime(),
};