Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/scoring/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,21 @@ export function labelMatchesPattern(label: string, pattern: string): boolean {
// safe and byte-identical to recompiling on every call.
const labelPatternRegExpCache = new Map<string, RegExp>();

// labelPatternToRegExp compiles each `*` to `.*`, so chained wildcards in a registry-supplied
// label_multipliers key can backtrack catastrophically on .test() — the same class of ReDoS
// globToRegExp guards against in change-guardrail.ts (#2445). fnmatch label patterns only
// use single-segment `*` wildcards (no `**` globstar), so each `*` is one wildcard group.
const MAX_LABEL_PATTERN_WILDCARD_GROUPS = 2;
const NEVER_MATCHES_LABEL_PATTERN = /^(?!)$/;

function countLabelPatternWildcardGroups(pattern: string): number {
let count = 0;
for (let i = 0; i < pattern.length; i += 1) {
if (pattern.charAt(i) === "*") count += 1;
}
return count;
}

// Upstream resolves label multipliers by matching each configured key as a Python `fnmatch` GLOB, not a
// literal string: `fnmatch(label.lower(), pattern.lower())` in
// gittensor/validator/oss_contributions/label_resolution.py, so a repo can configure `type:*`, `kind/*`, or
Expand All @@ -946,6 +961,10 @@ const labelPatternRegExpCache = new Map<string, RegExp>();
function labelPatternToRegExp(pattern: string): RegExp {
const cached = labelPatternRegExpCache.get(pattern);
if (cached !== undefined) return cached;
if (countLabelPatternWildcardGroups(pattern) > MAX_LABEL_PATTERN_WILDCARD_GROUPS) {
labelPatternRegExpCache.set(pattern, NEVER_MATCHES_LABEL_PATTERN);
return NEVER_MATCHES_LABEL_PATTERN;
}
let regex = "";
let i = 0;
while (i < pattern.length) {
Expand Down
11 changes: 11 additions & 0 deletions test/unit/scoring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1913,4 +1913,15 @@ describe("label pattern matcher memoization (#2106)", () => {
expect(labelMatchesPattern("bug", "bug")).toBe(true);
expect(labelMatchesPattern("bugfix", "bug")).toBe(false);
});

it("rejects over-complex wildcard label patterns instead of compiling a ReDoS-prone RegExp (#2456)", () => {
const overComplex = "*:*:*:*:*";
expect(labelMatchesPattern("type:bug-fix", overComplex)).toBe(false);
expect(labelMatchesPattern("anything", overComplex)).toBe(false);
// Repeated calls hit the cached never-match matcher.
expect(labelMatchesPattern("type:bug-fix", overComplex)).toBe(false);
// Two wildcards (at the cap) still compile and match normally.
expect(labelMatchesPattern("a:b", "*:*")).toBe(true);
expect(labelMatchesPattern("type:bug-fix", "type:*")).toBe(true);
});
});
Loading