From a67dbf6a488c8433ec3721d16ad3fa6623785a5f Mon Sep 17 00:00:00 2001 From: Fazmin Date: Fri, 9 Oct 2026 23:52:38 -0400 Subject: [PATCH] fix: reject directory paths for the Wakaru limiter and mitmdump `REA_JAVASCRIPT_PRLIMIT_COMMAND` and `REA_MITMDUMP_COMMAND` were admitted after an access check alone, which a searchable directory passes. Admission then succeeded, and the failure surfaced later as a launch-time adapter error instead of the capability refusal that names the configured path. Require a regular file, as #751 did for the firmware engine and limiter. Linux-only tests mirror that change; both checks only run on Linux hosts. Co-Authored-By: Claude Opus 5.5 --- .../history/MitmproxyCaptureAdapter.ts | 4 +- src/javascript/recovery/WakaruCommand.ts | 4 +- .../browser/historicalCaptureDecoder.test.ts | 38 +++++++++++++++ .../wakaruCommandConfiguration.test.ts | 47 +++++++++++++++++++ 4 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 tests/boundary/javascript-recovery/wakaruCommandConfiguration.test.ts diff --git a/src/browser/history/MitmproxyCaptureAdapter.ts b/src/browser/history/MitmproxyCaptureAdapter.ts index 2e71720c73..7918843411 100644 --- a/src/browser/history/MitmproxyCaptureAdapter.ts +++ b/src/browser/history/MitmproxyCaptureAdapter.ts @@ -1,4 +1,4 @@ -import { access } from "node:fs/promises"; +import { access, stat } from "node:fs/promises"; import { constants } from "node:fs"; import { isAbsolute } from "node:path"; import { fileURLToPath } from "node:url"; @@ -32,6 +32,8 @@ export class MitmproxyCaptureAdapter implements HistoricalCaptureFormatAdapter { ); try { await access(command, constants.R_OK | constants.X_OK); + if (!(await stat(command)).isFile()) + throw new TypeError("Configured path is not a regular file"); } catch (cause: unknown) { const reason = `Configured mitmdump executable is unavailable: ${command}.`; throw new AnalysisCapabilityUnavailableError( diff --git a/src/javascript/recovery/WakaruCommand.ts b/src/javascript/recovery/WakaruCommand.ts index e024474818..fe4500ba96 100644 --- a/src/javascript/recovery/WakaruCommand.ts +++ b/src/javascript/recovery/WakaruCommand.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; import { constants } from "node:fs"; -import { access, realpath } from "node:fs/promises"; +import { access, realpath, stat } from "node:fs/promises"; import { isAbsolute } from "node:path"; import { AnalysisCancelledError, @@ -87,6 +87,8 @@ export const resolveWakaruCommand = async ( if (!isAbsolute(limiter)) throw new TypeError("REA_JAVASCRIPT_PRLIMIT_COMMAND must be absolute"); await access(limiter, constants.X_OK); + if (!(await stat(limiter)).isFile()) + throw new TypeError(`Resource limiter is not a regular file: ${limiter}`); return { command, limiter, ...fingerprint }; } catch (cause: unknown) { const reason = `Configured Wakaru or util-linux prlimit is unavailable: ${recoveryFailureMessage(cause)}`; diff --git a/tests/boundary/browser/historicalCaptureDecoder.test.ts b/tests/boundary/browser/historicalCaptureDecoder.test.ts index 23c4604a31..92b26d5df5 100644 --- a/tests/boundary/browser/historicalCaptureDecoder.test.ts +++ b/tests/boundary/browser/historicalCaptureDecoder.test.ts @@ -2,8 +2,10 @@ import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js"; import { access, chmod, + mkdir, mkdtemp, rm, + symlink, truncate, writeFile, } from "node:fs/promises"; @@ -11,6 +13,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { expect, it, onTestFinished } from "vitest"; import { HistoricalCaptureDecoder } from "../../../src/browser/history/HistoricalCaptureDecoder.js"; +import { MitmproxyCaptureAdapter } from "../../../src/browser/history/MitmproxyCaptureAdapter.js"; import { HAR_CAPTURE_PROVIDER_IDENTITY } from "../../../src/browser/history/CaptureRelease.js"; import { inspectWebNetworkCaptureInputSchema, @@ -20,6 +23,41 @@ import { historicalHar } from "../../fixtures/historicalHar.js"; import { decodeHarCapture } from "../../../src/browser/history/HarCapture.js"; import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; +const linux = it.skipIf(process.platform !== "linux"); + +linux("refuses a directory configured as the mitmdump executable", async () => { + const root = await mkdtemp(join(tmpdir(), "rea-mitmdump-command-")); + onTestFinished(() => rm(root, { recursive: true, force: true })); + const command = join(root, "mitmdump"); + await mkdir(command); + + await expect( + new MitmproxyCaptureAdapter({ REA_MITMDUMP_COMMAND: command }).command( + join(root, "request.json"), + join(root, "runtime"), + ), + ).rejects.toMatchObject({ + _tag: "AnalysisCapabilityUnavailableError", + reason: expect.stringContaining(command), + }); +}); + +linux("allows a symlink to a regular mitmdump executable", async () => { + const root = await mkdtemp(join(tmpdir(), "rea-mitmdump-command-symlink-")); + onTestFinished(() => rm(root, { recursive: true, force: true })); + const command = join(root, "mitmdump"); + const target = join(root, "mitmdump-bin"); + await writeFile(target, "#!/bin/sh\n", { mode: 0o700 }); + await symlink(target, command); + + await expect( + new MitmproxyCaptureAdapter({ REA_MITMDUMP_COMMAND: command }).command( + join(root, "request.json"), + join(root, "runtime"), + ), + ).resolves.toMatchObject({ command }); +}); + it.each([true, false])( "binds decoder replies to the complete selected provider identity (matching name: %s)", async (matching) => { diff --git a/tests/boundary/javascript-recovery/wakaruCommandConfiguration.test.ts b/tests/boundary/javascript-recovery/wakaruCommandConfiguration.test.ts new file mode 100644 index 0000000000..ce9845be5d --- /dev/null +++ b/tests/boundary/javascript-recovery/wakaruCommandConfiguration.test.ts @@ -0,0 +1,47 @@ +import { mkdir, symlink, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect, it } from "vitest"; +import { resolveWakaruCommand } from "../../../src/javascript/recovery/WakaruCommand.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +const linuxX64 = it.skipIf( + process.platform !== "linux" || process.arch !== "x64", +); + +linuxX64( + "refuses a directory configured as the Wakaru resource limiter", + async () => { + const root = await createTestTempDirectory("rea-wakaru-limiter-"); + const command = join(root, "wakaru"); + const limiter = join(root, "prlimit"); + await writeFile(command, "#!/bin/sh\n", { mode: 0o700 }); + await mkdir(limiter); + + await expect( + resolveWakaruCommand({ + REA_WAKARU_COMMAND: command, + REA_JAVASCRIPT_PRLIMIT_COMMAND: limiter, + }), + ).rejects.toMatchObject({ + _tag: "AnalysisCapabilityUnavailableError", + reason: expect.stringContaining(limiter), + }); + }, +); + +linuxX64("allows a symlink to a regular Wakaru resource limiter", async () => { + const root = await createTestTempDirectory("rea-wakaru-limiter-symlink-"); + const command = join(root, "wakaru"); + const limiter = join(root, "prlimit"); + const target = join(root, "prlimit-bin"); + await writeFile(command, "#!/bin/sh\n", { mode: 0o700 }); + await writeFile(target, "#!/bin/sh\n", { mode: 0o700 }); + await symlink(target, limiter); + + await expect( + resolveWakaruCommand({ + REA_WAKARU_COMMAND: command, + REA_JAVASCRIPT_PRLIMIT_COMMAND: limiter, + }), + ).resolves.toMatchObject({ limiter }); +});