From f878266503e5eef3e9dca6ec85126f83d60b41c9 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 27 Oct 2026 09:48:52 +0800 Subject: [PATCH] fix(artifacts): admit selected app bundle traversal --- src/application/AppBundleExecutable.ts | 3 ++ src/application/AppBundleResourceRoot.test.ts | 27 ++++++++++++++++ .../BinarySessionBundleResources.test.ts | 31 ++++++++++++++++++- 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/application/AppBundleExecutable.ts b/src/application/AppBundleExecutable.ts index 3303b44478..955ed7a239 100644 --- a/src/application/AppBundleExecutable.ts +++ b/src/application/AppBundleExecutable.ts @@ -119,6 +119,9 @@ export const findAppBundleResourceRoot = async ( options.cwd === undefined ? {} : { cwd: options.cwd }, ); try { + // Admit the selected spelling before Darwin realpath can collapse file/.. + // into a different, otherwise valid bundle resource root. + await stat(callerPath); const canonicalInput = await realpath(callerPath); const directLayout = await presentLayout(canonicalInput); const wrapped = diff --git a/src/application/AppBundleResourceRoot.test.ts b/src/application/AppBundleResourceRoot.test.ts index c8f76a12bb..fd1a716a08 100644 --- a/src/application/AppBundleResourceRoot.test.ts +++ b/src/application/AppBundleResourceRoot.test.ts @@ -2,6 +2,7 @@ import { chmod, mkdir, mkdtemp, + realpath, rm, symlink, writeFile, @@ -57,6 +58,32 @@ it("resolves resource roots without requiring or hashing CFBundleExecutable", as } }); +it.skipIf(process.platform === "win32")( + "preserves valid symlink-parent traversal when selecting resource roots", + async () => { + const temp = await mkdtemp(join(tmpdir(), "rea-resource-parent-")); + try { + await mkdir(join(temp, "physical", "child"), { recursive: true }); + await mkdir(join(temp, "physical", "Fixture.app")); + await writeFile( + join(temp, "physical", "Fixture.app", "Info.plist"), + "", + ); + await symlink(join(temp, "physical", "child"), join(temp, "alias")); + const selected = `${temp}/alias/../Fixture.app`; + const resolved = await resolveAppBundleResourceRoot(selected); + expect(resolved.ok).toBe(true); + if (!resolved.ok) throw resolved.error; + expect(resolved.value.callerPath).toBe(selected); + expect(resolved.value.canonicalPath).toBe( + await realpath(join(temp, "physical", "Fixture.app")), + ); + } finally { + await rm(temp, { recursive: true, force: true }); + } + }, +); + it("resolves a flat bundle resource root without inferring executable identity", async () => { const temp = await mkdtemp(join(tmpdir(), "rea-ios-resource-root-")); try { diff --git a/src/application/binary/BinarySessionBundleResources.test.ts b/src/application/binary/BinarySessionBundleResources.test.ts index 76e7d574bd..506ec45635 100644 --- a/src/application/binary/BinarySessionBundleResources.test.ts +++ b/src/application/binary/BinarySessionBundleResources.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, rm, stat, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, expect, it } from "vitest"; @@ -44,6 +44,35 @@ afterEach(async () => { ); }); +it.skipIf(process.platform === "win32")( + "refuses invalid selected traversal before creating a bundle resource client", + async () => { + const root = await mkdtemp(join(tmpdir(), "rea-resource-selected-")); + temporaryDirectories.push(root); + await mkdir(join(root, "Fixture.app")); + await writeFile( + join(root, "Fixture.app", "Info.plist"), + "", + ); + await writeFile(join(root, "regular"), "file"); + let created = false; + const session = new BinarySession( + SessionProviderRouter.selectable(new AnalysisProviderRegistry([]), []), + new InvestigationRecords(), + { + createClient: () => { + created = true; + throw new Error("Invalid selection must not create a client"); + }, + }, + ); + const selected = `${root}/regular/../Fixture.app`; + await expect(stat(selected)).rejects.toMatchObject({ code: "ENOTDIR" }); + expect((await session.openBundleResources(selected)).ok).toBe(false); + expect(created).toBe(false); + }, +); + it("opens a resource-only bundle without executable identity and keeps snapshot operations inapplicable", async () => { const rootPath = await mkdtemp(join(tmpdir(), "rea-resource-session-")); temporaryDirectories.push(rootPath);