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