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
+9 -33
View File
@@ -1,5 +1,5 @@
import type { FastifyInstance } from "fastify";
import { hasAgentDirEnvOverride, hasAgentSessionDirEnvOverride, loadPiWebConfig, parseUploadsConfig, resolveEffectivePiWebConfig, savePiWebConfig, type LoadOptions, type PiWebConfig } from "../config.js";
import { hasAgentDirEnvOverride, hasAgentSessionDirEnvOverride, loadPiWebConfig, parseAgentConfig, parseUploadsConfig, resolveEffectivePiWebConfig, savePiWebConfig, type AgentPathHost, type LoadOptions, type PiWebConfig } from "../config.js";
import type { PiWebConfigEnvOverrides, PiWebConfigResponse, PiWebConfigValues } from "../shared/apiTypes.js";
import { isPiWebPluginId } from "../shared/pluginIds.js";
@@ -83,13 +83,13 @@ export function registerLocalMachineConfigRoutes(app: FastifyInstance, service:
});
}
export function parseSelectedMachineConfigRequest(value: unknown): PiWebConfig {
export function parseSelectedMachineConfigRequest(value: unknown, agentPathHost: AgentPathHost = "current"): PiWebConfig {
if (!isRecord(value)) throw new Error("PI WEB selected-machine config update must include a config object");
for (const key of Object.keys(value)) {
if (!SELECTED_MACHINE_CONFIG_KEY_SET.has(key)) throw new Error(`PI WEB selected-machine config key is not allowed: ${key}`);
}
try {
return pickSelectedMachineConfig(parseConfigRequest(value));
return pickSelectedMachineConfig(parseConfigRequest(value, agentPathHost));
} catch (error) {
throw new Error(selectedMachineConfigErrorMessage(error), { cause: error });
}
@@ -112,13 +112,13 @@ export function parsePiWebConfigResponseBody(value: unknown, source = "PI WEB co
return {
path: requireResponseString(record, "path", source),
exists: requireResponseBoolean(record, "exists", source),
config: parseConfigRequest(record["config"]),
effectiveConfig: parseConfigRequest(record["effectiveConfig"]),
config: parseConfigRequest(record["config"], "portable"),
effectiveConfig: parseConfigRequest(record["effectiveConfig"], "portable"),
envOverrides: parsePiWebConfigEnvOverridesResponse(record["envOverrides"], source),
};
}
function parseConfigRequest(value: unknown): PiWebConfig {
function parseConfigRequest(value: unknown, agentPathHost: AgentPathHost = "current"): PiWebConfig {
if (!isRecord(value)) throw new Error("PI WEB config update must include a config object");
const config: PiWebConfig = {};
const host = value["host"];
@@ -154,7 +154,7 @@ function parseConfigRequest(value: unknown): PiWebConfig {
if (typeof subsessions !== "boolean") throw new Error("PI WEB config subsessions must be a boolean");
config.subsessions = subsessions;
}
if (agent !== undefined) config.agent = parseAgentRequest(agent);
if (agent !== undefined) config.agent = parseAgentRequest(agent, agentPathHost);
return config;
}
@@ -216,28 +216,8 @@ function parseMaxUploadBytesRequest(value: unknown): number {
return value;
}
function parseAgentRequest(value: unknown): NonNullable<PiWebConfig["agent"]> {
if (!isRecord(value)) throw new Error("PI WEB config agent must be an object");
const command = value["command"];
const dir = value["dir"];
return {
...(command === undefined ? {} : { command: parseAgentCommandRequest(command) }),
...(dir === undefined ? {} : { dir: parseAgentDirRequest(dir) }),
};
}
function parseAgentCommandRequest(value: unknown): string {
if (typeof value !== "string" || value.trim() === "") throw new Error("PI WEB config agent.command must be a non-empty string");
const command = value.trim();
if (/[\s;&|`$<>]/u.test(command)) throw new Error("PI WEB config agent.command must be a single command name or path without shell metacharacters");
return command;
}
function parseAgentDirRequest(value: unknown): string {
if (typeof value !== "string" || value.trim() === "") throw new Error("PI WEB config agent.dir must be a non-empty string");
const dir = value.trim();
if (!isAbsoluteOrHomePath(dir)) throw new Error("PI WEB config agent.dir must be an absolute path or start with ~");
return dir;
function parseAgentRequest(value: unknown, pathHost: AgentPathHost): NonNullable<PiWebConfig["agent"]> {
return parseAgentConfig(value, "request", pathHost);
}
function parsePluginsRequest(value: unknown): NonNullable<PiWebConfig["plugins"]> {
@@ -317,10 +297,6 @@ function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function isAbsoluteOrHomePath(value: string): boolean {
return value === "~" || value.startsWith("~/") || value.startsWith("~\\") || value.startsWith("/") || value.startsWith("\\") || /^[A-Za-z]:[\\/]/u.test(value);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}