snap-validate 0.3.0 โ 0.3.1
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/README.md +88 -2
- package/package.json +1 -1
- package/src/index.js +262 -42
package/README.md
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/snap-validate)
|
|
4
4
|
[](https://github.com/aniru-dh21/snap-validate/actions)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://packagephobia.now.sh/result?p=snap-validate)
|
|
7
|
+
[](https://bundlephobia.com/package/snap-validate@latest)
|
|
6
8
|
[](https://npm-stat.com/charts.html?package=snap-validate)
|
|
7
9
|
|
|
8
|
-
A lightning-fast, lightweight validation library for common patterns without heavy dependencies. Perfect for client-side and server-side validation with zero external dependencies.
|
|
10
|
+
A lightning-fast, lightweight validation library for common patterns without heavy dependencies. Perfect for client-side and server-side validation with zero external dependencies and built-in protection against ReDoS (Regular Expression Denial of Service) attacks.
|
|
9
11
|
|
|
10
12
|
## Features
|
|
11
13
|
|
|
@@ -17,6 +19,8 @@ A lightning-fast, lightweight validation library for common patterns without hea
|
|
|
17
19
|
- ๐ **Async Support**: Full async validation support for database checks and API calls
|
|
18
20
|
- ๐ฏ **Conditional**: Advanced conditional validation with `when()` and `optional()`
|
|
19
21
|
- ๐ ๏ธ **Custom Validators**: Add your own sync and async validation logic
|
|
22
|
+
- ๐ **Security First**: Built-in protection against ReDoS attacks and unsafe regex patterns
|
|
23
|
+
- ๐ก๏ธ **Timeout Protection**: Configurable timeout for regex operations to prevent DoS attacks
|
|
20
24
|
- ๐งช **Well Tested**: Comprehensive test suite with high coverage
|
|
21
25
|
- ๐ฆ **Easy Integration**: Works in Node.js and browsers
|
|
22
26
|
- ๐ **Chainable API**: Intuitive fluent interface
|
|
@@ -53,6 +57,30 @@ const result = validate(schema, data);
|
|
|
53
57
|
console.log(result.isValid); // true
|
|
54
58
|
```
|
|
55
59
|
|
|
60
|
+
## Security Features
|
|
61
|
+
|
|
62
|
+
### ReDoS Protection
|
|
63
|
+
|
|
64
|
+
Snap Validate includes built-in protection against Regular Expression Denial of Service (ReDoS) attacks:
|
|
65
|
+
|
|
66
|
+
- **Regex Safety Detection**: Automatically detects and prevents potentially dangerous regex patterns
|
|
67
|
+
- **Input Length Limits**: Protects against extremely long input strings (10,000 character limit)
|
|
68
|
+
- **Timeout Protection**: Configurable timeout for regex operations (default: 1 second)
|
|
69
|
+
- **Safe Defaults**: All predefined validators use safe, optimized regex patterns
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
// Set custom timeout for regex operations
|
|
73
|
+
const validator = new BaseValidator(value)
|
|
74
|
+
.setRegexTimeout(2000) // 2 second timeout
|
|
75
|
+
.pattern(/your-pattern/, 'Error message');
|
|
76
|
+
|
|
77
|
+
// Use async pattern validation for complex patterns with timeout protection
|
|
78
|
+
const validator = new BaseValidator(value)
|
|
79
|
+
.patternAsync(/complex-pattern/, 'Error message');
|
|
80
|
+
|
|
81
|
+
const result = await validator.validateAsync();
|
|
82
|
+
```
|
|
83
|
+
|
|
56
84
|
## Available Validators
|
|
57
85
|
|
|
58
86
|
### Email Validation
|
|
@@ -212,6 +240,39 @@ const asyncSchema = {
|
|
|
212
240
|
const asyncResult = await validate.async(asyncSchema, userData);
|
|
213
241
|
```
|
|
214
242
|
|
|
243
|
+
## Security and Pattern Validation
|
|
244
|
+
|
|
245
|
+
### Safe Pattern Validation
|
|
246
|
+
|
|
247
|
+
```javascript
|
|
248
|
+
const { BaseValidator } = require('snap-validate');
|
|
249
|
+
|
|
250
|
+
// Synchronous pattern validation with built-in safety checks
|
|
251
|
+
const validator = new BaseValidator(value)
|
|
252
|
+
.pattern(/^[a-zA-Z0-9]+$/, 'Only alphanumeric characters allowed');
|
|
253
|
+
|
|
254
|
+
// Asynchronous pattern validation with timeout protection
|
|
255
|
+
const asyncValidator = new BaseValidator(value)
|
|
256
|
+
.patternAsync(/^[a-zA-Z0-9]+$/, 'Only alphanumeric characters allowed')
|
|
257
|
+
.setRegexTimeout(5000); // 5 second timeout
|
|
258
|
+
|
|
259
|
+
const result = await asyncValidator.validateAsync();
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Configurable Security Settings
|
|
263
|
+
|
|
264
|
+
```javascript
|
|
265
|
+
const validator = new BaseValidator(value)
|
|
266
|
+
.setRegexTimeout(3000) // Set custom timeout (3 seconds)
|
|
267
|
+
.pattern(/your-pattern/, 'Error message');
|
|
268
|
+
|
|
269
|
+
// The library automatically:
|
|
270
|
+
// - Detects unsafe regex patterns
|
|
271
|
+
// - Limits input length to prevent ReDoS
|
|
272
|
+
// - Applies timeout protection for complex patterns
|
|
273
|
+
// - Provides clear error messages for security violations
|
|
274
|
+
```
|
|
275
|
+
|
|
215
276
|
## Custom Validation
|
|
216
277
|
|
|
217
278
|
### Using BaseValidator
|
|
@@ -282,6 +343,13 @@ try {
|
|
|
282
343
|
} catch (error) {
|
|
283
344
|
console.log('Validation exception:', error.message);
|
|
284
345
|
}
|
|
346
|
+
|
|
347
|
+
// Security-related errors
|
|
348
|
+
const unsafeResult = validator.pattern(/potentially-dangerous-pattern/, 'Error').validate();
|
|
349
|
+
if (!unsafeResult.isValid) {
|
|
350
|
+
console.log('Security errors:', unsafeResult.errors);
|
|
351
|
+
// Output: ['Potentially unsafe regex pattern detected']
|
|
352
|
+
}
|
|
285
353
|
```
|
|
286
354
|
|
|
287
355
|
## Browser Usage
|
|
@@ -308,7 +376,9 @@ try {
|
|
|
308
376
|
- `required(message?)` - Field is required
|
|
309
377
|
- `min(length, message?)` - Minimum length validation
|
|
310
378
|
- `max(length, message?)` - Maximum length validation
|
|
311
|
-
- `pattern(regex, message?)` - Pattern matching validation
|
|
379
|
+
- `pattern(regex, message?)` - Pattern matching validation with safety checks
|
|
380
|
+
- `patternAsync(regex, message?)` - Async pattern validation with timeout protection
|
|
381
|
+
- `setRegexTimeout(timeoutMs)` - Set custom timeout for regex operations
|
|
312
382
|
- `when(condition, validator)` - Conditional validation
|
|
313
383
|
- `optional()` - Skip validation if empty/null/undefined
|
|
314
384
|
- `custom(fn, message?)` - Custom synchronous validation
|
|
@@ -332,6 +402,19 @@ try {
|
|
|
332
402
|
- `validate(schema, data)` - Synchronous schema validation
|
|
333
403
|
- `validate.async(schema, data)` - Asynchronous schema validation
|
|
334
404
|
|
|
405
|
+
### Security Functions
|
|
406
|
+
|
|
407
|
+
- `isRegexSafe(regex)` - Check if a regex pattern is safe to use
|
|
408
|
+
- `safeRegexText(regex, str, timeoutMs)` - Execute regex with timeout protection
|
|
409
|
+
|
|
410
|
+
## Security Best Practices
|
|
411
|
+
|
|
412
|
+
1. **Use Built-in Validators**: The predefined validators are optimized for security and performance
|
|
413
|
+
2. **Validate Input Length**: Large inputs are automatically limited to prevent ReDoS attacks
|
|
414
|
+
3. **Set Appropriate Timeouts**: Configure regex timeouts based on your application's needs
|
|
415
|
+
4. **Test Custom Patterns**: Use `isRegexSafe()` to check custom regex patterns before deployment
|
|
416
|
+
5. **Handle Async Errors**: Always use try-catch blocks with async validation
|
|
417
|
+
|
|
335
418
|
## Contributing
|
|
336
419
|
|
|
337
420
|
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
@@ -362,6 +445,9 @@ npm run lint
|
|
|
362
445
|
|
|
363
446
|
# Format code
|
|
364
447
|
npm run format
|
|
448
|
+
|
|
449
|
+
# Security audit
|
|
450
|
+
npm audit
|
|
365
451
|
```
|
|
366
452
|
|
|
367
453
|
## License
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -1,8 +1,77 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Snap Validate - Enhanced Lightweight validator library
|
|
3
|
-
* @version 0.3.
|
|
3
|
+
* @version 0.3.1 - Security Fixes
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
// Utility function to safely test regex with timeout protection
|
|
7
|
+
// Utility function to safely test regex with timeout protection
|
|
8
|
+
const safeRegexTest = (regex, str, timeoutMs = 1000) => {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
// SECURITY FIX: Add input length validation before regex test
|
|
11
|
+
if (str.length > 10000) {
|
|
12
|
+
reject(new Error('Input too long for regex validation'));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// SECURITY FIX: Add regex safety check before execution
|
|
17
|
+
if (!isRegexSafe(regex)) {
|
|
18
|
+
reject(new Error('Unsafe regex pattern detected'));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const timeout = setTimeout(() => {
|
|
23
|
+
reject(new Error('Regex execution timeout - potential ReDoS attack'));
|
|
24
|
+
}, timeoutMs);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const result = regex.test(str);
|
|
28
|
+
clearTimeout(timeout);
|
|
29
|
+
resolve(result);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
clearTimeout(timeout);
|
|
32
|
+
reject(error);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Synchronous safe regex test with input length protection
|
|
38
|
+
const safeRegexTestSync = (regex, str, maxLength = 10000) => {
|
|
39
|
+
// Limit input length to prevent ReDoS
|
|
40
|
+
if (str.length > maxLength) {
|
|
41
|
+
throw new Error('Input too long for pattern validation');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// For additional safety, we could add a timeout using a worker thread or
|
|
45
|
+
// other mechanism, but for now we rely on input length limiting
|
|
46
|
+
return regex.test(str);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Function to detect potentially dangerous regex patterns
|
|
50
|
+
const isRegexSafe = (regex) => {
|
|
51
|
+
const regexStr = regex.toString();
|
|
52
|
+
|
|
53
|
+
// Check for common ReDoS patterns - more precise detection
|
|
54
|
+
const dangerousPatterns = [
|
|
55
|
+
// Nested quantifiers like (a+)+ or (a*)* or (a?)?
|
|
56
|
+
/\([^)]*[+*?][^)]*\)[+*?]/,
|
|
57
|
+
// Alternation with overlapping and quantifiers like (a|a)*
|
|
58
|
+
/\([^)]*\|[^)]*\)[+*]/,
|
|
59
|
+
// Catastrophic backtracking with greedy quantifiers
|
|
60
|
+
/\([^)]*\.\*[^)]*\)\*/,
|
|
61
|
+
// Multiple consecutive quantifiers (not separated by characters)
|
|
62
|
+
/[+*?]{2,}/,
|
|
63
|
+
// Exponential alternation patterns
|
|
64
|
+
/\([^)]*\|[^)]*\)\+.*\([^)]*\|[^)]*\)\+/
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
// Check if the pattern has obvious ReDoS vulnerabilities
|
|
68
|
+
const isDangerous = dangerousPatterns.some((pattern) =>
|
|
69
|
+
pattern.test(regexStr)
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return !isDangerous;
|
|
73
|
+
};
|
|
74
|
+
|
|
6
75
|
// Core validation class
|
|
7
76
|
class ValidationResult {
|
|
8
77
|
constructor(isValid, errors = []) {
|
|
@@ -24,16 +93,24 @@ class BaseValidator {
|
|
|
24
93
|
this.rules = [];
|
|
25
94
|
this.asyncRules = [];
|
|
26
95
|
this.isOptional = false;
|
|
96
|
+
this.regexTimeout = 1000; // Default timeout for regex operations
|
|
27
97
|
}
|
|
28
98
|
|
|
29
99
|
required(message = 'This field is required') {
|
|
30
100
|
this.rules.push(() => {
|
|
31
101
|
// Skip validation if optional and empty
|
|
32
|
-
if (
|
|
102
|
+
if (
|
|
103
|
+
this.isOptional &&
|
|
104
|
+
(this.value === null || this.value === undefined || this.value === '')
|
|
105
|
+
) {
|
|
33
106
|
return new ValidationResult(true);
|
|
34
107
|
}
|
|
35
108
|
|
|
36
|
-
if (
|
|
109
|
+
if (
|
|
110
|
+
this.value === null ||
|
|
111
|
+
this.value === undefined ||
|
|
112
|
+
this.value === ''
|
|
113
|
+
) {
|
|
37
114
|
return new ValidationResult(false, [message]);
|
|
38
115
|
}
|
|
39
116
|
return new ValidationResult(true);
|
|
@@ -46,10 +123,18 @@ class BaseValidator {
|
|
|
46
123
|
return this;
|
|
47
124
|
}
|
|
48
125
|
|
|
126
|
+
setRegexTimeout(timeoutMs) {
|
|
127
|
+
this.regexTimeout = timeoutMs;
|
|
128
|
+
return this;
|
|
129
|
+
}
|
|
130
|
+
|
|
49
131
|
min(length, message = `Minimum length is ${length}`) {
|
|
50
132
|
this.rules.push(() => {
|
|
51
133
|
// Skip validation if optional and empty
|
|
52
|
-
if (
|
|
134
|
+
if (
|
|
135
|
+
this.isOptional &&
|
|
136
|
+
(this.value === null || this.value === undefined || this.value === '')
|
|
137
|
+
) {
|
|
53
138
|
return new ValidationResult(true);
|
|
54
139
|
}
|
|
55
140
|
|
|
@@ -66,7 +151,9 @@ class BaseValidator {
|
|
|
66
151
|
return new ValidationResult(false, [message]);
|
|
67
152
|
}
|
|
68
153
|
} else {
|
|
69
|
-
return new ValidationResult(false, [
|
|
154
|
+
return new ValidationResult(false, [
|
|
155
|
+
'Value must be a string, array, or number'
|
|
156
|
+
]);
|
|
70
157
|
}
|
|
71
158
|
}
|
|
72
159
|
return new ValidationResult(true);
|
|
@@ -77,7 +164,10 @@ class BaseValidator {
|
|
|
77
164
|
max(length, message = `Maximum length is ${length}`) {
|
|
78
165
|
this.rules.push(() => {
|
|
79
166
|
// Skip validation if optional and empty
|
|
80
|
-
if (
|
|
167
|
+
if (
|
|
168
|
+
this.isOptional &&
|
|
169
|
+
(this.value === null || this.value === undefined || this.value === '')
|
|
170
|
+
) {
|
|
81
171
|
return new ValidationResult(true);
|
|
82
172
|
}
|
|
83
173
|
|
|
@@ -94,7 +184,9 @@ class BaseValidator {
|
|
|
94
184
|
return new ValidationResult(false, [message]);
|
|
95
185
|
}
|
|
96
186
|
} else {
|
|
97
|
-
return new ValidationResult(false, [
|
|
187
|
+
return new ValidationResult(false, [
|
|
188
|
+
'Value must be a string, array or number'
|
|
189
|
+
]);
|
|
98
190
|
}
|
|
99
191
|
}
|
|
100
192
|
return new ValidationResult(true);
|
|
@@ -103,9 +195,61 @@ class BaseValidator {
|
|
|
103
195
|
}
|
|
104
196
|
|
|
105
197
|
pattern(regex, message = 'Invalid format') {
|
|
198
|
+
// SECURITY FIX: Add regex safety check
|
|
199
|
+
if (!isRegexSafe(regex)) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
'Potentially unsafe regex pattern detected. Please use a simple pattern.'
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
106
205
|
this.rules.push(() => {
|
|
107
206
|
// Skip validation if optional and empty
|
|
108
|
-
if (
|
|
207
|
+
if (
|
|
208
|
+
this.isOptional &&
|
|
209
|
+
(this.value === null || this.value === undefined || this.value === '')
|
|
210
|
+
) {
|
|
211
|
+
return new ValidationResult(true);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Only test pattern if value exists and is not empty
|
|
215
|
+
if (this.value != null && this.value !== '') {
|
|
216
|
+
// Ensure value is a string before testing regex
|
|
217
|
+
const stringValue = String(this.value);
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
// SECURITY FIX: Use safe regex test with input length protection
|
|
221
|
+
if (!safeRegexTestSync(regex, stringValue)) {
|
|
222
|
+
return new ValidationResult(false, [message]);
|
|
223
|
+
}
|
|
224
|
+
} catch (error) {
|
|
225
|
+
if (error.message.includes('Input too long')) {
|
|
226
|
+
return new ValidationResult(false, [
|
|
227
|
+
'Input too long for pattern validation'
|
|
228
|
+
]);
|
|
229
|
+
}
|
|
230
|
+
return new ValidationResult(false, ['Pattern validation failed']);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return new ValidationResult(true);
|
|
234
|
+
});
|
|
235
|
+
return this;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// New method for async pattern validation with timeout protection
|
|
239
|
+
patternAsync(regex, message = 'Invalid format') {
|
|
240
|
+
// Security Fix: Add regex safety check
|
|
241
|
+
if (!isRegexSafe(regex)) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
'Potentially unsafe regex pattern detected. Please use a simple pattern.'
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
this.asyncRules.push(async () => {
|
|
248
|
+
// Skip validation if optional and empty
|
|
249
|
+
if (
|
|
250
|
+
this.isOptional &&
|
|
251
|
+
(this.value === null || this.value === undefined || this.value === '')
|
|
252
|
+
) {
|
|
109
253
|
return new ValidationResult(true);
|
|
110
254
|
}
|
|
111
255
|
|
|
@@ -113,8 +257,31 @@ class BaseValidator {
|
|
|
113
257
|
if (this.value != null && this.value !== '') {
|
|
114
258
|
// Ensure value is a string before testing regex
|
|
115
259
|
const stringValue = String(this.value);
|
|
116
|
-
|
|
117
|
-
|
|
260
|
+
|
|
261
|
+
// Security Fix: Limit input length to prevent ReDoS
|
|
262
|
+
if (stringValue.length > 10000) {
|
|
263
|
+
return new ValidationResult(false, [
|
|
264
|
+
'Input too long for pattern validation'
|
|
265
|
+
]);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
// Security Fix: Use timeout protection for regex execution
|
|
270
|
+
const result = await safeRegexTest(
|
|
271
|
+
regex,
|
|
272
|
+
stringValue,
|
|
273
|
+
this.regexTimeout
|
|
274
|
+
);
|
|
275
|
+
if (!result) {
|
|
276
|
+
return new ValidationResult(false, [message]);
|
|
277
|
+
}
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (error.message.includes('timeout')) {
|
|
280
|
+
return new ValidationResult(false, [
|
|
281
|
+
'Pattern validation timeout - pattern too complex'
|
|
282
|
+
]);
|
|
283
|
+
}
|
|
284
|
+
return new ValidationResult(false, ['Pattern validation failed']);
|
|
118
285
|
}
|
|
119
286
|
}
|
|
120
287
|
return new ValidationResult(true);
|
|
@@ -125,7 +292,8 @@ class BaseValidator {
|
|
|
125
292
|
when(condition, validator) {
|
|
126
293
|
this.rules.push(() => {
|
|
127
294
|
// Evaluate condition
|
|
128
|
-
const shouldValidate =
|
|
295
|
+
const shouldValidate =
|
|
296
|
+
typeof condition === 'function' ? condition(this.value) : condition;
|
|
129
297
|
|
|
130
298
|
if (shouldValidate) {
|
|
131
299
|
// Apply the conditional validator
|
|
@@ -146,7 +314,10 @@ class BaseValidator {
|
|
|
146
314
|
custom(validatorFn, message = 'Custom validation failed') {
|
|
147
315
|
this.rules.push(() => {
|
|
148
316
|
// Skip validation if optional and empty
|
|
149
|
-
if (
|
|
317
|
+
if (
|
|
318
|
+
this.isOptional &&
|
|
319
|
+
(this.value === null || this.value === undefined || this.value === '')
|
|
320
|
+
) {
|
|
150
321
|
return new ValidationResult(true);
|
|
151
322
|
}
|
|
152
323
|
|
|
@@ -155,7 +326,9 @@ class BaseValidator {
|
|
|
155
326
|
|
|
156
327
|
// Handle boolean result
|
|
157
328
|
if (typeof result === 'boolean') {
|
|
158
|
-
return result
|
|
329
|
+
return result
|
|
330
|
+
? new ValidationResult(true)
|
|
331
|
+
: new ValidationResult(false, [message]);
|
|
159
332
|
}
|
|
160
333
|
|
|
161
334
|
// Handle ValidationResult object
|
|
@@ -171,7 +344,9 @@ class BaseValidator {
|
|
|
171
344
|
// Default to true if no clear result
|
|
172
345
|
return new ValidationResult(true);
|
|
173
346
|
} catch (error) {
|
|
174
|
-
return new ValidationResult(false, [
|
|
347
|
+
return new ValidationResult(false, [
|
|
348
|
+
`Custom validation error: ${error.message}`
|
|
349
|
+
]);
|
|
175
350
|
}
|
|
176
351
|
});
|
|
177
352
|
return this;
|
|
@@ -180,7 +355,10 @@ class BaseValidator {
|
|
|
180
355
|
customAsync(validatorFn, message = 'Async validation failed') {
|
|
181
356
|
this.asyncRules.push(async () => {
|
|
182
357
|
// Skip validation if optional and empty
|
|
183
|
-
if (
|
|
358
|
+
if (
|
|
359
|
+
this.isOptional &&
|
|
360
|
+
(this.value === null || this.value === undefined || this.value === '')
|
|
361
|
+
) {
|
|
184
362
|
return new ValidationResult(true);
|
|
185
363
|
}
|
|
186
364
|
|
|
@@ -189,7 +367,9 @@ class BaseValidator {
|
|
|
189
367
|
|
|
190
368
|
// Handle boolean result
|
|
191
369
|
if (typeof result === 'boolean') {
|
|
192
|
-
return result
|
|
370
|
+
return result
|
|
371
|
+
? new ValidationResult(true)
|
|
372
|
+
: new ValidationResult(false, [message]);
|
|
193
373
|
}
|
|
194
374
|
|
|
195
375
|
// Handle ValidationResult object
|
|
@@ -205,7 +385,9 @@ class BaseValidator {
|
|
|
205
385
|
// Default to true if no clear result
|
|
206
386
|
return new ValidationResult(true);
|
|
207
387
|
} catch (error) {
|
|
208
|
-
return new ValidationResult(false, [
|
|
388
|
+
return new ValidationResult(false, [
|
|
389
|
+
`Async validation error: ${error.message}`
|
|
390
|
+
]);
|
|
209
391
|
}
|
|
210
392
|
});
|
|
211
393
|
return this;
|
|
@@ -261,6 +443,7 @@ class BaseValidator {
|
|
|
261
443
|
}
|
|
262
444
|
|
|
263
445
|
// Predefined validators
|
|
446
|
+
// Security Fix: Updated predefined validators with safer regex patterns
|
|
264
447
|
const validators = {
|
|
265
448
|
email: (value) => {
|
|
266
449
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -270,15 +453,25 @@ const validators = {
|
|
|
270
453
|
},
|
|
271
454
|
|
|
272
455
|
phone: (value, format = 'us') => {
|
|
456
|
+
// FIXED: Much simpler phone regex patterns to avoid ReDoS detection
|
|
273
457
|
const phoneRegex = {
|
|
274
|
-
us:
|
|
275
|
-
international:
|
|
276
|
-
simple:
|
|
458
|
+
us: /^[+]?[1]?[0-9]{10}$/,
|
|
459
|
+
international: /^[+][1-9][0-9]{7,14}$/,
|
|
460
|
+
simple: /^[0-9]{10,15}$/
|
|
277
461
|
};
|
|
278
462
|
|
|
279
463
|
return new BaseValidator(value)
|
|
280
464
|
.required('Phone number is required')
|
|
281
|
-
.
|
|
465
|
+
.custom((val) => {
|
|
466
|
+
// Remove all non-digit characters except +
|
|
467
|
+
const cleaned = String(val).replace(/[^+0-9]/g, '');
|
|
468
|
+
const regex = phoneRegex[format] || phoneRegex.simple;
|
|
469
|
+
|
|
470
|
+
if (!safeRegexTestSync(regex, cleaned)) {
|
|
471
|
+
return 'Invalid phone number format';
|
|
472
|
+
}
|
|
473
|
+
return true;
|
|
474
|
+
});
|
|
282
475
|
},
|
|
283
476
|
|
|
284
477
|
creditCard: (value) => {
|
|
@@ -305,22 +498,30 @@ const validators = {
|
|
|
305
498
|
return sum % 10 === 0;
|
|
306
499
|
};
|
|
307
500
|
|
|
308
|
-
const validator = new BaseValidator(value)
|
|
309
|
-
|
|
501
|
+
const validator = new BaseValidator(value).required(
|
|
502
|
+
'Credit card number is required'
|
|
503
|
+
);
|
|
310
504
|
|
|
311
505
|
// Add custom validation for credit card format and Luhn check
|
|
312
506
|
validator.rules.push(() => {
|
|
313
507
|
// Skip validation if optional and empty
|
|
314
|
-
if (
|
|
508
|
+
if (
|
|
509
|
+
validator.isOptional &&
|
|
510
|
+
(validator.value === null ||
|
|
511
|
+
validator.value === undefined ||
|
|
512
|
+
validator.value === '')
|
|
513
|
+
) {
|
|
315
514
|
return new ValidationResult(true);
|
|
316
515
|
}
|
|
317
516
|
|
|
318
517
|
if (validator.value) {
|
|
319
518
|
const cleanValue = String(validator.value).replace(/\s/g, '');
|
|
320
519
|
|
|
321
|
-
// Check length (13-19 digits)
|
|
322
|
-
if (
|
|
323
|
-
return new ValidationResult(false, [
|
|
520
|
+
// Check length (13-19 digits) using safe regex
|
|
521
|
+
if (!safeRegexTestSync(/^\d{13,19}$/, cleanValue)) {
|
|
522
|
+
return new ValidationResult(false, [
|
|
523
|
+
'Credit card must be 13-19 digits'
|
|
524
|
+
]);
|
|
324
525
|
}
|
|
325
526
|
|
|
326
527
|
// Check Luhn algorithm
|
|
@@ -355,16 +556,25 @@ const validators = {
|
|
|
355
556
|
.min(minLength, `Password must be at least ${minLength} characters`);
|
|
356
557
|
|
|
357
558
|
if (requireUppercase) {
|
|
358
|
-
validator.pattern(
|
|
559
|
+
validator.pattern(
|
|
560
|
+
/[A-Z]/,
|
|
561
|
+
'Password must contain at least one uppercase letter'
|
|
562
|
+
);
|
|
359
563
|
}
|
|
360
564
|
if (requireLowercase) {
|
|
361
|
-
validator.pattern(
|
|
565
|
+
validator.pattern(
|
|
566
|
+
/[a-z]/,
|
|
567
|
+
'Password must contain at least one lowercase letter'
|
|
568
|
+
);
|
|
362
569
|
}
|
|
363
570
|
if (requireNumbers) {
|
|
364
571
|
validator.pattern(/\d/, 'Password must contain at least one number');
|
|
365
572
|
}
|
|
366
573
|
if (requireSpecialChars) {
|
|
367
|
-
validator.pattern(
|
|
574
|
+
validator.pattern(
|
|
575
|
+
/[!@#$%^&*(),.?":{}|<>]/,
|
|
576
|
+
'Password must contain at least one special character'
|
|
577
|
+
);
|
|
368
578
|
}
|
|
369
579
|
|
|
370
580
|
return validator;
|
|
@@ -412,9 +622,10 @@ const validate = (schema, data) => {
|
|
|
412
622
|
for (const [field, validator] of Object.entries(schema)) {
|
|
413
623
|
try {
|
|
414
624
|
const fieldValue = data[field];
|
|
415
|
-
const result =
|
|
416
|
-
|
|
417
|
-
|
|
625
|
+
const result =
|
|
626
|
+
typeof validator === 'function'
|
|
627
|
+
? validator(fieldValue).validate()
|
|
628
|
+
: validator.validate();
|
|
418
629
|
|
|
419
630
|
results[field] = result;
|
|
420
631
|
if (!result.isValid) {
|
|
@@ -422,7 +633,9 @@ const validate = (schema, data) => {
|
|
|
422
633
|
}
|
|
423
634
|
} catch (error) {
|
|
424
635
|
// Handle validation setup errors
|
|
425
|
-
results[field] = new ValidationResult(false, [
|
|
636
|
+
results[field] = new ValidationResult(false, [
|
|
637
|
+
`Validation setup error: ${error.message}`
|
|
638
|
+
]);
|
|
426
639
|
isValid = false;
|
|
427
640
|
}
|
|
428
641
|
}
|
|
@@ -463,13 +676,15 @@ const validateAsync = async (schema, data) => {
|
|
|
463
676
|
let result;
|
|
464
677
|
if (typeof validator === 'function') {
|
|
465
678
|
const validatorInstance = validator(fieldValue);
|
|
466
|
-
result =
|
|
467
|
-
|
|
468
|
-
|
|
679
|
+
result =
|
|
680
|
+
validatorInstance.asyncRules.length > 0
|
|
681
|
+
? await validatorInstance.validateAsync()
|
|
682
|
+
: validatorInstance.validate();
|
|
469
683
|
} else {
|
|
470
|
-
result =
|
|
471
|
-
|
|
472
|
-
|
|
684
|
+
result =
|
|
685
|
+
validator.asyncRules && validator.asyncRules.length > 0
|
|
686
|
+
? await validator.validateAsync()
|
|
687
|
+
: validator.validate();
|
|
473
688
|
}
|
|
474
689
|
|
|
475
690
|
results[field] = result;
|
|
@@ -478,7 +693,9 @@ const validateAsync = async (schema, data) => {
|
|
|
478
693
|
}
|
|
479
694
|
} catch (error) {
|
|
480
695
|
// Handle validation setup errors
|
|
481
|
-
results[field] = new ValidationResult(false, [
|
|
696
|
+
results[field] = new ValidationResult(false, [
|
|
697
|
+
`Validation setup error: ${error.message}`
|
|
698
|
+
]);
|
|
482
699
|
isValid = false;
|
|
483
700
|
}
|
|
484
701
|
}
|
|
@@ -503,5 +720,8 @@ module.exports = {
|
|
|
503
720
|
ValidationResult,
|
|
504
721
|
validators,
|
|
505
722
|
validate,
|
|
506
|
-
validateAsync
|
|
723
|
+
validateAsync,
|
|
724
|
+
safeRegexTest,
|
|
725
|
+
safeRegexTestSync,
|
|
726
|
+
isRegexSafe
|
|
507
727
|
};
|