From 976c2c083eee1fdd9e89a1a720f768a6ee6df277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Andrade?= <95101505+viniciusdsandrade@users.noreply.github.com> Date: Sun, 11 Oct 2026 21:53:03 -0300 Subject: [PATCH] fix(contracts): reject OS-invalid local paths in binary and artifact tools (#1925) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(contracts): reject OS-invalid local paths in binary and artifact tools Enforce localPathStringSchema across inspect_pe_resources, inspect_binary_layout, inspect_recorded_crash, inspect_wasm_artifact, and inspect_evm_interface input schemas to reject embedded NUL bytes at the contract boundary instead of failing later during filesystem operations. Add corresponding acceptance test cases to the advertised MCP contracts matrix in localToolContracts.test.ts. Signed-off-by: Vinícius DS Andrade <95101505+viniciusdsandrade@users.noreply.github.com> * style(contracts): wrap pe resources path description under print width Wrap the inspectPeResourcesInputSchema path property description to keep lines within the 80-column oxfmt limit. Signed-off-by: Vinícius DS Andrade <95101505+viniciusdsandrade@users.noreply.github.com> --------- Signed-off-by: Vinícius DS Andrade <95101505+viniciusdsandrade@users.noreply.github.com> --- src/domain/evm/evmInterface.ts | 9 +++++---- src/domain/native/binaryLayout.ts | 8 ++++---- src/domain/native/peResources.ts | 6 +++++- src/domain/native/recordedCrash.ts | 8 ++++---- src/domain/wasm/wasmArtifact.ts | 13 +++++++------ tests/acceptance/tools/localToolContracts.test.ts | 10 ++++++++++ 6 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/domain/evm/evmInterface.ts b/src/domain/evm/evmInterface.ts index 75ada1f97..6db3f7e16 100644 --- a/src/domain/evm/evmInterface.ts +++ b/src/domain/evm/evmInterface.ts @@ -1,14 +1,15 @@ import { z } from "zod"; +import { localPathStringSchema } from "../localPath.js"; + const byteOffset = z.number().int().nonnegative(); const sha256 = z.string().regex(/^[a-f0-9]{64}$/); /** Select the carrier representation explicitly; no RPC or deployed-contract lookup is implied. */ export const inspectEvmInterfaceInputSchema = z.strictObject({ - path: z - .string() - .min(1) - .describe("Absolute path to caller-selected local bytecode carrier"), + path: localPathStringSchema.describe( + "Absolute path to caller-selected local bytecode carrier", + ), encoding: z .enum(["raw", "hex"]) .describe( diff --git a/src/domain/native/binaryLayout.ts b/src/domain/native/binaryLayout.ts index e2a8d20cf..62ebb34f2 100644 --- a/src/domain/native/binaryLayout.ts +++ b/src/domain/native/binaryLayout.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { localPathStringSchema } from "../localPath.js"; import { CANONICAL_BASE64_PATTERN } from "../stringPatterns.js"; const unsignedHex = z.string().regex(/^0x(?:0|[1-9a-f][0-9a-f]{0,15})$/); @@ -16,10 +17,9 @@ const name = z.strictObject({ /** Explicit local object selection, independent of an active disassembler target. */ export const inspectBinaryLayoutInputSchema = z.strictObject({ - path: z - .string() - .min(1) - .describe("Absolute filesystem path to the selected binary"), + path: localPathStringSchema.describe( + "Absolute filesystem path to the selected binary", + ), }); /** File and linked-image evidence; numeric ELF values never pass through unsafe JSON numbers. */ diff --git a/src/domain/native/peResources.ts b/src/domain/native/peResources.ts index 80fcc80bb..9ed9c3552 100644 --- a/src/domain/native/peResources.ts +++ b/src/domain/native/peResources.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +import { localPathStringSchema } from "../localPath.js"; + const uint = z.number().int().min(0).max(0xffffffff); const range = z.strictObject({ offset: uint, bytes: uint }); const identity = z.discriminatedUnion("kind", [ @@ -14,7 +16,9 @@ const identity = z.discriminatedUnion("kind", [ /** Complete bounded inspection of a caller-selected PE image, without execution. */ export const inspectPeResourcesInputSchema = z.strictObject({ - path: z.string().min(1).describe("Absolute filesystem path to a PE image"), + path: localPathStringSchema.describe( + "Absolute filesystem path to a PE image", + ), max_file_bytes: z .number() .int() diff --git a/src/domain/native/recordedCrash.ts b/src/domain/native/recordedCrash.ts index adef5a7ea..110bd1d91 100644 --- a/src/domain/native/recordedCrash.ts +++ b/src/domain/native/recordedCrash.ts @@ -1,5 +1,6 @@ import { z } from "zod"; +import { localPathStringSchema } from "../localPath.js"; import { CANONICAL_BASE64_PATTERN } from "../stringPatterns.js"; import { recordedNoteCoverageIssues } from "./recordedCrashNoteCoverage.js"; @@ -50,10 +51,9 @@ const supportedRegisters = new Set(RECORDED_LINUX_AMD64_REGISTERS); /** Select an explicit recording and optionally request debugger-derived mapping context. */ export const inspectRecordedCrashInputSchema = z.strictObject({ - path: z - .string() - .min(1) - .describe("Absolute filesystem path to a supplied Linux ELF core"), + path: localPathStringSchema.describe( + "Absolute filesystem path to a supplied Linux ELF core", + ), include_debugger_context: z .boolean() .default(false) diff --git a/src/domain/wasm/wasmArtifact.ts b/src/domain/wasm/wasmArtifact.ts index 70a86b79a..ed2a120e0 100644 --- a/src/domain/wasm/wasmArtifact.ts +++ b/src/domain/wasm/wasmArtifact.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +import { localPathStringSchema } from "../localPath.js"; + const digest = z.string().regex(/^[a-f0-9]{64}$/); const artifact = z.strictObject({ path: z.string().min(1), @@ -8,12 +10,11 @@ const artifact = z.strictObject({ }); /** All artifacts are explicitly selected local files; references never trigger acquisition. */ export const inspectWasmArtifactInputSchema = z.strictObject({ - path: z - .string() - .min(1) - .describe("Absolute selected local WASM artifact path"), - glue_paths: z.array(z.string().min(1)).max(16).default([]), - candidate_paths: z.array(z.string().min(1)).max(16).default([]), + path: localPathStringSchema.describe( + "Absolute selected local WASM artifact path", + ), + glue_paths: z.array(localPathStringSchema).max(16).default([]), + candidate_paths: z.array(localPathStringSchema).max(16).default([]), }); /** Upstream output stays intact; import/export names are never split on ambiguous punctuation. */ export const wasmArtifactSchema = z diff --git a/tests/acceptance/tools/localToolContracts.test.ts b/tests/acceptance/tools/localToolContracts.test.ts index 638240296..dfd38344b 100644 --- a/tests/acceptance/tools/localToolContracts.test.ts +++ b/tests/acceptance/tools/localToolContracts.test.ts @@ -96,6 +96,16 @@ const pathRequests: readonly [string, Record][] = [ "export_web_scripts", { capture_path: nul, output_directory: "/tmp/exported-scripts" }, ], + ["inspect_pe_resources", { path: nul }], + ["inspect_binary_layout", { path: nul }], + ["inspect_recorded_crash", { path: nul }], + ["inspect_wasm_artifact", { path: nul }], + ["inspect_wasm_artifact", { path: "/tmp/sample.wasm", glue_paths: [nul] }], + [ + "inspect_wasm_artifact", + { path: "/tmp/sample.wasm", candidate_paths: [nul] }, + ], + ["inspect_evm_interface", { path: nul, encoding: "raw" }], [ "export_web_scripts", { capture_path: "/tmp/capture.json", output_directory: nul },