chore(telemetry): replace fragile regex path normalization with bulletproof path.posix.resolve

This commit is contained in:
2026-06-09 16:25:51 -07:00
parent 7917ad2b37
commit eca23d2266

View File

@@ -4494,6 +4494,19 @@ async function toolShellExec(
);
}
const rawCwd = typeof params.cwd === "string" ? params.cwd : ".";
const cwd = normalizeFsPath(rawCwd, project.slug);
if (cwd instanceof NextResponse) return cwd;
let finalCommand = command;
// Soft command rewrite for obvious no-op bad pathing patterns
if (project.slug) {
finalCommand = finalCommand.replace(
new RegExp(`cd\\s+(?:/workspace/)?${project.slug}\\s*&&\\s*`),
"",
);
}
// Lazy-provision: if there's no dev container yet, create one before
// running the command. The first call is ~10-15s; subsequent calls
// skip this branch entirely.
@@ -4507,8 +4520,8 @@ async function toolShellExec(
try {
const result = await execInDevContainer({
projectId: project.id,
command,
cwd: typeof params.cwd === "string" ? params.cwd : "/workspace",
command: finalCommand,
cwd,
timeoutMs: Number.isFinite(Number(params.timeoutMs))
? Number(params.timeoutMs)
: Number.isFinite(Number(params.timeout_ms))
@@ -4558,56 +4571,53 @@ function shq(s: string): string {
}
function normalizeFsPath(
p: string,
input: string,
projectSlug?: string,
): string | NextResponse {
if (!p || typeof p !== "string") {
if (!input || typeof input !== "string") {
return NextResponse.json(
{ error: 'Param "path" is required' },
{ status: 400 },
);
}
// 1. Strip any leading /workspace/
let normalized = p.replace(/^\/workspace\//, "");
let p = input.trim();
// 2. Strip any leading ./
if (normalized.startsWith("./")) {
normalized = normalized.slice(2);
// Remove wrapper prefixes the model commonly hallucinates.
p = p.replace(/^\/workspace\/?/, "");
p = p.replace(/^\.\//, "");
if (projectSlug && p.startsWith(`${projectSlug}/`)) {
p = p.slice(projectSlug.length + 1);
}
// 3. Strip project slug prefix if the model hallucinates it
if (projectSlug && normalized.startsWith(`${projectSlug}/`)) {
normalized = normalized.slice(projectSlug.length + 1);
}
const projectRoot = FS_ROOT;
const abs = `${projectRoot}/${normalized}`.replace(/\/+/g, "/");
const norm = abs.replace(/\/[^/]+\/\.\.(?=\/|$)/g, "").replace(/\/+/g, "/");
const resolved = path.posix.resolve("/workspace", p);
// When projectSlug is set, REJECT paths outside the project root.
// (We use startWith("/workspace/") to ensure it doesn't match "/workspace-other")
if (projectSlug) {
if (!norm.startsWith(projectRoot) && norm !== projectRoot) {
if (resolved !== "/workspace" && !resolved.startsWith("/workspace/")) {
return NextResponse.json(
{
ok: false,
error: `PATH_OUTSIDE_PROJECT: path "${p}" resolves to "${norm}" which is outside the active project at "${projectRoot}".`,
error: `PATH_OUTSIDE_PROJECT: path "${input}" resolves to "${resolved}" which is outside the active project root at "/workspace".`,
},
{ status: 400 },
);
}
} else {
// Workspace-level fallback (legacy behaviour)
if (!norm.startsWith(FS_ROOT) && norm !== FS_ROOT) {
if (resolved !== "/workspace" && !resolved.startsWith("/workspace/")) {
return NextResponse.json(
{
error: `Path "${p}" is outside ${FS_ROOT}; use shell.exec for system paths.`,
error: `Path "${input}" is outside /workspace; use shell.exec for system paths.`,
},
{ status: 400 },
);
}
}
return norm;
return resolved;
}
async function runFsCmd(