Skip to content

Commit cecc92b

Browse files
committed
security: precompile banned source regex patterns
1 parent 37c5c2d commit cecc92b

2 files changed

Lines changed: 49 additions & 10 deletions

File tree

packages/security/src/index.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,10 @@ export class DefaultSecurityChecker implements SecurityChecker {
217217
private policy: RuntimeSecurityPolicy = getSecurityProfilePolicy(
218218
DEFAULT_SECURITY_PROFILE,
219219
);
220+
private sourceBannedPatterns: Array<{
221+
raw: string;
222+
regex: RegExp;
223+
}> = compileSourceBannedPatterns(this.policy.sourceBannedPatternStrings);
220224
private profile: RuntimeSecurityProfile = DEFAULT_SECURITY_PROFILE;
221225

222226
initialize(input?: SecurityInitializationInput): void {
@@ -243,6 +247,9 @@ export class DefaultSecurityChecker implements SecurityChecker {
243247
normalized.overrides?.sourceBannedPatternStrings ??
244248
basePolicy.sourceBannedPatternStrings,
245249
};
250+
this.sourceBannedPatterns = compileSourceBannedPatterns(
251+
this.policy.sourceBannedPatternStrings,
252+
);
246253
this.profile = profile;
247254
}
248255

@@ -598,16 +605,9 @@ export class DefaultSecurityChecker implements SecurityChecker {
598605
issues.push("Runtime source dynamic import() is disabled by policy");
599606
}
600607

601-
for (const patternText of this.policy.sourceBannedPatternStrings) {
602-
let pattern: RegExp;
603-
try {
604-
pattern = new RegExp(patternText, "i");
605-
} catch {
606-
continue;
607-
}
608-
609-
if (pattern.test(source.code)) {
610-
issues.push(`Runtime source contains blocked pattern: ${patternText}`);
608+
for (const pattern of this.sourceBannedPatterns) {
609+
if (pattern.regex.test(source.code)) {
610+
issues.push(`Runtime source contains blocked pattern: ${pattern.raw}`);
611611
}
612612
}
613613

@@ -818,6 +818,27 @@ function clonePolicy(policy: RuntimeSecurityPolicy): RuntimeSecurityPolicy {
818818
};
819819
}
820820

821+
function compileSourceBannedPatterns(patterns: string[]): Array<{
822+
raw: string;
823+
regex: RegExp;
824+
}> {
825+
const compiled: Array<{
826+
raw: string;
827+
regex: RegExp;
828+
}> = [];
829+
830+
for (const patternText of patterns) {
831+
try {
832+
compiled.push({
833+
raw: patternText,
834+
regex: new RegExp(patternText, "i"),
835+
});
836+
} catch {}
837+
}
838+
839+
return compiled;
840+
}
841+
821842
function walkNodes(
822843
node: RuntimeNode,
823844
visitor: (node: RuntimeNode) => void,

tests/security.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,24 @@ test("security checker supports profile initialization", async () => {
179179
assert.equal(checker.getProfile(), "strict");
180180
});
181181

182+
test("security checker precompiles source banned patterns on initialize", async () => {
183+
const checker = new DefaultSecurityChecker();
184+
checker.initialize({
185+
sourceBannedPatternStrings: ["\\beval\\s*\\(", "["],
186+
});
187+
188+
const compiled = (
189+
checker as unknown as {
190+
sourceBannedPatterns?: Array<{ raw?: string }>;
191+
}
192+
).sourceBannedPatterns;
193+
194+
assert.deepEqual(
195+
(compiled ?? []).map((entry) => entry.raw),
196+
["\\beval\\s*\\("],
197+
);
198+
});
199+
182200
test("security profiles list includes strict balanced relaxed", async () => {
183201
const profiles = listSecurityProfiles();
184202
assert.deepEqual(profiles.sort(), ["balanced", "relaxed", "strict"]);

0 commit comments

Comments
 (0)