Files
reverse-engineering-anything/tests/boundary/filesystem/electronFileScope.test.ts
T
Kaoru0822andClaude Opus 5.5 8451b961ad feat(artifacts): make ASAR header admission configurable (#6209)
* feat(artifacts): make ASAR header admission configurable

ASAR header/index admission was a fixed 16 MiB with no override, so
operators with large Electron indexes could only extract the archive.

REA_ASAR_MAX_HEADER_BYTES now sets it (default 16777216, maximum
268435456). One owner resolves the effective admission for the
classification probe, inventory, ASAR reader and Electron file scope,
including the Electron header cache budget. Startup and doctor validate
the setting; admission refuses an invalid value instead of falling back.
Recovery and error details name the effective value and its source.

Fixes #6176

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(artifacts): resolve ASAR header admission from the caller's environment

The admission read process.env, so composition roots that select their own
environment (createDirectAnalysis, createServer, CLI, optional observation
factories) could not raise or lower it, and an ambient value leaked into
callers that left it unset.

Nothing reads process.env now. ArtifactProvider and extraction resolve the
admission from their environment; JavaScript analysis parses it from the
environment its server or CLI caller passes and refuses an invalid value;
the Electron and V8 providers receive it from their factories. Inventory,
nested ASAR readers, classification probes and Electron page authorization
carry that one value. A header cached by Electron under a larger admission
is refused for a smaller one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:18:22 -07:00

159 lines
6.0 KiB
TypeScript

import {
mkdir,
readFile,
realpath,
symlink,
writeFile,
} from "node:fs/promises";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { createPackageWithOptions } from "@electron/asar";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { authorizedElectronFile } from "../../../src/browser/ElectronFileScope.js";
describe("Electron file scope", () => {
it("accepts dot-prefixed child names beneath a canonical root", async () => {
const root = await createTestTempDirectory("rea-electron-dot-child-");
const directory = join(root, "..cache");
await mkdir(directory);
const path = join(directory, "index.html");
await writeFile(path, "allowed");
expect(await authorizedElectronFile(pathToFileURL(path).href)).toBe(path);
});
it("accepts explicit canonical local files and rejects remote or malformed URLs", async () => {
const base = await createTestTempDirectory("rea-electron-scope-");
const root = join(base, "root");
const outside = join(base, "outside");
await mkdir(root);
await mkdir(outside);
const allowed = join(root, "index.html");
const denied = join(outside, "secret.html");
await writeFile(allowed, "allowed");
await writeFile(denied, "denied");
await symlink(denied, join(root, "escape.html"));
expect(await authorizedElectronFile(pathToFileURL(allowed).href)).toBe(
allowed,
);
expect(await authorizedElectronFile(pathToFileURL(denied).href)).toBe(
denied,
);
expect(
await authorizedElectronFile(
pathToFileURL(join(root, "escape.html")).href,
),
).toBe(denied);
expect(
await authorizedElectronFile("file://server/share/index.html"),
).toBeUndefined();
expect(
await authorizedElectronFile("file:///tmp/root%2Findex.html"),
).toBeUndefined();
});
it("resolves regular files Electron serves from inside an ASAR archive", async () => {
const root = await createTestTempDirectory("rea-electron-asar-scope-");
const source = join(root, "source");
await mkdir(join(source, "build", "static"), { recursive: true });
await mkdir(join(source, "native"));
await writeFile(join(source, "build", "index.html"), "<p>packed</p>");
await writeFile(join(source, "build", "static", "app.js"), "app();");
await writeFile(join(source, "native", "addon.node"), "native");
await writeFile(join(source, "notes.txt"), "plain");
const archive = join(root, "app.asar");
await createPackageWithOptions(source, archive, { unpack: "*.node" });
const canonical = await realpath(archive);
const member = (...parts: string[]) =>
pathToFileURL(join(archive, ...parts)).href;
expect(await authorizedElectronFile(member("build", "index.html"))).toBe(
join(canonical, "build", "index.html"),
);
expect(
await authorizedElectronFile(
`${member("build", "static", "app.js")}?isBookDoubleClicked=0#/library`,
),
).toBe(join(canonical, "build", "static", "app.js"));
expect(await authorizedElectronFile(member("native", "addon.node"))).toBe(
join(canonical, "native", "addon.node"),
);
expect(await authorizedElectronFile(member("build"))).toBeUndefined();
expect(
await authorizedElectronFile(member("build", "missing.html")),
).toBeUndefined();
expect(
await authorizedElectronFile(
pathToFileURL(join(source, "notes.txt", "index.html")).href,
),
).toBeUndefined();
});
it("accepts file URLs with long query text", async () => {
const root = await createTestTempDirectory("rea-electron-long-url-");
const path = join(root, "index.html");
await writeFile(path, "allowed");
const value = `${pathToFileURL(path).href}?${"x".repeat(70_000)}`;
await expect(authorizedElectronFile(value)).resolves.toBe(path);
});
it("resolves an archive beneath an ASAR-named directory and rejects oversized declared headers", async () => {
const root = await createTestTempDirectory("rea-electron-header-");
const source = join(root, "source");
await mkdir(source);
await writeFile(join(source, "index.html"), "page");
const parent = join(root, "parent.asar");
await mkdir(parent);
const archive = join(parent, "app.asar");
await createPackageWithOptions(source, archive, {});
expect(
await authorizedElectronFile(
pathToFileURL(join(archive, "index.html")).href,
),
).toBe(join(await realpath(archive), "index.html"));
const malformed = join(root, "bad.asar");
const header = Buffer.alloc(16);
header.writeUInt32LE(4, 0);
header.writeUInt32LE(0x40000000, 4);
await writeFile(malformed, header);
expect(
await authorizedElectronFile(
pathToFileURL(join(malformed, "index.html")).href,
),
).toBeUndefined();
});
it("authorizes ASAR members only under the provider's selected header admission", async () => {
const root = await createTestTempDirectory("rea-electron-admission-");
const source = join(root, "source");
await mkdir(source);
await writeFile(join(source, "index.html"), "page");
const archive = join(root, "app.asar");
await createPackageWithOptions(source, archive, {});
const headerBytes = (await readFile(archive)).readUInt32LE(4);
const page = pathToFileURL(join(archive, "index.html")).href;
const admission = (maximumBytes: number) =>
({ maximumBytes, source: "setting" }) as const;
// The first read caches the header under an admission that fits it.
expect(
await authorizedElectronFile(
page,
undefined,
"inspect_electron_page",
admission(headerBytes),
),
).toBe(join(await realpath(archive), "index.html"));
// A provider with a lower admission must not reuse that cached header.
expect(
await authorizedElectronFile(
page,
undefined,
"inspect_electron_page",
admission(headerBytes - 1),
),
).toBeUndefined();
});
});