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 CHANGED
@@ -3,23 +3,28 @@
3
3
  [![npm version](https://img.shields.io/npm/v/snap-validate.svg?style=flat-square)](https://www.npmjs.com/package/snap-validate)
4
4
  [![Build Status](https://github.com/aniru-dh21/snap-validate/workflows/CI%2FCD%20Pipeline/badge.svg)](https://github.com/aniru-dh21/snap-validate/actions)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![install size](https://img.shields.io/badge/dynamic/json?url=https://packagephobia.com/v2/api.json?p=snap-validate&query=$.install.pretty&label=install%20size&style=flat-square)](https://packagephobia.now.sh/result?p=snap-validate)
7
+ [![npm bundle size](https://img.shields.io/bundlephobia/minzip/snap-validate?style=flat-square)](https://bundlephobia.com/package/snap-validate@latest)
6
8
  [![npm downloads](https://img.shields.io/npm/dm/snap-validate.svg?style=flat-square)](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
- - ๐Ÿงช **Well Tested**: Comprehensive test suite with high coverage
21
- - ๐Ÿ“ฆ **Easy Integration**: Works in Node.js and browsers
22
- - ๐Ÿ”— **Chainable API**: Intuitive fluent interface
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.0",
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.0
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 (this.isOptional && (this.value === null || this.value === undefined || this.value === '')) {
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 (this.value === null || this.value === undefined || this.value === '') {
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 (this.isOptional && (this.value === null || this.value === undefined || this.value === '')) {
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, ['Value must be a string, array, or number']);
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 (this.isOptional && (this.value === null || this.value === undefined || this.value === '')) {
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, ['Value must be a string, array or number']);
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 (this.isOptional && (this.value === null || this.value === undefined || this.value === '')) {
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
- if (!regex.test(stringValue)) {
117
- return new ValidationResult(false, [message]);
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 = typeof condition === 'function' ? condition(this.value) : condition;
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 (this.isOptional && (this.value === null || this.value === undefined || this.value === '')) {
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 ? new ValidationResult(true) : new ValidationResult(false, [message]);
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, [`Custom validation error: ${error.message}`]);
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 (this.isOptional && (this.value === null || this.value === undefined || this.value === '')) {
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 ? new ValidationResult(true) : new ValidationResult(false, [message]);
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, [`Async validation error: ${error.message}`]);
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: /^\+?1?[-.\s]?\(?([0-9]{3})\)?[-.\s]?([0-9]{3})[-.\s]?([0-9]{4})$/,
275
- international: /^\+[1-9]\d{1,14}$/,
276
- simple: /^\d{10,15}$/
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
- .pattern(phoneRegex[format] || phoneRegex.simple, 'Invalid phone number format');
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
- .required('Credit card number is required');
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 (validator.isOptional && (validator.value === null || validator.value === undefined || validator.value === '')) {
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 (!/^\d{13,19}$/.test(cleanValue)) {
323
- return new ValidationResult(false, ['Credit card must be 13-19 digits']);
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(/[A-Z]/, 'Password must contain at least one uppercase letter');
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(/[a-z]/, 'Password must contain at least one lowercase letter');
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(/[!@#$%^&*(),.?":{}|<>]/, 'Password must contain at least one special character');
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 = typeof validator === 'function'
416
- ? validator(fieldValue).validate()
417
- : validator.validate();
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, [`Validation setup error: ${error.message}`]);
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 = validatorInstance.asyncRules.length > 0
467
- ? await validatorInstance.validateAsync()
468
- : validatorInstance.validate();
678
+ result =
679
+ validatorInstance.asyncRules.length > 0
680
+ ? await validatorInstance.validateAsync()
681
+ : validatorInstance.validate();
469
682
  } else {
470
- result = validator.asyncRules && validator.asyncRules.length > 0
471
- ? await validator.validateAsync()
472
- : validator.validate();
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, [`Validation setup error: ${error.message}`]);
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
  }