chore(telemetry): replace fragile regex path normalization with bulletproof path.posix.resolve
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user