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 75ada1f971..6db3f7e161 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 e2a8d20cf6..62ebb34f2f 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 80fcc80bb5..9ed9c35527 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 adef5a7eab..110bd1d914 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 70a86b79a7..ed2a120e0b 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 6382402964..dfd38344b1 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 },