From b178e4d27f04c24a2c7d751965ac7e2da00a1eb2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Apr 2026 19:07:42 +0000 Subject: [PATCH] fix: guard findFiles against out-of-boundary directory paths When path.relative(workspaceRoot, fileDir) produces an absolute path (e.g. on Windows when the document and workspace root are on different drives), path.resolve(parent, directory) returns the absolute directory unchanged. The subsequent upward walk then escapes the workspace boundary, potentially scanning up to the filesystem root. This commit adds an early-exit guard: if the resolved start path is not within parent, findFiles returns null immediately. It also uses the pre-resolved parent path in the loop's stop condition for consistency. A regression test covering the out-of-boundary case is included. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/utils.js | 17 ++++++++++++++--- test/unit.test.js | 13 +++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index 5627bec..6a28827 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -7,13 +7,24 @@ const fs = require("fs"); * whose name matches one of the entries in `name`. Stops at `parent`. * * @param {string} parent - Root of the workspace (walk stops here). - * @param {string} directory - Relative directory to start from. + * @param {string} directory - Relative directory to start from (must be within parent). * @param {string|string[]} name - File name(s) to look for. * @returns {string|null} Absolute path to the first matching file, or null. */ function findFiles(parent, directory, name) { const names = [].concat(name); - const chunks = path.resolve(parent, directory).split(path.sep); + const resolvedParent = path.resolve(parent); + const resolvedStart = path.resolve(parent, directory); + + // Guard: if the resolved start path is outside parent (e.g. cross-drive path + // on Windows produced by path.relative between different drives), there is + // nothing to find within the workspace boundary — return null immediately. + if (resolvedStart !== resolvedParent && + !resolvedStart.startsWith(resolvedParent + path.sep)) { + return null; + } + + const chunks = resolvedStart.split(path.sep); while (chunks.length) { let currentDir = chunks.join(path.sep); @@ -23,7 +34,7 @@ function findFiles(parent, directory, name) { return filePath; } } - if (parent === currentDir) { + if (resolvedParent === currentDir) { break; } chunks.pop(); diff --git a/test/unit.test.js b/test/unit.test.js index 0fb759e..8eb7b19 100644 --- a/test/unit.test.js +++ b/test/unit.test.js @@ -98,6 +98,19 @@ describe("findFiles", () => { assert.equal(result, null); }); + test("returns null when directory is outside parent (simulated cross-drive path)", () => { + // Simulate what happens when path.relative() returns an absolute path + // (e.g. Windows cross-drive: path.relative("C:\\ws", "D:\\other") -> "D:\\other"). + // findFiles should return null immediately without walking outside parent. + mkFile("outside-guard", "phpcs.xml"); + // Pass an absolute path as `directory` that is not within parent. + const outsideDir = path.join(tmpRoot, "outside-guard"); + const unrelatedParent = path.join(tmpRoot, "unrelated-parent"); + fs.mkdirSync(unrelatedParent, { recursive: true }); + const result = findFiles(unrelatedParent, outsideDir, "phpcs.xml"); + assert.equal(result, null); + }); + test("handles a single-segment directory (file at root)", () => { const expected = mkFile("single", "phpcs.xml"); const result = findFiles(path.join(tmpRoot, "single"), ".", "phpcs.xml");