From 6714fb1d02feac15ea395655334c15b87fdcb948 Mon Sep 17 00:00:00 2001 From: Rohit Ghumare <48523873+rohitg00@users.noreply.github.com> Date: Fri, 23 Oct 2026 00:04:28 +0530 Subject: [PATCH] fix(javascript): preserve ESM path intent and artifact confinement (#2700) * fix(javascript): preserve legacy ESM directory intent * fix(javascript): normalize legacy ESM URL directory requests * fix(javascript): retain invalid package URL evidence * test(apple): bound cold archive fixture compilation * test(release): scope preflight schema to selected jobs * fix(javascript): preserve package URL admission and target facts * fix(javascript): validate package target structure before URL lookup * fix(javascript): preserve surrogate identity in structural package URLs * fix(javascript): isolate structural package URL identities * fix(javascript): resolve paths after opaque ancestors normalize away * fix(javascript): confine legacy URLs and retain target provenance --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> --- .../JavaScriptArtifactPathResolution.ts | 19 ++++-- .../packageSpecifierSuffixCli.test.ts | 60 +++++++++++++++++++ .../applications/packageUrlUnicodeCli.test.ts | 14 +++++ 3 files changed, 89 insertions(+), 4 deletions(-) diff --git a/src/application/javascript/JavaScriptArtifactPathResolution.ts b/src/application/javascript/JavaScriptArtifactPathResolution.ts index 5f02256700..1642369779 100644 --- a/src/application/javascript/JavaScriptArtifactPathResolution.ts +++ b/src/application/javascript/JavaScriptArtifactPathResolution.ts @@ -1096,10 +1096,19 @@ const resolveLegacyEsmSubpath = ( directory: string, subpath: string, ): CandidateResolution => { - const declarationRejection = rejectDeclaration( - stripQueryAndFragment(htmlUrlText(subpath)), - ); + const subpathPath = stripQueryAndFragment(htmlUrlText(subpath)); + const declarationRejection = rejectDeclaration(subpathPath); if (declarationRejection !== null) return declarationRejection; + let depth = directory === "." ? 0 : directory.split("/").length; + for (const segment of subpathPath.split("/")) { + if (segment === "..") { + if (depth === 0) + return unresolvedCandidate("rejected", [ + "The resolved candidate escapes the canonical artifact root.", + ]); + depth--; + } else if (segment !== ".") depth++; + } const metadataUrl = packageMetadataUrl( posix.join(directory, "package.json"), subpath, @@ -1208,7 +1217,9 @@ const resolveExportTarget = ( const candidate = confineCandidate(decoded); if (typeof candidate !== "string") return withTargetSource(candidate); if (target.lookupLimitation !== undefined) - return unresolvedCandidate("unavailable", [target.lookupLimitation]); + return withTargetSource( + unresolvedCandidate("unavailable", [target.lookupLimitation]), + ); return ( resolveFileCandidates(input, [candidate]) ?? withTargetSource(exactPackageTargetNotFound(candidate, mapKind)) diff --git a/tests/acceptance/applications/packageSpecifierSuffixCli.test.ts b/tests/acceptance/applications/packageSpecifierSuffixCli.test.ts index ee319ff7e0..687db996d5 100644 --- a/tests/acceptance/applications/packageSpecifierSuffixCli.test.ts +++ b/tests/acceptance/applications/packageSpecifierSuffixCli.test.ts @@ -9,6 +9,7 @@ import { expect } from "vitest"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest } from "../../support/cli/cliFixture.js"; import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js"; +import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js"; type PackageSpecifierCase = { readonly specifier: string; @@ -382,3 +383,62 @@ cliTest.for(["CLI", "stdio MCP"] as const)( ); }, ); + +cliTest.for(["CLI", "stdio MCP"] as const)( + "confines legacy package traversal before URL normalization through %s", + async (surface, { cli }) => { + const root = await createTestTempDirectory("rea-package-root-traversal-"); + const cases = [ + { specifier: "legacy/../../outside.js", status: "resolved" }, + { + specifier: "legacy/../../outside.js?cache=/../../rea-artifact/#/..", + status: "resolved", + }, + { specifier: "legacy//../../../outside.js", status: "resolved" }, + { + specifier: "legacy/../../../rea-artifact/outside.js", + status: "rejected", + }, + { + specifier: "legacy/../../../../rea-artifact/outside.js", + status: "rejected", + }, + { + specifier: "legacy/../.\t./.\n./rea-artifact/outside.js?cache=1#entry", + status: "rejected", + }, + ] as const; + const source = cases + .map(({ specifier }) => `import ${JSON.stringify(specifier)};`) + .join("\n"); + await writeFixtureFiles(root, { + "main.mjs": source, + "outside.js": "export default 1;", + "node_modules/legacy/package.json": "{}", + }); + const { graph } = await analyzeJavaScriptDirectory(surface, cli, root); + for (const { specifier, status } of cases) + expect(graph.edges).toContainEqual( + expect.objectContaining({ + relation: "imports", + properties: expect.objectContaining({ + specifier, + resolution_status: status, + resolved_path: status === "resolved" ? "outside.js" : null, + }), + evidence: expect.objectContaining({ + ...(status === "rejected" + ? { + limitations: expect.arrayContaining([ + "The resolved candidate escapes the canonical artifact root.", + ]), + } + : {}), + artifact: expect.objectContaining({ + sha256: createHash("sha256").update(source).digest("hex"), + }), + }), + }), + ); + }, +); diff --git a/tests/acceptance/applications/packageUrlUnicodeCli.test.ts b/tests/acceptance/applications/packageUrlUnicodeCli.test.ts index f7447feae4..5dcdd46db6 100644 --- a/tests/acceptance/applications/packageUrlUnicodeCli.test.ts +++ b/tests/acceptance/applications/packageUrlUnicodeCli.test.ts @@ -232,6 +232,13 @@ cliTest.for(["CLI", "stdio MCP"] as const)( request.status === "root-legacy" ? fileStatus : request.status; + const mapKind = request.specifier.startsWith("#") + ? "imports" + : "exports"; + const selectedTarget = + request.specifier === "mapped/structuralReentry" + ? targets.structuralReentry.value + : targets.feature.value; expect(graph.edges).toContainEqual( expect.objectContaining({ relation: "imports", @@ -249,6 +256,13 @@ cliTest.for(["CLI", "stdio MCP"] as const)( status === "unavailable" && request.status !== "unavailable" ? expect.arrayContaining([ expect.stringContaining("unpaired UTF-16 surrogate"), + ...(request.status === "relative" + ? [ + expect.stringContaining( + `${mapKind} target ${JSON.stringify(selectedTarget)}:`, + ), + ] + : []), ]) : expect.not.arrayContaining([ expect.stringContaining("unpaired UTF-16 surrogate"),