Skip to content

Commit b207e09

Browse files
wpe1arhyclaude
andauthored
fix(adoption-analyzer): remove polynomial ReDoS in nx-projects path regex (#4965)
* fix(adoption-analyzer): remove polynomial ReDoS in nx-projects path regex Replace the trailing-slash regex /\/+$/ with a linear-time helper. The regex ran on project.root (parsed from project.json/package.json, treated as untrusted library input) and had O(n^2) backtracking on inputs with many trailing '/' not ending in '/', flagged by CodeQL js/polynomial-redos (alert #120, high). Behaviour is unchanged; adds a targeted test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(adoption-analyzer): cover root "." → empty prefix branch Adds the test case suggested in review to fully cover the buildProjectIndex conditional (root "." maps to an empty prefix that resolves every path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8718148 commit b207e09

2 files changed

Lines changed: 27 additions & 1 deletion

File tree

libs/backpack-adoption-analyzer/src/analysis/nx-projects-test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,26 @@ describe("nx-projects", () => {
102102
});
103103
});
104104

105+
describe("buildProjectIndex", () => {
106+
it("strips trailing slashes from the project root when building the prefix", () => {
107+
const [entry] = buildProjectIndex([
108+
{ name: "web", root: "apps/web///", type: null },
109+
]);
110+
111+
expect(entry.prefix).toBe("apps/web/");
112+
expect(resolveProject("apps/web/Home.tsx", [entry])).toBe("web");
113+
});
114+
115+
it("maps a root of \".\" to an empty prefix that matches every path", () => {
116+
const [entry] = buildProjectIndex([
117+
{ name: "root", root: ".", type: null },
118+
]);
119+
120+
expect(entry.prefix).toBe("");
121+
expect(resolveProject("App.tsx", [entry])).toBe("root");
122+
});
123+
});
124+
105125
describe("resolveProject", () => {
106126
it("uses longest-prefix match and falls back to the unassigned bucket", () => {
107127
const index = buildProjectIndex([

libs/backpack-adoption-analyzer/src/analysis/nx-projects.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,18 @@ export function detectNxProjects(
8888
return { isNx: true, projects };
8989
}
9090

91+
function stripTrailingSlashes(value: string): string {
92+
let end = value.length;
93+
while (end > 0 && value[end - 1] === "/") end -= 1;
94+
return value.slice(0, end);
95+
}
96+
9197
export function buildProjectIndex(projects: NxProject[]): NxProjectIndexEntry[] {
9298
return projects
9399
.map((project) => ({
94100
name: project.name,
95101
root: project.root,
96-
prefix: project.root === "." ? "" : `${project.root.replace(/\/+$/, "")}/`,
102+
prefix: project.root === "." ? "" : `${stripTrailingSlashes(project.root)}/`,
97103
}))
98104
.sort((a, b) => b.prefix.length - a.prefix.length);
99105
}

0 commit comments

Comments
 (0)