fix(target): confine app program directories to their canonical bundle
Validate the canonical programs directory before resolving its executable, preserving containment failures even when the external child is missing. Retain in-bundle directory and bundle aliases. Co-authored-by: gongfpp <22683417+gongfpp@users.noreply.github.com>
This commit is contained in:
1 parent
fb51960d7d
commit
5fb6edc34b
3 files changed
+124
-4
No files matched your search
@@ -32,3 +32,46 @@ describe("app bundle Info.plist magic", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("app bundle program directory containment", () => {
|
||||
it("rejects an escaping programs directory before resolving its child", async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-app-programs-"));
|
||||
try {
|
||||
const bundle = join(root, "Fixture.app");
|
||||
const programs = join(bundle, "Contents", "MacOS");
|
||||
const executable = join(programs, "Missing");
|
||||
const canonicalBundle = join(root, "Canonical.app");
|
||||
const outsidePrograms = join(root, "outside");
|
||||
const plist =
|
||||
"<plist><dict><key>CFBundleExecutable</key><string>Missing</string></dict></plist>";
|
||||
await mkdir(join(bundle, "Contents"), { recursive: true });
|
||||
await writeFile(join(bundle, "Contents", "Info.plist"), plist);
|
||||
let childResolved = false;
|
||||
|
||||
const result = await resolveAppBundleExecutable(bundle, {
|
||||
readFile: async () => Buffer.from(plist),
|
||||
realpath: async (path) => {
|
||||
if (path === bundle) return canonicalBundle;
|
||||
if (path === programs) return outsidePrograms;
|
||||
if (path === executable) {
|
||||
childResolved = true;
|
||||
throw Object.assign(new Error("missing executable"), {
|
||||
code: "ENOENT",
|
||||
});
|
||||
}
|
||||
return path;
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
reason: "app program directory leaves Contents/MacOS",
|
||||
},
|
||||
});
|
||||
expect(childResolved).toBe(false);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -219,11 +219,24 @@ const resolveLayoutExecutable = async (
|
||||
);
|
||||
const programs = join(bundle, ...layout.programs);
|
||||
const executable = join(programs, name);
|
||||
let canonicalBundle: string;
|
||||
try {
|
||||
const [canonicalPrograms, canonicalExecutable] = await Promise.all([
|
||||
fileSystem.realpath(programs),
|
||||
fileSystem.realpath(executable),
|
||||
]);
|
||||
canonicalBundle = await fileSystem.realpath(bundle);
|
||||
} catch (cause: unknown) {
|
||||
return err(
|
||||
new BinaryTargetError(bundle, errorReason(cause, bundle), { cause }),
|
||||
);
|
||||
}
|
||||
try {
|
||||
const canonicalPrograms = await fileSystem.realpath(programs);
|
||||
if (!isPathWithinRoot(canonicalBundle, canonicalPrograms))
|
||||
return err(
|
||||
new BinaryTargetError(
|
||||
bundle,
|
||||
`app program directory leaves ${layout.programsLabel}`,
|
||||
),
|
||||
);
|
||||
const canonicalExecutable = await fileSystem.realpath(executable);
|
||||
if (!isPathWithinRoot(canonicalPrograms, canonicalExecutable))
|
||||
return err(
|
||||
new BinaryTargetError(
|
||||
|
||||
@@ -429,7 +429,9 @@ describe("binary target I/O: filesystem and app bundle target resolution", () =>
|
||||
).ok,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("app bundle canonical containment", () => {
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"rejects an app program symlink that leaves the bundle",
|
||||
async () => {
|
||||
@@ -455,6 +457,68 @@ describe("binary target I/O: filesystem and app bundle target resolution", () =>
|
||||
).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"rejects a Contents/MacOS directory symlink that leaves the bundle",
|
||||
async () => {
|
||||
const directory = await createTestTempDirectory("rea-target-");
|
||||
const app = join(directory, "Escaping.app");
|
||||
const contents = join(app, "Contents");
|
||||
const programs = join(contents, "MacOS");
|
||||
const outside = join(directory, "outside");
|
||||
await mkdir(contents, { recursive: true });
|
||||
await mkdir(outside);
|
||||
await writeFile(
|
||||
join(contents, "Info.plist"),
|
||||
"<plist><dict><key>CFBundleExecutable</key><string>Escaping</string></dict></plist>",
|
||||
);
|
||||
await writeFile(
|
||||
join(outside, "Escaping"),
|
||||
thinMach(0xfeedfacf, 0x0100000c),
|
||||
);
|
||||
await symlink(outside, programs, "dir");
|
||||
|
||||
const result = await parseBinaryTarget(app, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "arm64",
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
path: app,
|
||||
reason: "app program directory leaves Contents/MacOS",
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"allows bundle and program directory aliases within the canonical bundle",
|
||||
async () => {
|
||||
const directory = await createTestTempDirectory("rea-target-");
|
||||
const app = join(directory, "Canonical.app");
|
||||
const alias = join(directory, "Alias.app");
|
||||
const contents = join(app, "Contents");
|
||||
const programs = join(contents, "Programs");
|
||||
const executable = join(programs, "Example");
|
||||
await mkdir(programs, { recursive: true });
|
||||
await writeFile(executable, thinMach(0xfeedfacf, 0x0100000c));
|
||||
await writeFile(
|
||||
join(contents, "Info.plist"),
|
||||
"<plist><dict><key>CFBundleExecutable</key><string>Example</string></dict></plist>",
|
||||
);
|
||||
await symlink(programs, join(contents, "MacOS"), "dir");
|
||||
await symlink(app, alias, "dir");
|
||||
const result = await parseBinaryTarget(alias, {
|
||||
cwd: directory,
|
||||
hostArchitecture: "arm64",
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
ok: true,
|
||||
value: { path: await realpath(executable), format: "mach-o" },
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("binary target I/O: explicit kinds and executable headers", () => {
|
||||
|
||||
Reference in new issue
Block a user