Files
rea/src/domain/webNetworkCapture.ts
T
DeryFerd 499ac18011 fix(web): reject NUL bytes in web tool local path inputs
Four web tools accepted caller-selected local paths as plain strings, so
a NUL byte passed contract validation and reached filesystem calls. The
services only checked isAbsolute, and isAbsolute admits a NUL, so the
failure surfaced later as a raw ERR_INVALID_ARG_VALUE instead of a named
input constraint. Every other local-input tool already refuses NUL through
the shared local-path schema, and the existing OS-invalid-local-input
harness covers those but not these web tools.

inspect_web_network_capture.capture_path, export_web_scripts.capture_path
and output_directory, trace_web_source_location.manifest_path and
source_map.path, and trace_web_module_imports.manifest_path and
import_map.path now use localPathStringSchema. Each field already required
a nonempty string, so the advertised pattern and minLength stay the same
and only the NUL refusal is added.

Validation: a new schema test rejects NUL in all seven fields while
accepting ordinary absolute paths; the four tools are added to the
OS-invalid-local-input harness and reject NUL through the real advertised
MCP contracts; typecheck, oxlint, oxfmt, and toolSchemaValidity with a
regenerated catalog. Two webScriptExport boundary cases fail identically
with and without this change on this Windows host (symlink EPERM and
private-runtime-root authority), so they are environmental.
2026-10-11 23:32:52 +07:00

142 lines
4.6 KiB
TypeScript

import { z } from "zod";
import { jsonValueSchema } from "./jsonValue.js";
import { localPathStringSchema } from "./localPath.js";
/** Complete historical evidence budgets; exceeding a budget returns no partial capture. */
export const WEB_NETWORK_CAPTURE_LIMITS = {
inputBytes: 32 * 1024 * 1024,
outputBytes: 96 * 1024 * 1024,
timeoutMs: 30_000,
depth: 64,
} as const;
/** Inspect an explicit retained capture without contacting its recorded destinations. */
export const inspectWebNetworkCaptureInputSchema = z.strictObject({
capture_path: localPathStringSchema.describe(
"Absolute local HAR or mitmproxy capture path.",
),
format: z.enum(["har", "mitmproxy"]),
record_ordinals: z
.array(z.number().int().nonnegative())
.min(1)
.optional()
.describe(
"Zero-based producer record ordinals, in caller-selected order. Omit to inspect every record.",
),
sensitive_values: z
.array(z.string().min(1))
.default([])
.describe(
"Literal text excluded from capture payloads, byte/numeric projections, producer coordinates, artifact paths/digests and diagnostic text. Declaration strings are never persisted in Evidence parameters. Canonical contract keys, enums and provider identities remain intact.",
),
});
const pointerSchema = z
.string()
.describe("RFC 6901 pointer in the producer's reported object.");
const redactionSchema = z.union([
z.strictObject({
pointer: pointerSchema,
reason: z.enum(["transport-credential", "explicit-sensitive-value"]),
}),
z.strictObject({
pointer: pointerSchema,
reason: z.literal("explicit-sensitive-value"),
scope: z
.literal("property-name")
.describe(
"An omitted property name; pointer identifies its actual parent, not an invented child coordinate.",
),
}),
]);
const binaryBase = {
pointer: pointerSchema,
representation: z.enum(["producer-bytes", "har-base64-content"]),
};
const binarySchema = z.discriminatedUnion("state", [
z.strictObject({
...binaryBase,
state: z.literal("retained"),
content_base64: z.string(),
bytes: z.number().int().nonnegative(),
sha256: z.string().regex(/^[0-9a-f]{64}$/),
}),
z.strictObject({
...binaryBase,
state: z.literal("redacted"),
content_base64: z.null(),
bytes: z.null(),
sha256: z.null(),
}),
]);
const numberSchema = z.strictObject({
pointer: pointerSchema,
producer_type: z.enum(["json-number", "integer", "float"]),
literal: z.string(),
});
/** Preserve producer representations with independent sidecars rather than invented browser IDs. */
export const webNetworkCaptureRecordSchema = z.strictObject({
ordinal: z.number().int().nonnegative(),
location: z.discriminatedUnion("kind", [
z.strictObject({
kind: z.literal("unknown"),
reason: z.literal("explicit-sensitive-value"),
}),
z.strictObject({ kind: z.literal("json-pointer"), pointer: pointerSchema }),
z.strictObject({
kind: z.literal("byte-range"),
offset: z.number().int().nonnegative(),
bytes: z.number().int().positive(),
}),
]),
reported: jsonValueSchema,
binary_fields: z.array(binarySchema),
numeric_literals: z.array(numberSchema),
redactions: z.array(redactionSchema),
limitations: z.array(z.string()),
});
/** Historical capture inspection has file identity, producer provenance and explicit unknowns. */
export const webNetworkCaptureSchema = z.strictObject({
artifact: z.strictObject({
path: z
.string()
.describe(
"Selected artifact path; empty when explicitly excluded from Evidence.",
),
sha256: z
.string()
.regex(/^[0-9a-f]{64}$/)
.nullable()
.describe(
"Observed artifact SHA-256; null only when explicitly excluded by a sensitive declaration.",
),
bytes: z.number().int().nonnegative(),
}),
format: z.enum(["har", "mitmproxy"]),
decoder: z.strictObject({
id: z.string().min(1),
name: z.string().min(1),
version: z.string().min(1),
}),
container: z.strictObject({
reported: jsonValueSchema,
numeric_literals: z.array(numberSchema),
redactions: z.array(redactionSchema),
records_pointer: pointerSchema.nullable(),
}),
total_records: z.number().int().nonnegative(),
records: z.array(webNetworkCaptureRecordSchema),
runtime_attribution: z.literal("unknown"),
limitations: z.array(z.string()),
});
export type InspectWebNetworkCaptureInput = z.output<
typeof inspectWebNetworkCaptureInputSchema
>;
export type WebNetworkCapture = z.output<typeof webNetworkCaptureSchema>;
export type WebNetworkCaptureRecord = z.output<
typeof webNetworkCaptureRecordSchema
>;