fix: reject PTY environment names the backend cannot preserve

This commit is contained in:
morluto
2026-10-24 02:26:54 +08:00
parent c10a360cab
commit d3305887cb
3 changed files with 79 additions and 1 deletions
+5
View File
@@ -12,6 +12,11 @@ sandbox. Filesystem observation paths select what REA snapshots; they do not
restrict what the process can read or write. The inherited environment is not
recorded, so its influence may remain unknown.
The installed PTY backend cannot preserve an explicit environment name
`__proto__`. REA rejects that override before launch; use `/usr/bin/env` with
`__proto__=VALUE` as an argument when that literal variable is required. The
reserved `REA_PROCESS_RUN_ID` name also remains unavailable to scenario overrides.
## Host support
Capturing a new scenario currently requires Linux or macOS and a working
@@ -1,4 +1,6 @@
import { expect, it } from "vitest";
import { z } from "zod";
import { Ajv2020 } from "ajv/dist/2020.js";
import { SESSION_TOOL_CONTRACTS } from "../../contracts/sessionToolContracts.js";
import {
@@ -12,6 +14,23 @@ import {
const baseScenario = { executable: "/usr/bin/true" };
it("advertises the same unsupported PTY environment name as runtime admission", () => {
const schema = z.toJSONSchema(processScenarioSchema.shape.environment, {
io: "input",
});
const ajv = new Ajv2020({ strict: false });
const validate = ajv.compile(schema);
expect(validate(Object.fromEntries([["__proto__", "selected"]]))).toBe(false);
expect(
validate({
constructor: "ordinary",
prototype: "ordinary",
NORMAL_CAPTURE_KEY: "control",
}),
).toBe(true);
expect(validate({ REA_PROCESS_RUN_ID: "reserved" })).toBe(false);
});
it("defaults the finalization interval to zero without changing committed identity", () => {
const scenario = parseProcessScenario(baseScenario);
@@ -237,3 +256,26 @@ it("identifies each event scheduled after the scenario timeout", () => {
}),
]);
});
it.each(["selected-value", 7])(
"rejects a selected __proto__ PTY environment value instead of omitting %j",
(value) => {
const environment = Object.fromEntries([
["__proto__", value],
["NORMAL_CAPTURE_KEY", "control"],
]);
const parsed = processScenarioSchema.safeParse({
...baseScenario,
environment,
});
expect(parsed.success).toBe(false);
if (parsed.success)
throw new Error("Expected the unsupported PTY name to be rejected");
expect(parsed.error.issues).toContainEqual(
expect.objectContaining({
path: ["environment", "__proto__"],
message: expect.stringContaining("PTY backend cannot preserve"),
}),
);
},
);
+32 -1
View File
@@ -45,12 +45,43 @@ const environmentName = z
? `${reservedRunIdEnvironmentName} is reserved by the process adapter`
: "Environment names cannot contain '=' or NUL",
});
const unsupportedPtyEnvironmentName = "__proto__";
const ptyEnvironmentName = environmentName.regex(
environmentNamePattern(unsupportedPtyEnvironmentName),
{
error:
"The installed PTY backend cannot preserve the __proto__ environment name; pass it through /usr/bin/env instead.",
},
);
const childProcessString = z
.string()
.regex(
/^[^\x00]*$/u,
"Values passed to operating-system APIs cannot contain NUL",
);
const environmentRecordSchema = z.record(
ptyEnvironmentName,
childProcessString,
);
// z.record intentionally skips this key, so validate its presence before parsing.
const processEnvironmentSchema = z
.unknown()
.superRefine((value, context) => {
if (
value !== null &&
typeof value === "object" &&
Object.hasOwn(value, unsupportedPtyEnvironmentName)
)
context.addIssue({
code: "custom",
path: [unsupportedPtyEnvironmentName],
message:
"The installed PTY backend cannot preserve the __proto__ environment name; pass it through /usr/bin/env instead.",
});
})
.pipe(environmentRecordSchema);
processEnvironmentSchema._zod.toJSONSchema = () =>
z.toJSONSchema(environmentRecordSchema, { io: "input" });
const fitsProcessTerminalCellBudget = (
columns: number,
@@ -153,7 +184,7 @@ export const processScenarioSchema = z
executable: childProcessString.min(1),
arguments: z.array(childProcessString).default([]),
working_directory: childProcessString.default("."),
environment: z.record(environmentName, childProcessString).default({}),
environment: processEnvironmentSchema.default({}),
filesystem_observation_paths: z
.array(childProcessString.min(1))
.default([]),