fix(contracts): reject OS-invalid local paths in binary and artifact tools (#1925)

* 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>
This commit is contained in:
Vinícius Andrade 2026-10-11 21:53:03 -03:00 committed by GitHub
parent eaf88aca2a
commit 976c2c083e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 35 additions and 19 deletions

View File

@ -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(

View File

@ -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. */

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -96,6 +96,16 @@ const pathRequests: readonly [string, Record<string, unknown>][] = [
"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 },