Compare commits

...

1 Commits

Author SHA1 Message Date
abeatrix 05ecd36ff1 fix(example): validate handoff paths before resolving
Add explicit handoff path validation for empty, absolute, overly long,
invalid, or parent-directory paths. Use path-relative containment checks to
ensure resolved handoff files cannot escape the handoffs directory.

Remove the regex field from the zod schema as it doesn't work with openai codex provider
2026-05-22 14:58:43 -07:00
+38 -6
View File
@@ -5,7 +5,7 @@ import {
readFileSync,
writeFileSync,
} from "node:fs";
import { dirname, join, resolve } from "node:path";
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
import {
type AgentPlugin,
@@ -90,6 +90,8 @@ const GLOBAL_SKILLS_DIR = join(resolveClineDataDirPath(), "settings", "skills");
/** Safe identifier pattern for conversation IDs used in filesystem paths. */
const SAFE_ID_RE = /^[A-Za-z0-9_-]+$/;
const HANDOFF_PATH_ALLOWED_RE = /^[A-Za-z0-9._/-]+$/;
const HANDOFF_PATH_MAX_LENGTH = 240;
const envOr = (key: string, fallback: string): string =>
process.env[key]?.trim() || fallback;
@@ -298,14 +300,45 @@ function resolveHandoffPath(
ctx: AgentToolContext,
relativePath: string,
): string {
const handoffPath = validateHandoffRelativePath(relativePath);
const dir = handoffsDir(ctx);
const resolved = resolve(dir, relativePath);
if (!resolved.startsWith(`${dir}/`)) {
const resolved = resolve(dir, handoffPath);
const pathFromHandoffsDir = relative(dir, resolved);
if (
!pathFromHandoffsDir ||
pathFromHandoffsDir === ".." ||
pathFromHandoffsDir.startsWith(`..${sep}`) ||
isAbsolute(pathFromHandoffsDir)
) {
throw new Error(`Handoff path escapes directory: ${relativePath}`);
}
return resolved;
}
function validateHandoffRelativePath(relativePath: string): string {
const trimmed = relativePath.trim();
if (!trimmed) {
throw new Error("Handoff path must not be empty");
}
if (trimmed.length > HANDOFF_PATH_MAX_LENGTH) {
throw new Error(
`Handoff path must be ${HANDOFF_PATH_MAX_LENGTH} characters or fewer`,
);
}
if (trimmed.startsWith("/")) {
throw new Error(`Handoff path must be relative: ${relativePath}`);
}
if (!HANDOFF_PATH_ALLOWED_RE.test(trimmed)) {
throw new Error(
"Use a relative file path with letters, numbers, '.', '_', '-', or '/'.",
);
}
if (trimmed.split("/").includes("..")) {
throw new Error(`Handoff path must not contain '..': ${relativePath}`);
}
return trimmed;
}
function emitSteer(sessionId: string | undefined, prompt: string): void {
if (sessionId && prompt.trim()) {
globalThis.__clinePluginHost?.emitEvent?.("steer_message", {
@@ -407,9 +440,8 @@ const HandoffPathInput = z
.trim()
.min(1)
.max(240)
.regex(
/^(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$))[A-Za-z0-9._/-]+$/,
"Use a relative file path with letters, numbers, '.', '_', '-', or '/'.",
.describe(
"Relative file path using letters, numbers, '.', '_', '-', or '/'. Must not be absolute or contain '..' segments.",
);
const StartSubagentInput = z