@angular-helpers/security 21.4.3 → 21.5.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.
|
@@ -108,12 +108,12 @@ function hibpPassword(path, options) {
|
|
|
108
108
|
validateAsync(path, {
|
|
109
109
|
params: ({ value }) => {
|
|
110
110
|
const raw = value();
|
|
111
|
-
return raw && raw.length >= 8 ? raw :
|
|
111
|
+
return { password: raw && raw.length >= 8 ? raw : '' };
|
|
112
112
|
},
|
|
113
|
-
factory: (
|
|
113
|
+
factory: (paramsSignal) => {
|
|
114
114
|
const hibp = inject(HibpService);
|
|
115
115
|
return resource({
|
|
116
|
-
params: () =>
|
|
116
|
+
params: () => paramsSignal()?.password,
|
|
117
117
|
loader: async ({ params: password, abortSignal }) => {
|
|
118
118
|
if (!password)
|
|
119
119
|
return undefined;
|
|
@@ -1,55 +1,137 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import {
|
|
2
|
+
import { Injectable, inject, PLATFORM_ID, InjectionToken, DestroyRef, signal, computed, NgZone, Injector, makeEnvironmentProviders } from '@angular/core';
|
|
3
|
+
import { injectWorkerPool } from '@angular-helpers/core';
|
|
3
4
|
import { isPlatformBrowser, DOCUMENT } from '@angular/common';
|
|
4
5
|
import { Observable, Subject, fromEvent, merge } from 'rxjs';
|
|
5
6
|
import { throttleTime } from 'rxjs/operators';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
*
|
|
9
|
-
* using Web Workers for safe execution with timeout
|
|
9
|
+
* Builder pattern to construct safe regular expressions
|
|
10
10
|
*/
|
|
11
|
-
class
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
class RegexSecurityBuilder {
|
|
12
|
+
patternValue = '';
|
|
13
|
+
optionsValue = {};
|
|
14
|
+
securityConfigValue = {};
|
|
14
15
|
/**
|
|
15
|
-
*
|
|
16
|
+
* Defines the base pattern
|
|
16
17
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
pattern(pattern) {
|
|
19
|
+
this.patternValue = pattern;
|
|
20
|
+
return this;
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
|
-
*
|
|
23
|
+
* Appends text to the current pattern
|
|
22
24
|
*/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
timeout: false,
|
|
34
|
-
error: `Pattern rejected: ${securityCheck.warnings.join(', ')}`,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
// Execute in Web Worker with timeout
|
|
38
|
-
const result = await this.executeInWorker(pattern, text, finalConfig);
|
|
39
|
-
return {
|
|
40
|
-
...result,
|
|
41
|
-
executionTime: performance.now() - startTime,
|
|
42
|
-
};
|
|
25
|
+
append(text) {
|
|
26
|
+
this.patternValue += text;
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Adds a capturing group
|
|
31
|
+
*/
|
|
32
|
+
group(content, name) {
|
|
33
|
+
if (name) {
|
|
34
|
+
this.patternValue += `(?<${name}>${content})`;
|
|
43
35
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
match: false,
|
|
47
|
-
executionTime: performance.now() - startTime,
|
|
48
|
-
timeout: false,
|
|
49
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
50
|
-
};
|
|
36
|
+
else {
|
|
37
|
+
this.patternValue += `(${content})`;
|
|
51
38
|
}
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Adds a non-capturing group
|
|
43
|
+
*/
|
|
44
|
+
nonCapturingGroup(content) {
|
|
45
|
+
this.patternValue += `(?:${content})`;
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Adds an alternative
|
|
50
|
+
*/
|
|
51
|
+
or(alternative) {
|
|
52
|
+
this.patternValue += `|${alternative}`;
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Adds a quantifier
|
|
57
|
+
*/
|
|
58
|
+
quantifier(quantifier) {
|
|
59
|
+
this.patternValue += quantifier;
|
|
60
|
+
return this;
|
|
52
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Adds a character set
|
|
64
|
+
*/
|
|
65
|
+
characterSet(chars, negate = false) {
|
|
66
|
+
this.patternValue += `[${negate ? '^' : ''}${chars}]`;
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Adds a start of line anchor
|
|
71
|
+
*/
|
|
72
|
+
startOfLine() {
|
|
73
|
+
this.patternValue += '^';
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Adds an end of line anchor
|
|
78
|
+
*/
|
|
79
|
+
endOfLine() {
|
|
80
|
+
this.patternValue += '$';
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Configures regular expression options
|
|
85
|
+
*/
|
|
86
|
+
options(options) {
|
|
87
|
+
this.optionsValue = { ...this.optionsValue, ...options };
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Configures security options
|
|
92
|
+
*/
|
|
93
|
+
security(config) {
|
|
94
|
+
this.securityConfigValue = { ...this.securityConfigValue, ...config };
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Configures timeout
|
|
99
|
+
*/
|
|
100
|
+
timeout(ms) {
|
|
101
|
+
this.securityConfigValue.timeout = ms;
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Activates safe mode
|
|
106
|
+
*/
|
|
107
|
+
safeMode() {
|
|
108
|
+
this.securityConfigValue.safeMode = true;
|
|
109
|
+
return this;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Builds the final regular expression
|
|
113
|
+
*/
|
|
114
|
+
build() {
|
|
115
|
+
return {
|
|
116
|
+
pattern: this.patternValue,
|
|
117
|
+
options: this.optionsValue,
|
|
118
|
+
security: this.securityConfigValue,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Builds and executes the regular expression
|
|
123
|
+
*/
|
|
124
|
+
async execute(text, service) {
|
|
125
|
+
const { pattern, security } = this.build();
|
|
126
|
+
return service.testRegex(pattern, text, security);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Service responsible for statically analyzing regular expressions
|
|
132
|
+
* to detect ReDoS vulnerabilities and complexity issues.
|
|
133
|
+
*/
|
|
134
|
+
class RegexAnalyzerService {
|
|
53
135
|
/**
|
|
54
136
|
* Analyzes the security of a regular expression pattern
|
|
55
137
|
*/
|
|
@@ -114,131 +196,6 @@ class RegexSecurityService {
|
|
|
114
196
|
recommendations,
|
|
115
197
|
};
|
|
116
198
|
}
|
|
117
|
-
/**
|
|
118
|
-
* Executes the regular expression in a Web Worker
|
|
119
|
-
*/
|
|
120
|
-
async executeInWorker(pattern, text, config) {
|
|
121
|
-
return new Promise((resolve) => {
|
|
122
|
-
const workerName = `regex-worker-${Date.now()}`;
|
|
123
|
-
try {
|
|
124
|
-
// Create temporary worker
|
|
125
|
-
const workerCode = this.generateWorkerCode();
|
|
126
|
-
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
127
|
-
const worker = new Worker(URL.createObjectURL(blob));
|
|
128
|
-
this.workers.set(workerName, worker);
|
|
129
|
-
const taskId = `regex_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
130
|
-
const task = {
|
|
131
|
-
id: taskId,
|
|
132
|
-
type: 'regex-test',
|
|
133
|
-
data: {
|
|
134
|
-
pattern,
|
|
135
|
-
text,
|
|
136
|
-
timeout: config.timeout || 5000,
|
|
137
|
-
},
|
|
138
|
-
};
|
|
139
|
-
// Timeout for execution
|
|
140
|
-
const timeoutId = setTimeout(() => {
|
|
141
|
-
worker.terminate();
|
|
142
|
-
this.workers.delete(workerName);
|
|
143
|
-
resolve({
|
|
144
|
-
match: false,
|
|
145
|
-
executionTime: 0,
|
|
146
|
-
timeout: true,
|
|
147
|
-
error: 'Execution timeout',
|
|
148
|
-
});
|
|
149
|
-
}, config.timeout || 5000);
|
|
150
|
-
worker.onmessage = (event) => {
|
|
151
|
-
clearTimeout(timeoutId);
|
|
152
|
-
worker.terminate();
|
|
153
|
-
this.workers.delete(workerName);
|
|
154
|
-
if (event.data.id === taskId) {
|
|
155
|
-
resolve(event.data.data);
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
worker.onerror = (error) => {
|
|
159
|
-
clearTimeout(timeoutId);
|
|
160
|
-
worker.terminate();
|
|
161
|
-
this.workers.delete(workerName);
|
|
162
|
-
resolve({
|
|
163
|
-
match: false,
|
|
164
|
-
executionTime: 0,
|
|
165
|
-
timeout: false,
|
|
166
|
-
error: `Worker error: ${error.message || 'Unknown error'}`,
|
|
167
|
-
});
|
|
168
|
-
};
|
|
169
|
-
worker.postMessage(task);
|
|
170
|
-
}
|
|
171
|
-
catch (error) {
|
|
172
|
-
resolve({
|
|
173
|
-
match: false,
|
|
174
|
-
executionTime: 0,
|
|
175
|
-
timeout: false,
|
|
176
|
-
error: `Failed to create worker: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
/**
|
|
182
|
-
* Generates the Web Worker code
|
|
183
|
-
*/
|
|
184
|
-
generateWorkerCode() {
|
|
185
|
-
return `
|
|
186
|
-
self.addEventListener('message', function(event) {
|
|
187
|
-
const task = event.data;
|
|
188
|
-
|
|
189
|
-
if (task.type === 'regex-test') {
|
|
190
|
-
const { pattern, text, timeout } = task.data;
|
|
191
|
-
const startTime = performance.now();
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
const regex = new RegExp(pattern, 'g');
|
|
195
|
-
const matches = [];
|
|
196
|
-
let match;
|
|
197
|
-
|
|
198
|
-
while ((match = regex.exec(text)) !== null) {
|
|
199
|
-
matches.push([...match]);
|
|
200
|
-
|
|
201
|
-
// Prevention of infinite loops
|
|
202
|
-
if (matches.length > 1000) {
|
|
203
|
-
throw new Error('Too many matches - possible infinite loop');
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const groups = {};
|
|
208
|
-
if (matches.length > 0) {
|
|
209
|
-
const firstMatch = matches[0];
|
|
210
|
-
for (let i = 1; i < firstMatch.length; i++) {
|
|
211
|
-
groups[\`group\${i}\`] = firstMatch[i];
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
self.postMessage({
|
|
216
|
-
id: task.id,
|
|
217
|
-
type: 'regex-result',
|
|
218
|
-
data: {
|
|
219
|
-
match: matches.length > 0,
|
|
220
|
-
matches,
|
|
221
|
-
groups,
|
|
222
|
-
executionTime: performance.now() - startTime,
|
|
223
|
-
timeout: false
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
} catch (error) {
|
|
227
|
-
self.postMessage({
|
|
228
|
-
id: task.id,
|
|
229
|
-
type: 'regex-result',
|
|
230
|
-
data: {
|
|
231
|
-
match: false,
|
|
232
|
-
executionTime: performance.now() - startTime,
|
|
233
|
-
timeout: false,
|
|
234
|
-
error: error.message || 'Execution error'
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
});
|
|
240
|
-
`;
|
|
241
|
-
}
|
|
242
199
|
/**
|
|
243
200
|
* Calculates the complexity of a pattern
|
|
244
201
|
*/
|
|
@@ -266,153 +223,146 @@ class RegexSecurityService {
|
|
|
266
223
|
const levels = { low: 1, medium: 2, high: 3, critical: 4 };
|
|
267
224
|
return levels[risk] || 0;
|
|
268
225
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
*/
|
|
272
|
-
mergeConfig(config) {
|
|
273
|
-
return {
|
|
274
|
-
timeout: config.timeout || 5000,
|
|
275
|
-
maxComplexity: config.maxComplexity || 10,
|
|
276
|
-
allowBacktracking: config.allowBacktracking || false,
|
|
277
|
-
safeMode: config.safeMode || false,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
/**
|
|
281
|
-
* Cleans up resources when the service is destroyed
|
|
282
|
-
*/
|
|
283
|
-
ngOnDestroy() {
|
|
284
|
-
this.workers.forEach((worker) => {
|
|
285
|
-
worker.terminate();
|
|
286
|
-
});
|
|
287
|
-
this.workers.clear();
|
|
288
|
-
}
|
|
289
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RegexSecurityService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
290
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RegexSecurityService });
|
|
226
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RegexAnalyzerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
227
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RegexAnalyzerService });
|
|
291
228
|
}
|
|
292
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type:
|
|
229
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RegexAnalyzerService, decorators: [{
|
|
293
230
|
type: Injectable
|
|
294
231
|
}] });
|
|
232
|
+
|
|
295
233
|
/**
|
|
296
|
-
*
|
|
234
|
+
* Service responsible for managing Web Workers for safe regex execution.
|
|
235
|
+
* Avoids creating a new worker for every single execution.
|
|
297
236
|
*/
|
|
298
|
-
class
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
237
|
+
class RegexWorkerPoolService {
|
|
238
|
+
pool;
|
|
239
|
+
constructor() {
|
|
240
|
+
this.pool = injectWorkerPool(new URL('../workers/regex.worker', import.meta.url), {
|
|
241
|
+
defaultTimeout: 5000,
|
|
242
|
+
fallbackExecutor: async (type, data) => {
|
|
243
|
+
if (type !== 'regex-test')
|
|
244
|
+
throw new Error(`Unknown task type: ${type}`);
|
|
245
|
+
const { pattern, text } = data;
|
|
246
|
+
try {
|
|
247
|
+
const start = Date.now();
|
|
248
|
+
const regex = new RegExp(pattern);
|
|
249
|
+
const match = regex.test(text);
|
|
250
|
+
return {
|
|
251
|
+
match,
|
|
252
|
+
executionTime: Date.now() - start,
|
|
253
|
+
timeout: false,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
catch (e) {
|
|
257
|
+
return {
|
|
258
|
+
match: false,
|
|
259
|
+
executionTime: 0,
|
|
260
|
+
timeout: false,
|
|
261
|
+
error: e.message,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
});
|
|
315
266
|
}
|
|
316
267
|
/**
|
|
317
|
-
*
|
|
268
|
+
* Executes the regular expression in a Web Worker
|
|
318
269
|
*/
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
this.
|
|
270
|
+
async executeInWorker(pattern, text, config) {
|
|
271
|
+
try {
|
|
272
|
+
return await this.pool.execute('regex-test', {
|
|
273
|
+
pattern,
|
|
274
|
+
text,
|
|
275
|
+
timeout: config.timeout || 5000,
|
|
276
|
+
}, config.timeout || 5000);
|
|
322
277
|
}
|
|
323
|
-
|
|
324
|
-
|
|
278
|
+
catch (err) {
|
|
279
|
+
return {
|
|
280
|
+
match: false,
|
|
281
|
+
executionTime: 0,
|
|
282
|
+
timeout: err.message === 'Execution timeout',
|
|
283
|
+
error: err.message,
|
|
284
|
+
};
|
|
325
285
|
}
|
|
326
|
-
return this;
|
|
327
|
-
}
|
|
328
|
-
/**
|
|
329
|
-
* Adds a non-capturing group
|
|
330
|
-
*/
|
|
331
|
-
nonCapturingGroup(content) {
|
|
332
|
-
this.patternValue += `(?:${content})`;
|
|
333
|
-
return this;
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Adds an alternative
|
|
337
|
-
*/
|
|
338
|
-
or(alternative) {
|
|
339
|
-
this.patternValue += `|${alternative}`;
|
|
340
|
-
return this;
|
|
341
|
-
}
|
|
342
|
-
/**
|
|
343
|
-
* Adds a quantifier
|
|
344
|
-
*/
|
|
345
|
-
quantifier(quantifier) {
|
|
346
|
-
this.patternValue += quantifier;
|
|
347
|
-
return this;
|
|
348
|
-
}
|
|
349
|
-
/**
|
|
350
|
-
* Adds a character set
|
|
351
|
-
*/
|
|
352
|
-
characterSet(chars, negate = false) {
|
|
353
|
-
this.patternValue += `[${negate ? '^' : ''}${chars}]`;
|
|
354
|
-
return this;
|
|
355
|
-
}
|
|
356
|
-
/**
|
|
357
|
-
* Adds a start of line anchor
|
|
358
|
-
*/
|
|
359
|
-
startOfLine() {
|
|
360
|
-
this.patternValue += '^';
|
|
361
|
-
return this;
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* Adds an end of line anchor
|
|
365
|
-
*/
|
|
366
|
-
endOfLine() {
|
|
367
|
-
this.patternValue += '$';
|
|
368
|
-
return this;
|
|
369
286
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
*/
|
|
373
|
-
options(options) {
|
|
374
|
-
this.optionsValue = { ...this.optionsValue, ...options };
|
|
375
|
-
return this;
|
|
287
|
+
ngOnDestroy() {
|
|
288
|
+
this.pool.terminate();
|
|
376
289
|
}
|
|
290
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RegexWorkerPoolService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
291
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RegexWorkerPoolService });
|
|
292
|
+
}
|
|
293
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RegexWorkerPoolService, decorators: [{
|
|
294
|
+
type: Injectable
|
|
295
|
+
}], ctorParameters: () => [] });
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Security service for regular expressions that prevents ReDoS
|
|
299
|
+
* Facade pattern that delegates to Analyzer and Worker Pool.
|
|
300
|
+
*/
|
|
301
|
+
class RegexSecurityService {
|
|
302
|
+
analyzer = inject(RegexAnalyzerService);
|
|
303
|
+
workerPool = inject(RegexWorkerPoolService);
|
|
377
304
|
/**
|
|
378
|
-
*
|
|
305
|
+
* Builder pattern to construct safe regular expressions
|
|
379
306
|
*/
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
return this;
|
|
307
|
+
static builder() {
|
|
308
|
+
return new RegexSecurityBuilder();
|
|
383
309
|
}
|
|
384
310
|
/**
|
|
385
|
-
*
|
|
311
|
+
* Executes a regular expression safely with a timeout
|
|
386
312
|
*/
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
313
|
+
async testRegex(pattern, text, config = {}) {
|
|
314
|
+
const startTime = performance.now();
|
|
315
|
+
const finalConfig = this.mergeConfig(config);
|
|
316
|
+
try {
|
|
317
|
+
// First, analyze pattern security
|
|
318
|
+
const securityCheck = await this.analyzePatternSecurity(pattern);
|
|
319
|
+
if (!securityCheck.safe && !finalConfig.safeMode) {
|
|
320
|
+
return {
|
|
321
|
+
match: false,
|
|
322
|
+
executionTime: performance.now() - startTime,
|
|
323
|
+
timeout: false,
|
|
324
|
+
error: `Pattern rejected: ${securityCheck.warnings.join(', ')}`,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
// Execute in Web Worker with timeout
|
|
328
|
+
const result = await this.workerPool.executeInWorker(pattern, text, finalConfig);
|
|
329
|
+
return {
|
|
330
|
+
...result,
|
|
331
|
+
executionTime: performance.now() - startTime,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
return {
|
|
336
|
+
match: false,
|
|
337
|
+
executionTime: performance.now() - startTime,
|
|
338
|
+
timeout: false,
|
|
339
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
340
|
+
};
|
|
341
|
+
}
|
|
390
342
|
}
|
|
391
343
|
/**
|
|
392
|
-
*
|
|
344
|
+
* Analyzes the security of a regular expression pattern
|
|
393
345
|
*/
|
|
394
|
-
|
|
395
|
-
this.
|
|
396
|
-
return this;
|
|
346
|
+
async analyzePatternSecurity(pattern) {
|
|
347
|
+
return this.analyzer.analyzePatternSecurity(pattern);
|
|
397
348
|
}
|
|
398
349
|
/**
|
|
399
|
-
*
|
|
350
|
+
* Merges configuration with default values
|
|
400
351
|
*/
|
|
401
|
-
|
|
352
|
+
mergeConfig(config) {
|
|
402
353
|
return {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
354
|
+
timeout: config.timeout || 5000,
|
|
355
|
+
maxComplexity: config.maxComplexity || 10,
|
|
356
|
+
allowBacktracking: config.allowBacktracking || false,
|
|
357
|
+
safeMode: config.safeMode || false,
|
|
406
358
|
};
|
|
407
359
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
*/
|
|
411
|
-
async execute(text, service) {
|
|
412
|
-
const { pattern, security } = this.build();
|
|
413
|
-
return service.testRegex(pattern, text, security);
|
|
414
|
-
}
|
|
360
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RegexSecurityService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
361
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RegexSecurityService });
|
|
415
362
|
}
|
|
363
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RegexSecurityService, decorators: [{
|
|
364
|
+
type: Injectable
|
|
365
|
+
}] });
|
|
416
366
|
|
|
417
367
|
class WebCryptoService {
|
|
418
368
|
platformId = inject(PLATFORM_ID);
|
|
@@ -2021,7 +1971,7 @@ function provideSecurity(config = {}) {
|
|
|
2021
1971
|
const mergedConfig = { ...defaultSecurityConfig, ...config };
|
|
2022
1972
|
const providers = [];
|
|
2023
1973
|
if (mergedConfig.enableRegexSecurity)
|
|
2024
|
-
providers.push(RegexSecurityService);
|
|
1974
|
+
providers.push(RegexAnalyzerService, RegexWorkerPoolService, RegexSecurityService);
|
|
2025
1975
|
if (mergedConfig.enableWebCrypto)
|
|
2026
1976
|
providers.push(WebCryptoService);
|
|
2027
1977
|
if (mergedConfig.enableSecureStorage)
|
|
@@ -2049,7 +1999,11 @@ function provideSecurity(config = {}) {
|
|
|
2049
1999
|
return makeEnvironmentProviders(providers);
|
|
2050
2000
|
}
|
|
2051
2001
|
function provideRegexSecurity() {
|
|
2052
|
-
return makeEnvironmentProviders([
|
|
2002
|
+
return makeEnvironmentProviders([
|
|
2003
|
+
RegexAnalyzerService,
|
|
2004
|
+
RegexWorkerPoolService,
|
|
2005
|
+
RegexSecurityService,
|
|
2006
|
+
]);
|
|
2053
2007
|
}
|
|
2054
2008
|
function provideWebCrypto() {
|
|
2055
2009
|
return makeEnvironmentProviders([WebCryptoService]);
|
|
@@ -2106,4 +2060,4 @@ function provideSecureMessage() {
|
|
|
2106
2060
|
* Generated bundle index. Do not edit.
|
|
2107
2061
|
*/
|
|
2108
2062
|
|
|
2109
|
-
export { CSRF_CONFIG, ClipboardUnsupportedError, CsrfService, DEFAULT_ALLOWED_ATTRIBUTES, DEFAULT_ALLOWED_TAGS, HIBP_CONFIG, HibpService, InputSanitizerService, InvalidJwtError, JwtService, PasswordStrengthService, RATE_LIMITER_CONFIG, RateLimitExceededError, RateLimiterService, RegexSecurityBuilder, RegexSecurityService, SANITIZER_CONFIG, SECURE_STORAGE_CONFIG, SecureMessageService, SecureStorageService, SensitiveClipboardService, SessionIdleService, WebCryptoService, assessPasswordStrength, containsScriptInjection, containsSqlInjectionHints, defaultSecurityConfig, isHtmlSafe, isUrlSafe, provideCsrf, provideHibp, provideInputSanitizer, provideJwt, providePasswordStrength, provideRateLimiter, provideRegexSecurity, provideSecureMessage, provideSecureStorage, provideSecurity, provideSensitiveClipboard, provideSessionIdle, provideWebCrypto, sanitizeHtmlString, sanitizeUrlString, withCsrfHeader };
|
|
2063
|
+
export { CSRF_CONFIG, ClipboardUnsupportedError, CsrfService, DEFAULT_ALLOWED_ATTRIBUTES, DEFAULT_ALLOWED_TAGS, HIBP_CONFIG, HibpService, InputSanitizerService, InvalidJwtError, JwtService, PasswordStrengthService, RATE_LIMITER_CONFIG, RateLimitExceededError, RateLimiterService, RegexAnalyzerService, RegexSecurityBuilder, RegexSecurityService, RegexWorkerPoolService, SANITIZER_CONFIG, SECURE_STORAGE_CONFIG, SecureMessageService, SecureStorageService, SensitiveClipboardService, SessionIdleService, WebCryptoService, assessPasswordStrength, containsScriptInjection, containsSqlInjectionHints, defaultSecurityConfig, isHtmlSafe, isUrlSafe, provideCsrf, provideHibp, provideInputSanitizer, provideJwt, providePasswordStrength, provideRateLimiter, provideRegexSecurity, provideSecureMessage, provideSecureStorage, provideSecurity, provideSensitiveClipboard, provideSessionIdle, provideWebCrypto, sanitizeHtmlString, sanitizeUrlString, withCsrfHeader };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@angular-helpers/security",
|
|
3
|
-
"version": "21.
|
|
3
|
+
"version": "21.5.0",
|
|
4
4
|
"description": "Angular security helpers for preventing ReDoS and other security vulnerabilities",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"angular",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"@angular/common": "^21.0.0",
|
|
41
41
|
"@angular/core": "^21.0.0",
|
|
42
42
|
"@angular/forms": "^21.0.0",
|
|
43
|
+
"@angular-helpers/core": "workspace:*",
|
|
43
44
|
"rxjs": "^7.0.0 || ^8.0.0"
|
|
44
45
|
},
|
|
45
46
|
"peerDependenciesMeta": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, Signal, EnvironmentProviders } from '@angular/core';
|
|
2
|
+
import { InjectionToken, Signal, EnvironmentProviders, OnDestroy } from '@angular/core';
|
|
3
3
|
import { Observable } from 'rxjs';
|
|
4
4
|
import { HttpInterceptorFn } from '@angular/common/http';
|
|
5
5
|
|
|
@@ -34,52 +34,7 @@ interface RegexBuilderOptions {
|
|
|
34
34
|
unicode?: boolean;
|
|
35
35
|
sticky?: boolean;
|
|
36
36
|
}
|
|
37
|
-
|
|
38
|
-
* Security service for regular expressions that prevents ReDoS
|
|
39
|
-
* using Web Workers for safe execution with timeout
|
|
40
|
-
*/
|
|
41
|
-
declare class RegexSecurityService {
|
|
42
|
-
private destroyRef;
|
|
43
|
-
private workers;
|
|
44
|
-
/**
|
|
45
|
-
* Builder pattern to construct safe regular expressions
|
|
46
|
-
*/
|
|
47
|
-
static builder(): RegexSecurityBuilder;
|
|
48
|
-
/**
|
|
49
|
-
* Executes a regular expression safely with a timeout
|
|
50
|
-
*/
|
|
51
|
-
testRegex(pattern: string, text: string, config?: RegexSecurityConfig): Promise<RegexTestResult>;
|
|
52
|
-
/**
|
|
53
|
-
* Analyzes the security of a regular expression pattern
|
|
54
|
-
*/
|
|
55
|
-
analyzePatternSecurity(pattern: string): Promise<RegexSecurityResult>;
|
|
56
|
-
/**
|
|
57
|
-
* Executes the regular expression in a Web Worker
|
|
58
|
-
*/
|
|
59
|
-
private executeInWorker;
|
|
60
|
-
/**
|
|
61
|
-
* Generates the Web Worker code
|
|
62
|
-
*/
|
|
63
|
-
private generateWorkerCode;
|
|
64
|
-
/**
|
|
65
|
-
* Calculates the complexity of a pattern
|
|
66
|
-
*/
|
|
67
|
-
private calculateComplexity;
|
|
68
|
-
/**
|
|
69
|
-
* Gets numeric risk level
|
|
70
|
-
*/
|
|
71
|
-
private getRiskLevel;
|
|
72
|
-
/**
|
|
73
|
-
* Merges configuration with default values
|
|
74
|
-
*/
|
|
75
|
-
private mergeConfig;
|
|
76
|
-
/**
|
|
77
|
-
* Cleans up resources when the service is destroyed
|
|
78
|
-
*/
|
|
79
|
-
ngOnDestroy(): void;
|
|
80
|
-
static ɵfac: i0.ɵɵFactoryDeclaration<RegexSecurityService, never>;
|
|
81
|
-
static ɵprov: i0.ɵɵInjectableDeclaration<RegexSecurityService>;
|
|
82
|
-
}
|
|
37
|
+
|
|
83
38
|
/**
|
|
84
39
|
* Builder pattern to construct safe regular expressions
|
|
85
40
|
*/
|
|
@@ -153,6 +108,33 @@ declare class RegexSecurityBuilder {
|
|
|
153
108
|
execute(text: string, service: RegexSecurityService): Promise<RegexTestResult>;
|
|
154
109
|
}
|
|
155
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Security service for regular expressions that prevents ReDoS
|
|
113
|
+
* Facade pattern that delegates to Analyzer and Worker Pool.
|
|
114
|
+
*/
|
|
115
|
+
declare class RegexSecurityService {
|
|
116
|
+
private analyzer;
|
|
117
|
+
private workerPool;
|
|
118
|
+
/**
|
|
119
|
+
* Builder pattern to construct safe regular expressions
|
|
120
|
+
*/
|
|
121
|
+
static builder(): RegexSecurityBuilder;
|
|
122
|
+
/**
|
|
123
|
+
* Executes a regular expression safely with a timeout
|
|
124
|
+
*/
|
|
125
|
+
testRegex(pattern: string, text: string, config?: RegexSecurityConfig): Promise<RegexTestResult>;
|
|
126
|
+
/**
|
|
127
|
+
* Analyzes the security of a regular expression pattern
|
|
128
|
+
*/
|
|
129
|
+
analyzePatternSecurity(pattern: string): Promise<RegexSecurityResult>;
|
|
130
|
+
/**
|
|
131
|
+
* Merges configuration with default values
|
|
132
|
+
*/
|
|
133
|
+
private mergeConfig;
|
|
134
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<RegexSecurityService, never>;
|
|
135
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<RegexSecurityService>;
|
|
136
|
+
}
|
|
137
|
+
|
|
156
138
|
type HashAlgorithm = 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512';
|
|
157
139
|
type HmacAlgorithm = 'HMAC-SHA-256' | 'HMAC-SHA-384' | 'HMAC-SHA-512';
|
|
158
140
|
type AesKeyLength = 128 | 192 | 256;
|
|
@@ -857,5 +839,42 @@ declare function provideCsrf(config?: CsrfConfig): EnvironmentProviders;
|
|
|
857
839
|
declare function provideSessionIdle(): EnvironmentProviders;
|
|
858
840
|
declare function provideSecureMessage(): EnvironmentProviders;
|
|
859
841
|
|
|
860
|
-
|
|
842
|
+
/**
|
|
843
|
+
* Service responsible for statically analyzing regular expressions
|
|
844
|
+
* to detect ReDoS vulnerabilities and complexity issues.
|
|
845
|
+
*/
|
|
846
|
+
declare class RegexAnalyzerService {
|
|
847
|
+
/**
|
|
848
|
+
* Analyzes the security of a regular expression pattern
|
|
849
|
+
*/
|
|
850
|
+
analyzePatternSecurity(pattern: string): Promise<RegexSecurityResult>;
|
|
851
|
+
/**
|
|
852
|
+
* Calculates the complexity of a pattern
|
|
853
|
+
*/
|
|
854
|
+
private calculateComplexity;
|
|
855
|
+
/**
|
|
856
|
+
* Gets numeric risk level
|
|
857
|
+
*/
|
|
858
|
+
private getRiskLevel;
|
|
859
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<RegexAnalyzerService, never>;
|
|
860
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<RegexAnalyzerService>;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Service responsible for managing Web Workers for safe regex execution.
|
|
865
|
+
* Avoids creating a new worker for every single execution.
|
|
866
|
+
*/
|
|
867
|
+
declare class RegexWorkerPoolService implements OnDestroy {
|
|
868
|
+
private pool;
|
|
869
|
+
constructor();
|
|
870
|
+
/**
|
|
871
|
+
* Executes the regular expression in a Web Worker
|
|
872
|
+
*/
|
|
873
|
+
executeInWorker(pattern: string, text: string, config: RegexSecurityConfig): Promise<RegexTestResult>;
|
|
874
|
+
ngOnDestroy(): void;
|
|
875
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<RegexWorkerPoolService, never>;
|
|
876
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<RegexWorkerPoolService>;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
export { CSRF_CONFIG, ClipboardUnsupportedError, CsrfService, DEFAULT_ALLOWED_ATTRIBUTES, DEFAULT_ALLOWED_TAGS, HIBP_CONFIG, HibpService, InputSanitizerService, InvalidJwtError, JwtService, PasswordStrengthService, RATE_LIMITER_CONFIG, RateLimitExceededError, RateLimiterService, RegexAnalyzerService, RegexSecurityBuilder, RegexSecurityService, RegexWorkerPoolService, SANITIZER_CONFIG, SECURE_STORAGE_CONFIG, SecureMessageService, SecureStorageService, SensitiveClipboardService, SessionIdleService, WebCryptoService, assessPasswordStrength, containsScriptInjection, containsSqlInjectionHints, defaultSecurityConfig, isHtmlSafe, isUrlSafe, provideCsrf, provideHibp, provideInputSanitizer, provideJwt, providePasswordStrength, provideRateLimiter, provideRegexSecurity, provideSecureMessage, provideSecureStorage, provideSecurity, provideSensitiveClipboard, provideSessionIdle, provideWebCrypto, sanitizeHtmlString, sanitizeUrlString, withCsrfHeader };
|
|
861
880
|
export type { AesEncryptResult, AesKeyLength, CopyStatus, CsrfConfig, CsrfHeaderOptions, CsrfStorageTarget, HashAlgorithm, HibpConfig, HibpResult, HmacAlgorithm, HtmlSanitizerOptions, HttpMethod, JwtStandardClaims, PasswordAssessment, PasswordLabel, PasswordScore, PasswordStrengthResult, RateLimitPolicy, RateLimiterConfig, RegexBuilderOptions, RegexSecurityConfig, RegexSecurityResult, RegexTestResult, SanitizerConfig, SecureMessage, SecureMessageConfig, SecureStorageConfig, SecurityConfig, SensitiveCopyOptions, SessionIdleConfig, StorageTarget };
|