fix: harden agent profile boundaries

This commit is contained in:
Federico Jaramillo Martinez
2026-07-13 23:39:49 +02:00
parent 97e0afc6fa
commit adc2e297a4
25 changed files with 419 additions and 150 deletions
+25 -1
View File
@@ -10,6 +10,8 @@ const ACTIVE_AGENT_PROFILE_FIELDS = new Set([
"sessionDirEnvKeys",
]);
const SHA256_REVISION_PATTERN = /^sha256:[0-9a-f]{64}$/u;
const SAFE_BARE_AGENT_COMMAND_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9._+-]*$/u;
const ACTIVE_SESSION_DIR_ENV_KEYS = new Set(["PI_WEB_AGENT_SESSION_DIR", "PI_CODING_AGENT_SESSION_DIR"]);
export function parseActiveAgentProfileDescriptor(value: unknown): ActiveAgentProfileDescriptor | undefined {
if (!isRecord(value) || Object.keys(value).some((key) => !ACTIVE_AGENT_PROFILE_FIELDS.has(key))) return undefined;
@@ -21,9 +23,11 @@ export function parseActiveAgentProfileDescriptor(value: unknown): ActiveAgentPr
const sessionDirEnvKeys = value["sessionDirEnvKeys"];
if (schemaVersion !== ACTIVE_AGENT_PROFILE_SCHEMA_VERSION) return undefined;
if (typeof revision !== "string" || !SHA256_REVISION_PATTERN.test(revision)) return undefined;
if (typeof command !== "string" || command === "" || typeof dir !== "string" || dir === "") return undefined;
if (typeof command !== "string" || !isPortableAgentCommand(command)) return undefined;
if (typeof dir !== "string" || !isPortableAbsolutePath(dir)) return undefined;
if (!isNonEmptyStringArray(sessionDirEnvKeys)) return undefined;
if (new Set(sessionDirEnvKeys).size !== sessionDirEnvKeys.length) return undefined;
if (sessionDirEnvKeys[0] !== "PI_WEB_AGENT_SESSION_DIR" || sessionDirEnvKeys.some((key) => !ACTIVE_SESSION_DIR_ENV_KEYS.has(key))) return undefined;
return Object.freeze({
schemaVersion,
@@ -34,6 +38,26 @@ export function parseActiveAgentProfileDescriptor(value: unknown): ActiveAgentPr
});
}
function isPortableAgentCommand(value: string): boolean {
if (value !== value.trim() || /[\s;&|`$<>]/u.test(value)) return false;
if (isPortableAbsolutePath(value)) return !value.endsWith("/") && !value.endsWith("\\");
return SAFE_BARE_AGENT_COMMAND_PATTERN.test(value);
}
function isPortableAbsolutePath(value: string): boolean {
if (value === "" || value !== value.trim() || hasControlCharacter(value)) return false;
const withForwardSlashes = value.replace(/\\/g, "/");
return withForwardSlashes.startsWith("/") || /^[A-Za-z]:\//u.test(withForwardSlashes);
}
function hasControlCharacter(value: string): boolean {
for (const character of value) {
const code = character.charCodeAt(0);
if (code < 32 || code === 127) return true;
}
return false;
}
function isNonEmptyStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every((entry: unknown) => typeof entry === "string" && entry !== "");
}
+3
View File
@@ -80,6 +80,9 @@ describe("PI WEB status parsing", () => {
});
expect(parsePiWebRuntimeResponse(responseFor(undefined, { ...profile, token: "secret" }))).toBeUndefined();
expect(parsePiWebRuntimeResponse(responseFor(undefined, { ...profile, command: "./acme-agent" }))).toBeUndefined();
expect(parsePiWebRuntimeResponse(responseFor(undefined, { ...profile, dir: "relative/state" }))).toBeUndefined();
expect(parsePiWebRuntimeResponse(responseFor(undefined, { ...profile, sessionDirEnvKeys: ["ARBITRARY_AGENT_SESSION_DIR"] }))).toBeUndefined();
expect(parsePiWebRuntimeResponse(responseFor(profile, undefined))).toBeUndefined();
});