@angular-helpers/security 21.4.2 → 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;
|
|
52
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Adds a quantifier
|
|
57
|
+
*/
|
|
58
|
+
quantifier(quantifier) {
|
|
59
|
+
this.patternValue += quantifier;
|
|
60
|
+
return this;
|
|
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
286
|
}
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
}
|
|
370
|
-
/**
|
|
371
|
-
* Configures regular expression options
|
|
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);
|
|
@@ -922,6 +872,7 @@ const DEFAULT_ALLOWED_TAGS = [
|
|
|
922
872
|
const DEFAULT_ALLOWED_ATTRIBUTES = {
|
|
923
873
|
a: ['href'],
|
|
924
874
|
};
|
|
875
|
+
const URL_ATTRIBUTES = new Set(['href', 'src', 'action', 'formaction', 'data', 'poster']);
|
|
925
876
|
/**
|
|
926
877
|
* Sanitizes an HTML string by allowlist. Browser-only: requires DOMParser.
|
|
927
878
|
*
|
|
@@ -980,7 +931,7 @@ function sanitizeElementAttributes(element, tagName, allowedAttributes) {
|
|
|
980
931
|
attrsToRemove.push(attr.name);
|
|
981
932
|
continue;
|
|
982
933
|
}
|
|
983
|
-
if (attr.name
|
|
934
|
+
if (URL_ATTRIBUTES.has(attr.name) && sanitizeUrlString(attr.value) === null) {
|
|
984
935
|
attrsToRemove.push(attr.name);
|
|
985
936
|
}
|
|
986
937
|
}
|
|
@@ -1512,13 +1463,23 @@ class RateLimiterService {
|
|
|
1512
1463
|
this.configure(key, policy);
|
|
1513
1464
|
}
|
|
1514
1465
|
}
|
|
1515
|
-
this.destroyRef.onDestroy(() =>
|
|
1466
|
+
this.destroyRef.onDestroy(() => {
|
|
1467
|
+
for (const bucket of this.buckets.values()) {
|
|
1468
|
+
if (bucket.timerId)
|
|
1469
|
+
clearTimeout(bucket.timerId);
|
|
1470
|
+
}
|
|
1471
|
+
this.buckets.clear();
|
|
1472
|
+
});
|
|
1516
1473
|
}
|
|
1517
1474
|
/**
|
|
1518
1475
|
* Registers or updates the policy for `key`. Re-configuring an existing key resets its state.
|
|
1519
1476
|
*/
|
|
1520
1477
|
configure(key, policy) {
|
|
1521
1478
|
validatePolicy(policy);
|
|
1479
|
+
const existing = this.buckets.get(key);
|
|
1480
|
+
if (existing && existing.timerId) {
|
|
1481
|
+
clearTimeout(existing.timerId);
|
|
1482
|
+
}
|
|
1522
1483
|
const now = Date.now();
|
|
1523
1484
|
const initialRemaining = policy.type === 'token-bucket' ? policy.capacity : policy.max;
|
|
1524
1485
|
this.buckets.set(key, {
|
|
@@ -1553,6 +1514,7 @@ class RateLimiterService {
|
|
|
1553
1514
|
}
|
|
1554
1515
|
bucket.tokens -= tokens;
|
|
1555
1516
|
bucket.remaining.set(Math.floor(bucket.tokens));
|
|
1517
|
+
this.scheduleAutoRefill(key, bucket);
|
|
1556
1518
|
return;
|
|
1557
1519
|
}
|
|
1558
1520
|
// sliding-window
|
|
@@ -1566,6 +1528,7 @@ class RateLimiterService {
|
|
|
1566
1528
|
for (let i = 0; i < tokens; i++)
|
|
1567
1529
|
bucket.timestamps.push(now);
|
|
1568
1530
|
bucket.remaining.set(bucket.policy.max - bucket.timestamps.length);
|
|
1531
|
+
this.scheduleAutoRefill(key, bucket);
|
|
1569
1532
|
}
|
|
1570
1533
|
/**
|
|
1571
1534
|
* Reactive signal indicating whether a single unit can be consumed from `key` right now.
|
|
@@ -1594,6 +1557,10 @@ class RateLimiterService {
|
|
|
1594
1557
|
const bucket = this.buckets.get(key);
|
|
1595
1558
|
if (!bucket)
|
|
1596
1559
|
return;
|
|
1560
|
+
if (bucket.timerId) {
|
|
1561
|
+
clearTimeout(bucket.timerId);
|
|
1562
|
+
bucket.timerId = undefined;
|
|
1563
|
+
}
|
|
1597
1564
|
bucket.timestamps = [];
|
|
1598
1565
|
if (bucket.policy.type === 'token-bucket') {
|
|
1599
1566
|
bucket.tokens = bucket.policy.capacity;
|
|
@@ -1615,6 +1582,41 @@ class RateLimiterService {
|
|
|
1615
1582
|
bucket.lastRefillAt = now;
|
|
1616
1583
|
bucket.remaining.set(Math.floor(bucket.tokens));
|
|
1617
1584
|
}
|
|
1585
|
+
scheduleAutoRefill(key, bucket) {
|
|
1586
|
+
if (bucket.timerId) {
|
|
1587
|
+
clearTimeout(bucket.timerId);
|
|
1588
|
+
bucket.timerId = undefined;
|
|
1589
|
+
}
|
|
1590
|
+
const now = Date.now();
|
|
1591
|
+
if (bucket.policy.type === 'token-bucket') {
|
|
1592
|
+
if (bucket.tokens >= bucket.policy.capacity) {
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
const currentFloor = Math.floor(bucket.tokens);
|
|
1596
|
+
const nextInteger = currentFloor + 1;
|
|
1597
|
+
const deficit = nextInteger - bucket.tokens;
|
|
1598
|
+
const timeToNextTokenMs = Math.max(10, Math.ceil((deficit / bucket.policy.refillPerSecond) * 1000));
|
|
1599
|
+
bucket.timerId = setTimeout(() => {
|
|
1600
|
+
const tNow = Date.now();
|
|
1601
|
+
this.refillTokenBucket(bucket, tNow);
|
|
1602
|
+
this.scheduleAutoRefill(key, bucket);
|
|
1603
|
+
}, timeToNextTokenMs);
|
|
1604
|
+
}
|
|
1605
|
+
else {
|
|
1606
|
+
// sliding-window
|
|
1607
|
+
const windowStart = now - bucket.policy.windowMs;
|
|
1608
|
+
bucket.timestamps = bucket.timestamps.filter((t) => t > windowStart);
|
|
1609
|
+
bucket.remaining.set(bucket.policy.max - bucket.timestamps.length);
|
|
1610
|
+
if (bucket.timestamps.length === 0) {
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
const oldest = bucket.timestamps[0];
|
|
1614
|
+
const timeToExpiryMs = Math.max(10, oldest + bucket.policy.windowMs - now);
|
|
1615
|
+
bucket.timerId = setTimeout(() => {
|
|
1616
|
+
this.scheduleAutoRefill(key, bucket);
|
|
1617
|
+
}, timeToExpiryMs);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1618
1620
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RateLimiterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1619
1621
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RateLimiterService });
|
|
1620
1622
|
}
|
|
@@ -1969,7 +1971,7 @@ function provideSecurity(config = {}) {
|
|
|
1969
1971
|
const mergedConfig = { ...defaultSecurityConfig, ...config };
|
|
1970
1972
|
const providers = [];
|
|
1971
1973
|
if (mergedConfig.enableRegexSecurity)
|
|
1972
|
-
providers.push(RegexSecurityService);
|
|
1974
|
+
providers.push(RegexAnalyzerService, RegexWorkerPoolService, RegexSecurityService);
|
|
1973
1975
|
if (mergedConfig.enableWebCrypto)
|
|
1974
1976
|
providers.push(WebCryptoService);
|
|
1975
1977
|
if (mergedConfig.enableSecureStorage)
|
|
@@ -1997,7 +1999,11 @@ function provideSecurity(config = {}) {
|
|
|
1997
1999
|
return makeEnvironmentProviders(providers);
|
|
1998
2000
|
}
|
|
1999
2001
|
function provideRegexSecurity() {
|
|
2000
|
-
return makeEnvironmentProviders([
|
|
2002
|
+
return makeEnvironmentProviders([
|
|
2003
|
+
RegexAnalyzerService,
|
|
2004
|
+
RegexWorkerPoolService,
|
|
2005
|
+
RegexSecurityService,
|
|
2006
|
+
]);
|
|
2001
2007
|
}
|
|
2002
2008
|
function provideWebCrypto() {
|
|
2003
2009
|
return makeEnvironmentProviders([WebCryptoService]);
|
|
@@ -2054,4 +2060,4 @@ function provideSecureMessage() {
|
|
|
2054
2060
|
* Generated bundle index. Do not edit.
|
|
2055
2061
|
*/
|
|
2056
2062
|
|
|
2057
|
-
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;
|
|
@@ -675,6 +657,7 @@ declare class RateLimiterService {
|
|
|
675
657
|
*/
|
|
676
658
|
reset(key: string): void;
|
|
677
659
|
private refillTokenBucket;
|
|
660
|
+
private scheduleAutoRefill;
|
|
678
661
|
static ɵfac: i0.ɵɵFactoryDeclaration<RateLimiterService, never>;
|
|
679
662
|
static ɵprov: i0.ɵɵInjectableDeclaration<RateLimiterService>;
|
|
680
663
|
}
|
|
@@ -856,5 +839,42 @@ declare function provideCsrf(config?: CsrfConfig): EnvironmentProviders;
|
|
|
856
839
|
declare function provideSessionIdle(): EnvironmentProviders;
|
|
857
840
|
declare function provideSecureMessage(): EnvironmentProviders;
|
|
858
841
|
|
|
859
|
-
|
|
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 };
|
|
860
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 };
|