From b4e58308d597de1ff2c5ea57fff4ae051f558c3a Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Mon, 16 Mar 2026 17:44:05 +0000 Subject: [PATCH] wrangler: reject cross-drive module paths --- .changeset/warm-mirrors-allow.md | 7 +++ .../src/__tests__/pages/routes-module.test.ts | 53 +++++++++++++++++++ .../wrangler/src/pages/functions/routes.ts | 28 +++++++--- 3 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 .changeset/warm-mirrors-allow.md create mode 100644 packages/wrangler/src/__tests__/pages/routes-module.test.ts diff --git a/.changeset/warm-mirrors-allow.md b/.changeset/warm-mirrors-allow.md new file mode 100644 index 0000000000..f77c28c4a2 --- /dev/null +++ b/.changeset/warm-mirrors-allow.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Reject cross-drive module paths in Pages Functions routing + +On Windows, module paths using a different drive letter could be parsed in a way that bypassed the project-root check. These paths are now parsed correctly and rejected when they resolve outside the project. diff --git a/packages/wrangler/src/__tests__/pages/routes-module.test.ts b/packages/wrangler/src/__tests__/pages/routes-module.test.ts new file mode 100644 index 0000000000..a67b1d127a --- /dev/null +++ b/packages/wrangler/src/__tests__/pages/routes-module.test.ts @@ -0,0 +1,53 @@ +import { UserError } from "@cloudflare/workers-utils"; +import { describe, it } from "vitest"; +import { writeRoutesModule } from "../../pages/functions/routes"; +import { toUrlPath } from "../../paths"; +import { runInTempDir } from "../helpers/run-in-tmp"; + +describe("routes module", () => { + runInTempDir(); + + it("accepts module paths when srcDir is a relative path", async ({ + expect, + }) => { + await expect( + writeRoutesModule({ + config: { + routes: [ + { + routePath: toUrlPath("/"), + mountPath: toUrlPath("/"), + module: "hello.js:onRequest", + }, + ], + }, + srcDir: "functions", + outfile: "_routes.js", + }) + ).resolves.toBeDefined(); + }); + + it.skipIf(process.platform !== "win32")( + "rejects module paths on a different drive", + async ({ expect }) => { + const modulePath = String.raw`D:\evil.js`; + const config = { + routes: [ + { + routePath: toUrlPath("/"), + mountPath: toUrlPath("/"), + module: modulePath, + }, + ], + }; + + await expect( + writeRoutesModule({ + config, + srcDir: String.raw`C:\project`, + outfile: "_routes.js", + }) + ).rejects.toThrow(new UserError(`Invalid module path "${modulePath}"`)); + } + ); +}); diff --git a/packages/wrangler/src/pages/functions/routes.ts b/packages/wrangler/src/pages/functions/routes.ts index 7730fb0b03..3fd7bfd55f 100755 --- a/packages/wrangler/src/pages/functions/routes.ts +++ b/packages/wrangler/src/pages/functions/routes.ts @@ -63,6 +63,7 @@ export async function writeRoutesModule({ } function parseConfig(config: Config, baseDir: string) { + baseDir = path.resolve(baseDir); const routes: RoutesCollection = []; const importMap: ImportMap = new Map(); const identifierCount = new Map(); // to keep track of identifier collisions @@ -77,14 +78,22 @@ function parseConfig(config: Config, baseDir: string) { } return paths.map((modulePath) => { - const [filepath, name = "default"] = modulePath.split(":"); - let { identifier } = importMap.get(modulePath) ?? {}; + const resolvedPath = path.resolve(baseDir, modulePath); + const moduleRoot = path.parse(resolvedPath).root; - const resolvedPath = path.resolve(baseDir, filepath); + // Strip the drive letter (if any) to avoid confusing the drive colon with the export name separator + const strippedPath = resolvedPath.slice(moduleRoot.length - 1); + const [filepath, name = "default"] = strippedPath.split(":"); + + const fullFilepath = path.resolve(moduleRoot, filepath); + const relativePath = path.relative(baseDir, fullFilepath); // ensure the filepath isn't attempting to resolve to anything outside of the project - if (path.relative(baseDir, resolvedPath).startsWith("..")) { - throw new UserError(`Invalid module path "${filepath}"`); + if ( + moduleRoot !== path.parse(baseDir).root || + relativePath.startsWith("..") + ) { + throw new UserError(`Invalid module path "${fullFilepath}"`); } // ensure the module name (if provided) is a valid identifier to guard against injection attacks @@ -92,8 +101,9 @@ function parseConfig(config: Config, baseDir: string) { throw new UserError(`Invalid module identifier "${name}"`); } + let { identifier } = importMap.get(resolvedPath) ?? {}; if (!identifier) { - identifier = normalizeIdentifier(`__${filepath}_${name}`); + identifier = normalizeIdentifier(`__${relativePath}_${name}`); let count = identifierCount.get(identifier) ?? 0; identifierCount.set(identifier, ++count); @@ -102,7 +112,11 @@ function parseConfig(config: Config, baseDir: string) { identifier += `_${count}`; } - importMap.set(modulePath, { filepath: resolvedPath, name, identifier }); + importMap.set(resolvedPath, { + filepath: fullFilepath, + name, + identifier, + }); } return identifier;