@angular-helpers/security 21.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +380 -0
- package/ng-package.json +7 -0
- package/package.json +49 -0
- package/src/index.ts +1 -0
- package/src/interfaces/security.interface.ts +32 -0
- package/src/providers.ts +29 -0
- package/src/services/regex-security.service.ts +487 -0
package/README.md
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
[English](README.en.md) | [Español](README.md)
|
|
2
|
+
|
|
3
|
+
# Angular Security Helpers
|
|
4
|
+
|
|
5
|
+
Security package for Angular applications that prevents common attacks like ReDoS (Regular Expression Denial of Service) using Web Workers for safe execution.
|
|
6
|
+
|
|
7
|
+
## 🛡️ Features
|
|
8
|
+
|
|
9
|
+
### **ReDoS Prevention**
|
|
10
|
+
- **Web Worker Execution**: Regular expressions are executed in a separate thread.
|
|
11
|
+
- **Configurable Timeout**: Prevents infinite executions.
|
|
12
|
+
- **Complexity Analysis**: Detects dangerous patterns before execution.
|
|
13
|
+
- **Safe Mode**: Only allows patterns verified as safe.
|
|
14
|
+
|
|
15
|
+
### **Builder Pattern**
|
|
16
|
+
- **Fluent API**: Intuitively build regular expressions.
|
|
17
|
+
- **Method Chaining**: `.pattern().group().quantifier()`
|
|
18
|
+
- **Real-time Validation**: Security analysis during construction.
|
|
19
|
+
|
|
20
|
+
## 📦 Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @angular-helpers/security
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 🚀 Basic Usage
|
|
27
|
+
|
|
28
|
+
### **Configuration**
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { provideSecurity } from '@angular-helpers/security';
|
|
32
|
+
|
|
33
|
+
bootstrapApplication(AppComponent, {
|
|
34
|
+
providers: [
|
|
35
|
+
provideSecurity({
|
|
36
|
+
enableRegexSecurity: true,
|
|
37
|
+
defaultTimeout: 5000,
|
|
38
|
+
safeMode: false
|
|
39
|
+
})
|
|
40
|
+
]
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### **Service Injection**
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { RegexSecurityService } from '@angular-helpers/security';
|
|
48
|
+
|
|
49
|
+
@Component({...})
|
|
50
|
+
export class MyComponent {
|
|
51
|
+
constructor(private regexSecurity: RegexSecurityService) {}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 📖 Usage Examples
|
|
56
|
+
|
|
57
|
+
### **1. Basic Regular Expression Test**
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
async testEmail(email: string): Promise<boolean> {
|
|
61
|
+
const result = await this.regexSecurity.testRegex(
|
|
62
|
+
'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
|
|
63
|
+
email,
|
|
64
|
+
{ timeout: 3000 }
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return result.match;
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### **2. Builder Pattern**
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { RegexSecurityBuilder } from '@angular-helpers/security';
|
|
75
|
+
|
|
76
|
+
// Fluent regular expression construction
|
|
77
|
+
const emailRegex = RegexSecurityBuilder
|
|
78
|
+
.builder()
|
|
79
|
+
.startOfLine()
|
|
80
|
+
.characterSet('a-zA-Z0-9._%+-')
|
|
81
|
+
.quantifier('+')
|
|
82
|
+
.append('@')
|
|
83
|
+
.characterSet('a-zA-Z0-9.-')
|
|
84
|
+
.quantifier('+')
|
|
85
|
+
.append('\\.')
|
|
86
|
+
.characterSet('a-zA-Z')
|
|
87
|
+
.quantifier('{2,}')
|
|
88
|
+
.endOfLine()
|
|
89
|
+
.timeout(5000)
|
|
90
|
+
.safeMode()
|
|
91
|
+
.build();
|
|
92
|
+
|
|
93
|
+
// Direct execution
|
|
94
|
+
const result = await RegexSecurityBuilder
|
|
95
|
+
.builder()
|
|
96
|
+
.pattern('^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$')
|
|
97
|
+
.timeout(3000)
|
|
98
|
+
.execute(email, this.regexSecurity);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### **3. Security Analysis**
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
async analyzePattern(pattern: string): Promise<void> {
|
|
105
|
+
const analysis = await this.regexSecurity.analyzePatternSecurity(pattern);
|
|
106
|
+
|
|
107
|
+
if (!analysis.safe) {
|
|
108
|
+
console.warn('⚠️ Pattern not safe:', analysis.warnings);
|
|
109
|
+
console.info('💡 Recommendations:', analysis.recommendations);
|
|
110
|
+
|
|
111
|
+
if (analysis.risk === 'critical') {
|
|
112
|
+
throw new Error('Pattern rejected due to critical security risk');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(`✅ Pattern complexity: ${analysis.complexity}`);
|
|
117
|
+
console.log(`🎯 Risk level: ${analysis.risk}`);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### **4. Form Validation**
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
@Component({...})
|
|
125
|
+
export class FormValidationComponent {
|
|
126
|
+
constructor(private regexSecurity: RegexSecurityService) {}
|
|
127
|
+
|
|
128
|
+
async validateUsername(username: string): Promise<boolean> {
|
|
129
|
+
const result = await this.regexSecurity.testRegex(
|
|
130
|
+
'^[a-zA-Z0-9_]{3,20}$',
|
|
131
|
+
username,
|
|
132
|
+
{ timeout: 1000, safeMode: true }
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
if (result.timeout) {
|
|
136
|
+
throw new Error('Username validation timeout - possible ReDoS attack');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (result.error) {
|
|
140
|
+
console.error('Validation error:', result.error);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return result.match;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async validateComplexInput(input: string): Promise<boolean> {
|
|
148
|
+
// Builder pattern for complex validation
|
|
149
|
+
const result = await RegexSecurityBuilder
|
|
150
|
+
.builder()
|
|
151
|
+
.startOfLine()
|
|
152
|
+
.nonCapturingGroup('[a-zA-Z]') // First letter
|
|
153
|
+
.characterSet('a-zA-Z0-9_') // Allowed characters
|
|
154
|
+
.quantifier('{2,19}') // Between 3 and 20 characters total
|
|
155
|
+
.endOfLine()
|
|
156
|
+
.timeout(2000)
|
|
157
|
+
.execute(input, this.regexSecurity);
|
|
158
|
+
|
|
159
|
+
return result.match;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## 🔧 Advanced Configuration
|
|
165
|
+
|
|
166
|
+
### **Security Options**
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
interface RegexSecurityConfig {
|
|
170
|
+
timeout?: number; // Timeout in ms (default: 5000)
|
|
171
|
+
maxComplexity?: number; // Max complexity (default: 10)
|
|
172
|
+
allowBacktracking?: boolean; // Allow backtracking (default: false)
|
|
173
|
+
safeMode?: boolean; // Safe mode (default: false)
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### **Builder Options**
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
interface RegexBuilderOptions {
|
|
181
|
+
global?: boolean; // 'g' flag
|
|
182
|
+
ignoreCase?: boolean; // 'i' flag
|
|
183
|
+
multiline?: boolean; // 'm' flag
|
|
184
|
+
dotAll?: boolean; // 's' flag
|
|
185
|
+
unicode?: boolean; // 'u' flag
|
|
186
|
+
sticky?: boolean; // 'y' flag
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## 🛡️ Security Features
|
|
191
|
+
|
|
192
|
+
### **Dangerous Pattern Detection**
|
|
193
|
+
|
|
194
|
+
The service automatically detects:
|
|
195
|
+
|
|
196
|
+
- **Nested quantifiers**: `**`, `++` (catastrophic backtracking)
|
|
197
|
+
- **Lookaheads/lookbehinds**: `(?=)`, `(?!)`, `(?<=)`, `(?<!)`
|
|
198
|
+
- **Atomic groups**: `(?>)`
|
|
199
|
+
- **Recursive patterns**: Deeply nested groups
|
|
200
|
+
- **Complex quantifiers**: `{n,m}` with high values
|
|
201
|
+
- **Greedy wildcards**: `.*`, `.+` with variable characters
|
|
202
|
+
|
|
203
|
+
### **Risk Levels**
|
|
204
|
+
|
|
205
|
+
- **🟢 Low**: Simple and safe patterns
|
|
206
|
+
- **🟡 Medium**: Patterns with lookahead/lookbehind
|
|
207
|
+
- **🟠 High**: Patterns with complex quantifiers
|
|
208
|
+
- **🔴 Critical**: Patterns with catastrophic backtracking
|
|
209
|
+
|
|
210
|
+
### **Attack Prevention**
|
|
211
|
+
|
|
212
|
+
- **Timeout**: Stops execution after the time limit
|
|
213
|
+
- **Web Worker**: Isolates execution from the main thread
|
|
214
|
+
- **Pre-analysis**: Rejects dangerous patterns before execution
|
|
215
|
+
- **Match limit**: Prevents infinite loops
|
|
216
|
+
|
|
217
|
+
## 📊 Metrics and Monitoring
|
|
218
|
+
|
|
219
|
+
### **Execution Results**
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
interface RegexTestResult {
|
|
223
|
+
match: boolean; // If there was a match
|
|
224
|
+
matches?: RegExpMatchArray[]; // All matches found
|
|
225
|
+
groups?: { [key: string]: string }; // Captured groups
|
|
226
|
+
executionTime: number; // Execution time in ms
|
|
227
|
+
timeout: boolean; // If there was a timeout
|
|
228
|
+
error?: string; // Error if one occurred
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### **Security Analysis**
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
interface RegexSecurityResult {
|
|
236
|
+
safe: boolean; // If the pattern is safe
|
|
237
|
+
complexity: number; // Complexity level (0-∞)
|
|
238
|
+
risk: 'low' | 'medium' | 'high' | 'critical';
|
|
239
|
+
warnings: string[]; // Security warnings
|
|
240
|
+
recommendations: string[]; // Improvement recommendations
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## 🔄 Form Integration
|
|
245
|
+
|
|
246
|
+
### **Angular Validators**
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
import { AbstractControl, ValidationErrors } from '@angular/forms';
|
|
250
|
+
|
|
251
|
+
export class SecurityValidators {
|
|
252
|
+
constructor(private regexSecurity: RegexSecurityService) {}
|
|
253
|
+
|
|
254
|
+
async securePattern(pattern: string, config?: RegexSecurityConfig) {
|
|
255
|
+
return async (control: AbstractControl): Promise<ValidationErrors | null> => {
|
|
256
|
+
const value = control.value;
|
|
257
|
+
|
|
258
|
+
if (!value) return null;
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const result = await this.regexSecurity.testRegex(pattern, value, config);
|
|
262
|
+
|
|
263
|
+
if (!result.match) {
|
|
264
|
+
return { securePattern: { value, reason: 'Pattern does not match' } };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (result.timeout) {
|
|
268
|
+
return { securePattern: { value, reason: 'Pattern execution timeout' } };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return null;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
return { securePattern: { value, reason: error.message } };
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### **Usage in Template Forms**
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
@Component({...})
|
|
284
|
+
export class SecureFormComponent {
|
|
285
|
+
constructor(
|
|
286
|
+
private regexSecurity: RegexSecurityService,
|
|
287
|
+
private securityValidators: SecurityValidators
|
|
288
|
+
) {}
|
|
289
|
+
|
|
290
|
+
emailValidator = this.securityValidators.securePattern(
|
|
291
|
+
'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
|
|
292
|
+
{ timeout: 3000, safeMode: true }
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## 🚨 Best Practices
|
|
298
|
+
|
|
299
|
+
### **1. Use Safe Mode in Production**
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
// In production, always use safeMode
|
|
303
|
+
const config = { safeMode: true, timeout: 3000 };
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### **2. Appropriate Timeout**
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
// For form validations: 1-3 seconds
|
|
310
|
+
// For text processing: 5-10 seconds
|
|
311
|
+
// Never more than 30 seconds
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### **3. Pre-analysis**
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
// Always analyze user-provided patterns
|
|
318
|
+
const analysis = await this.regexSecurity.analyzePatternSecurity(pattern);
|
|
319
|
+
if (!analysis.safe) {
|
|
320
|
+
// Consider using a safer alternative pattern
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### **4. Error Handling**
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
try {
|
|
328
|
+
const result = await this.regexSecurity.testRegex(pattern, text, config);
|
|
329
|
+
// Process result
|
|
330
|
+
} catch (error) {
|
|
331
|
+
// Handle error safely
|
|
332
|
+
console.error('Regex security error:', error);
|
|
333
|
+
// Fallback to a simpler validation
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## 🔍 Debugging
|
|
338
|
+
|
|
339
|
+
### **Security Logging**
|
|
340
|
+
|
|
341
|
+
The service includes automatic logging:
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
// Enables detailed logging
|
|
345
|
+
console.log('Regex security initialized');
|
|
346
|
+
console.log('Pattern analysis completed:', analysis);
|
|
347
|
+
console.log('Pattern execution completed:', result);
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### **Performance Monitoring**
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
// Monitor execution times
|
|
354
|
+
if (result.executionTime > 1000) {
|
|
355
|
+
console.warn('Slow regex pattern:', pattern, result.executionTime + 'ms');
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## 📝 License
|
|
360
|
+
|
|
361
|
+
MIT License - see the LICENSE file for details.
|
|
362
|
+
|
|
363
|
+
## 🤝 Contributions
|
|
364
|
+
|
|
365
|
+
Contributions are welcome. Please:
|
|
366
|
+
|
|
367
|
+
1. Create an issue to discuss changes
|
|
368
|
+
2. Fork the repository
|
|
369
|
+
3. Create a feature branch
|
|
370
|
+
4. Send a pull request
|
|
371
|
+
|
|
372
|
+
## 📚 Additional Resources
|
|
373
|
+
|
|
374
|
+
- [OWASP Regular Expression Denial of Service](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS)
|
|
375
|
+
- [MDN Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API)
|
|
376
|
+
- [Angular Security Best Practices](https://angular.io/guide/security)
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
**⚠️ Warning**: This package helps prevent ReDoS but does not replace other security practices. Always validate and sanitize user inputs.
|
package/ng-package.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@angular-helpers/security",
|
|
3
|
+
"version": "21.0.0",
|
|
4
|
+
"description": "Angular security helpers for preventing ReDoS and other security vulnerabilities",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"angular",
|
|
7
|
+
"security",
|
|
8
|
+
"regex",
|
|
9
|
+
"redos",
|
|
10
|
+
"prevention",
|
|
11
|
+
"web-worker",
|
|
12
|
+
"builder-pattern"
|
|
13
|
+
],
|
|
14
|
+
"author": "Angular Helpers Team",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/angular-helpers/security"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/angular-helpers/security/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/angular-helpers/security#readme",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "ng-packagr -p ng-package.json"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@angular/common": "^21.0.0",
|
|
32
|
+
"@angular/core": "^21.0.0",
|
|
33
|
+
"@angular-helpers/browser-web-apis": "21.0.0",
|
|
34
|
+
"rxjs": "^7.0.0 || ^8.0.0",
|
|
35
|
+
"tslib": "^2.0.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"tslib": "^2.0.0"
|
|
39
|
+
},
|
|
40
|
+
"ngPackage": {
|
|
41
|
+
"lib": {
|
|
42
|
+
"entryFile": "src/index.ts"
|
|
43
|
+
},
|
|
44
|
+
"dest": "../../dist/security",
|
|
45
|
+
"allowedNonPeerDependencies": [
|
|
46
|
+
"tslib"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './services/regex-security.service';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface RegexSecurityConfig {
|
|
2
|
+
timeout?: number; // Timeout en milisegundos (default: 5000)
|
|
3
|
+
maxComplexity?: number; // Máxima complejidad permitida
|
|
4
|
+
allowBacktracking?: boolean; // Permitir backtracking catastrófico
|
|
5
|
+
safeMode?: boolean; // Modo seguro solo con patrones seguros
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface RegexTestResult {
|
|
9
|
+
match: boolean;
|
|
10
|
+
matches?: RegExpMatchArray[];
|
|
11
|
+
groups?: { [key: string]: string };
|
|
12
|
+
executionTime: number;
|
|
13
|
+
timeout: boolean;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RegexSecurityResult {
|
|
18
|
+
safe: boolean;
|
|
19
|
+
complexity: number;
|
|
20
|
+
risk: 'low' | 'medium' | 'high' | 'critical';
|
|
21
|
+
warnings: string[];
|
|
22
|
+
recommendations: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RegexBuilderOptions {
|
|
26
|
+
global?: boolean;
|
|
27
|
+
ignoreCase?: boolean;
|
|
28
|
+
multiline?: boolean;
|
|
29
|
+
dotAll?: boolean;
|
|
30
|
+
unicode?: boolean;
|
|
31
|
+
sticky?: boolean;
|
|
32
|
+
}
|
package/src/providers.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { makeEnvironmentProviders, EnvironmentProviders } from '@angular/core';
|
|
2
|
+
import { RegexSecurityService } from './services/regex-security.service';
|
|
3
|
+
|
|
4
|
+
export interface SecurityConfig {
|
|
5
|
+
enableRegexSecurity?: boolean;
|
|
6
|
+
defaultTimeout?: number;
|
|
7
|
+
safeMode?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const defaultSecurityConfig: SecurityConfig = {
|
|
11
|
+
enableRegexSecurity: true,
|
|
12
|
+
defaultTimeout: 5000,
|
|
13
|
+
safeMode: false
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function provideSecurity(config: SecurityConfig = {}): EnvironmentProviders {
|
|
17
|
+
const mergedConfig = { ...defaultSecurityConfig, ...config };
|
|
18
|
+
const providers = [];
|
|
19
|
+
|
|
20
|
+
if (mergedConfig.enableRegexSecurity) {
|
|
21
|
+
providers.push(RegexSecurityService);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return makeEnvironmentProviders(providers);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function provideRegexSecurity(): EnvironmentProviders {
|
|
28
|
+
return makeEnvironmentProviders([RegexSecurityService]);
|
|
29
|
+
}
|
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import { Injectable, inject, DestroyRef } from '@angular/core';
|
|
2
|
+
import { Observable, Subject } from 'rxjs';
|
|
3
|
+
import { map, catchError, timeout } from 'rxjs/operators';
|
|
4
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
5
|
+
|
|
6
|
+
export interface RegexSecurityConfig {
|
|
7
|
+
timeout?: number; // Timeout in milliseconds (default: 5000)
|
|
8
|
+
maxComplexity?: number; // Maximum complexity allowed
|
|
9
|
+
allowBacktracking?: boolean; // Allow catastrophic backtracking
|
|
10
|
+
safeMode?: boolean; // Safe mode with secure patterns only
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RegexTestResult {
|
|
14
|
+
match: boolean;
|
|
15
|
+
matches?: RegExpMatchArray[];
|
|
16
|
+
groups?: { [key: string]: string };
|
|
17
|
+
executionTime: number;
|
|
18
|
+
timeout: boolean;
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RegexSecurityResult {
|
|
23
|
+
safe: boolean;
|
|
24
|
+
complexity: number;
|
|
25
|
+
risk: 'low' | 'medium' | 'high' | 'critical';
|
|
26
|
+
warnings: string[];
|
|
27
|
+
recommendations: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RegexBuilderOptions {
|
|
31
|
+
global?: boolean;
|
|
32
|
+
ignoreCase?: boolean;
|
|
33
|
+
multiline?: boolean;
|
|
34
|
+
dotAll?: boolean;
|
|
35
|
+
unicode?: boolean;
|
|
36
|
+
sticky?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Security service for regular expressions that prevents ReDoS
|
|
41
|
+
* using Web Workers for safe execution with timeout
|
|
42
|
+
*/
|
|
43
|
+
@Injectable()
|
|
44
|
+
export class RegexSecurityService {
|
|
45
|
+
private destroyRef = inject(DestroyRef);
|
|
46
|
+
private workers = new Map<string, Worker>();
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Builder pattern to construct safe regular expressions
|
|
50
|
+
*/
|
|
51
|
+
static builder(): RegexSecurityBuilder {
|
|
52
|
+
return new RegexSecurityBuilder();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Executes a regular expression safely with a timeout
|
|
57
|
+
*/
|
|
58
|
+
async testRegex(
|
|
59
|
+
pattern: string,
|
|
60
|
+
text: string,
|
|
61
|
+
config: RegexSecurityConfig = {}
|
|
62
|
+
): Promise<RegexTestResult> {
|
|
63
|
+
const startTime = performance.now();
|
|
64
|
+
const finalConfig = this.mergeConfig(config);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// First, analyze pattern security
|
|
68
|
+
const securityCheck = await this.analyzePatternSecurity(pattern);
|
|
69
|
+
|
|
70
|
+
if (!securityCheck.safe && !finalConfig.safeMode) {
|
|
71
|
+
return {
|
|
72
|
+
match: false,
|
|
73
|
+
executionTime: performance.now() - startTime,
|
|
74
|
+
timeout: false,
|
|
75
|
+
error: `Pattern rejected: ${securityCheck.warnings.join(', ')}`
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Execute in Web Worker with timeout
|
|
80
|
+
const result = await this.executeInWorker(pattern, text, finalConfig);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
...result,
|
|
84
|
+
executionTime: performance.now() - startTime
|
|
85
|
+
};
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return {
|
|
88
|
+
match: false,
|
|
89
|
+
executionTime: performance.now() - startTime,
|
|
90
|
+
timeout: false,
|
|
91
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Analyzes the security of a regular expression pattern
|
|
98
|
+
*/
|
|
99
|
+
async analyzePatternSecurity(pattern: string): Promise<RegexSecurityResult> {
|
|
100
|
+
const warnings: string[] = [];
|
|
101
|
+
const recommendations: string[] = [];
|
|
102
|
+
let complexity = 0;
|
|
103
|
+
let risk: 'low' | 'medium' | 'high' | 'critical' = 'low';
|
|
104
|
+
|
|
105
|
+
// Analysis of dangerous patterns
|
|
106
|
+
const dangerousPatterns = [
|
|
107
|
+
{ pattern: /\*\*/, risk: 'high' as const, message: 'Nested quantifiers (catastrophic backtracking)' },
|
|
108
|
+
{ pattern: /\+\+/, risk: 'high' as const, message: 'Nested plus quantifiers' },
|
|
109
|
+
{ pattern: /\(\?\=/, risk: 'medium' as const, message: 'Lookahead assertions' },
|
|
110
|
+
{ pattern: /\(\?\!/, risk: 'medium' as const, message: 'Negative lookahead' },
|
|
111
|
+
{ pattern: /\(\?\:/, risk: 'low' as const, message: 'Non-capturing groups' },
|
|
112
|
+
{ pattern: /\(\?\</, risk: 'high' as const, message: 'Lookbehind assertions' },
|
|
113
|
+
{ pattern: /\(\?\(\?\)/, risk: 'critical' as const, message: 'Recursive patterns' },
|
|
114
|
+
{ pattern: /(\{(\d+,)?\d+\})/, risk: 'medium' as const, message: 'Quantified repetition' },
|
|
115
|
+
{ pattern: /(\.\*)|(\.+)|(\.\?)/, risk: 'medium' as const, message: 'Greedy quantifiers with dot' },
|
|
116
|
+
{ pattern: /(\[.*\*.*\])|(\[.*\+.*\])/, risk: 'medium' as const, message: 'Character classes with quantifiers' }
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
// Calculate complexity
|
|
120
|
+
complexity = this.calculateComplexity(pattern);
|
|
121
|
+
|
|
122
|
+
// Evaluate dangerous patterns
|
|
123
|
+
for (const dangerous of dangerousPatterns) {
|
|
124
|
+
if (dangerous.pattern.test(pattern)) {
|
|
125
|
+
warnings.push(dangerous.message);
|
|
126
|
+
if (this.getRiskLevel(dangerous.risk) > this.getRiskLevel(risk)) {
|
|
127
|
+
risk = dangerous.risk;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Recommendations based on the analysis
|
|
133
|
+
if (complexity > 10) {
|
|
134
|
+
recommendations.push('Consider simplifying the pattern');
|
|
135
|
+
risk = this.getRiskLevel(risk) > this.getRiskLevel('high') ? risk : 'high';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (pattern.includes('**') || pattern.includes('++')) {
|
|
139
|
+
recommendations.push('Avoid nested quantifiers to prevent catastrophic backtracking');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (pattern.length > 100) {
|
|
143
|
+
recommendations.push('Long patterns are harder to maintain and may impact performance');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const safe = risk !== 'critical' && warnings.length === 0;
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
safe,
|
|
150
|
+
complexity,
|
|
151
|
+
risk,
|
|
152
|
+
warnings,
|
|
153
|
+
recommendations
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Executes the regular expression in a Web Worker
|
|
159
|
+
*/
|
|
160
|
+
private async executeInWorker(
|
|
161
|
+
pattern: string,
|
|
162
|
+
text: string,
|
|
163
|
+
config: RegexSecurityConfig
|
|
164
|
+
): Promise<RegexTestResult> {
|
|
165
|
+
return new Promise((resolve) => {
|
|
166
|
+
const workerName = `regex-worker-${Date.now()}`;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
// Create temporary worker
|
|
170
|
+
const workerCode = this.generateWorkerCode();
|
|
171
|
+
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
172
|
+
const worker = new Worker(URL.createObjectURL(blob));
|
|
173
|
+
|
|
174
|
+
this.workers.set(workerName, worker);
|
|
175
|
+
|
|
176
|
+
const taskId = `regex_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
177
|
+
|
|
178
|
+
const task = {
|
|
179
|
+
id: taskId,
|
|
180
|
+
type: 'regex-test',
|
|
181
|
+
data: {
|
|
182
|
+
pattern,
|
|
183
|
+
text,
|
|
184
|
+
timeout: config.timeout || 5000
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// Timeout for execution
|
|
189
|
+
const timeoutId = setTimeout(() => {
|
|
190
|
+
worker.terminate();
|
|
191
|
+
this.workers.delete(workerName);
|
|
192
|
+
resolve({
|
|
193
|
+
match: false,
|
|
194
|
+
executionTime: 0,
|
|
195
|
+
timeout: true,
|
|
196
|
+
error: 'Execution timeout'
|
|
197
|
+
});
|
|
198
|
+
}, config.timeout || 5000);
|
|
199
|
+
|
|
200
|
+
worker.onmessage = (event) => {
|
|
201
|
+
clearTimeout(timeoutId);
|
|
202
|
+
worker.terminate();
|
|
203
|
+
this.workers.delete(workerName);
|
|
204
|
+
|
|
205
|
+
if (event.data.id === taskId) {
|
|
206
|
+
resolve(event.data.data as RegexTestResult);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
worker.onerror = (error) => {
|
|
211
|
+
clearTimeout(timeoutId);
|
|
212
|
+
worker.terminate();
|
|
213
|
+
this.workers.delete(workerName);
|
|
214
|
+
resolve({
|
|
215
|
+
match: false,
|
|
216
|
+
executionTime: 0,
|
|
217
|
+
timeout: false,
|
|
218
|
+
error: `Worker error: ${error.message || 'Unknown error'}`
|
|
219
|
+
});
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
worker.postMessage(task);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
resolve({
|
|
225
|
+
match: false,
|
|
226
|
+
executionTime: 0,
|
|
227
|
+
timeout: false,
|
|
228
|
+
error: `Failed to create worker: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Generates the Web Worker code
|
|
236
|
+
*/
|
|
237
|
+
private generateWorkerCode(): string {
|
|
238
|
+
return `
|
|
239
|
+
self.addEventListener('message', function(event) {
|
|
240
|
+
const task = event.data;
|
|
241
|
+
|
|
242
|
+
if (task.type === 'regex-test') {
|
|
243
|
+
const { pattern, text, timeout } = task.data;
|
|
244
|
+
const startTime = performance.now();
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const regex = new RegExp(pattern, 'g');
|
|
248
|
+
const matches = [];
|
|
249
|
+
let match;
|
|
250
|
+
|
|
251
|
+
while ((match = regex.exec(text)) !== null) {
|
|
252
|
+
matches.push([...match]);
|
|
253
|
+
|
|
254
|
+
// Prevention of infinite loops
|
|
255
|
+
if (matches.length > 1000) {
|
|
256
|
+
throw new Error('Too many matches - possible infinite loop');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const groups = {};
|
|
261
|
+
if (matches.length > 0) {
|
|
262
|
+
const firstMatch = matches[0];
|
|
263
|
+
for (let i = 1; i < firstMatch.length; i++) {
|
|
264
|
+
groups[\`group\${i}\`] = firstMatch[i];
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
self.postMessage({
|
|
269
|
+
id: task.id,
|
|
270
|
+
type: 'regex-result',
|
|
271
|
+
data: {
|
|
272
|
+
match: matches.length > 0,
|
|
273
|
+
matches,
|
|
274
|
+
groups,
|
|
275
|
+
executionTime: performance.now() - startTime,
|
|
276
|
+
timeout: false
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
} catch (error) {
|
|
280
|
+
self.postMessage({
|
|
281
|
+
id: task.id,
|
|
282
|
+
type: 'regex-result',
|
|
283
|
+
data: {
|
|
284
|
+
match: false,
|
|
285
|
+
executionTime: performance.now() - startTime,
|
|
286
|
+
timeout: false,
|
|
287
|
+
error: error.message || 'Execution error'
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Calculates the complexity of a pattern
|
|
298
|
+
*/
|
|
299
|
+
private calculateComplexity(pattern: string): number {
|
|
300
|
+
let complexity = 0;
|
|
301
|
+
|
|
302
|
+
// Nested quantifiers increase complexity
|
|
303
|
+
complexity += (pattern.match(/\*\*/g) || []).length * 5;
|
|
304
|
+
complexity += (pattern.match(/\+\+/g) || []).length * 5;
|
|
305
|
+
complexity += (pattern.match(/\?\?/g) || []).length * 3;
|
|
306
|
+
|
|
307
|
+
// Lookaheads/lookbehinds
|
|
308
|
+
complexity += (pattern.match(/\(\?\=/g) || []).length * 2;
|
|
309
|
+
complexity += (pattern.match(/\(\?\!/g) || []).length * 2;
|
|
310
|
+
complexity += (pattern.match(/\(\?\</g) || []).length * 3;
|
|
311
|
+
|
|
312
|
+
// Nested groups
|
|
313
|
+
const openParens = (pattern.match(/\(/g) || []).length;
|
|
314
|
+
complexity += openParens * 0.5;
|
|
315
|
+
|
|
316
|
+
// Pattern length
|
|
317
|
+
complexity += pattern.length * 0.01;
|
|
318
|
+
|
|
319
|
+
return Math.round(complexity * 100) / 100;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Gets numeric risk level
|
|
324
|
+
*/
|
|
325
|
+
private getRiskLevel(risk: 'low' | 'medium' | 'high' | 'critical'): number {
|
|
326
|
+
const levels = { 'low': 1, 'medium': 2, 'high': 3, 'critical': 4 };
|
|
327
|
+
return levels[risk] || 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Merges configuration with default values
|
|
332
|
+
*/
|
|
333
|
+
private mergeConfig(config: RegexSecurityConfig): Required<RegexSecurityConfig> {
|
|
334
|
+
return {
|
|
335
|
+
timeout: config.timeout || 5000,
|
|
336
|
+
maxComplexity: config.maxComplexity || 10,
|
|
337
|
+
allowBacktracking: config.allowBacktracking || false,
|
|
338
|
+
safeMode: config.safeMode || false
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Cleans up resources when the service is destroyed
|
|
344
|
+
*/
|
|
345
|
+
ngOnDestroy(): void {
|
|
346
|
+
this.workers.forEach(worker => {
|
|
347
|
+
worker.terminate();
|
|
348
|
+
});
|
|
349
|
+
this.workers.clear();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Builder pattern to construct safe regular expressions
|
|
355
|
+
*/
|
|
356
|
+
export class RegexSecurityBuilder {
|
|
357
|
+
private patternValue: string = '';
|
|
358
|
+
private optionsValue: RegexBuilderOptions = {};
|
|
359
|
+
private securityConfigValue: RegexSecurityConfig = {};
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Defines the base pattern
|
|
363
|
+
*/
|
|
364
|
+
pattern(pattern: string): RegexSecurityBuilder {
|
|
365
|
+
this.patternValue = pattern;
|
|
366
|
+
return this;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Appends text to the current pattern
|
|
371
|
+
*/
|
|
372
|
+
append(text: string): RegexSecurityBuilder {
|
|
373
|
+
this.patternValue += text;
|
|
374
|
+
return this;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Adds a capturing group
|
|
379
|
+
*/
|
|
380
|
+
group(content: string, name?: string): RegexSecurityBuilder {
|
|
381
|
+
if (name) {
|
|
382
|
+
this.patternValue += `(?<${name}>${content})`;
|
|
383
|
+
} else {
|
|
384
|
+
this.patternValue += `(${content})`;
|
|
385
|
+
}
|
|
386
|
+
return this;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Adds a non-capturing group
|
|
391
|
+
*/
|
|
392
|
+
nonCapturingGroup(content: string): RegexSecurityBuilder {
|
|
393
|
+
this.patternValue += `(?:${content})`;
|
|
394
|
+
return this;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Adds an alternative
|
|
399
|
+
*/
|
|
400
|
+
or(alternative: string): RegexSecurityBuilder {
|
|
401
|
+
this.patternValue += `|${alternative}`;
|
|
402
|
+
return this;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Adds a quantifier
|
|
407
|
+
*/
|
|
408
|
+
quantifier(quantifier: '*' | '+' | '?' | '{n}' | '{n,}' | '{n,m}'): RegexSecurityBuilder {
|
|
409
|
+
this.patternValue += quantifier;
|
|
410
|
+
return this;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Adds a character set
|
|
415
|
+
*/
|
|
416
|
+
characterSet(chars: string, negate = false): RegexSecurityBuilder {
|
|
417
|
+
this.patternValue += `[${negate ? '^' : ''}${chars}]`;
|
|
418
|
+
return this;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Adds a start of line anchor
|
|
423
|
+
*/
|
|
424
|
+
startOfLine(): RegexSecurityBuilder {
|
|
425
|
+
this.patternValue += '^';
|
|
426
|
+
return this;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Adds an end of line anchor
|
|
431
|
+
*/
|
|
432
|
+
endOfLine(): RegexSecurityBuilder {
|
|
433
|
+
this.patternValue += '$';
|
|
434
|
+
return this;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Configures regular expression options
|
|
439
|
+
*/
|
|
440
|
+
options(options: RegexBuilderOptions): RegexSecurityBuilder {
|
|
441
|
+
this.optionsValue = { ...this.optionsValue, ...options };
|
|
442
|
+
return this;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Configures security options
|
|
447
|
+
*/
|
|
448
|
+
security(config: RegexSecurityConfig): RegexSecurityBuilder {
|
|
449
|
+
this.securityConfigValue = { ...this.securityConfigValue, ...config };
|
|
450
|
+
return this;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Configures timeout
|
|
455
|
+
*/
|
|
456
|
+
timeout(ms: number): RegexSecurityBuilder {
|
|
457
|
+
this.securityConfigValue.timeout = ms;
|
|
458
|
+
return this;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Activates safe mode
|
|
463
|
+
*/
|
|
464
|
+
safeMode(): RegexSecurityBuilder {
|
|
465
|
+
this.securityConfigValue.safeMode = true;
|
|
466
|
+
return this;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Builds the final regular expression
|
|
471
|
+
*/
|
|
472
|
+
build(): { pattern: string; options: RegexBuilderOptions; security: RegexSecurityConfig } {
|
|
473
|
+
return {
|
|
474
|
+
pattern: this.patternValue,
|
|
475
|
+
options: this.optionsValue,
|
|
476
|
+
security: this.securityConfigValue
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Builds and executes the regular expression
|
|
482
|
+
*/
|
|
483
|
+
async execute(text: string, service: RegexSecurityService): Promise<RegexTestResult> {
|
|
484
|
+
const { pattern, security } = this.build();
|
|
485
|
+
return service.testRegex(pattern, text, security);
|
|
486
|
+
}
|
|
487
|
+
}
|