Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions src/tools/file.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createTwoFilesPatch } from "diff";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { dirname, isAbsolute, resolve } from "path";
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "fs";
import { dirname, isAbsolute, relative, resolve } from "path";
import { summarizeDiagnostics, syncFileWithLsp } from "../lsp/runtime";
import type { LspDiagnosticFile } from "../lsp/types";

Expand All @@ -19,7 +19,39 @@ export interface FileResult {
lspDiagnostics?: LspDiagnosticFile[];
}

function safeRealpath(p: string): string {
try {
return realpathSync(p);
} catch {
return resolve(p);
}
}

/**
* Ensure a user/agent-supplied path cannot escape the workspace, whether via
* lexical traversal (e.g. `../../.ssh/id_rsa`) or a symlink that crosses
* outside the workspace root. Mirrors the directory-prefix guards applied
* elsewhere in this repo (e.g. `assertInsideSchedulesDir`). Throws on escape
* so the calling tool surfaces a clean failure instead of touching the host.
*/
function assertInsideWorkspace(filePath: string, cwd: string): void {
const root = safeRealpath(cwd);
const full = isAbsolute(filePath) ? filePath : resolve(cwd, filePath);

const rel = relative(root, full);
if (rel.startsWith("..") || isAbsolute(rel)) {
throw new Error(`Path "${filePath}" resolves outside the workspace and was rejected.`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dot-dot filename false reject

Low Severity

Escape detection uses rel.startsWith(".."), which also matches legitimate relative paths whose first segment is a filename starting with two dots (for example ..foo), so those in-workspace files are rejected incorrectly.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 33e1057. Configure here.

}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cwd symlink false rejection

Medium Severity

The workspace root is taken from safeRealpath(cwd) while the candidate path uses resolve(cwd, filePath). path.relative compares string paths and ignores symlinks, so when the agent’s cwd is a symlink into the workspace, valid relative paths can look like ../… and be rejected even though they refer to files inside the workspace.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 33e1057. Configure here.


const realTarget = safeRealpath(full);
const realRel = relative(root, realTarget);
if (realRel.startsWith("..") || isAbsolute(realRel)) {
throw new Error(`Path "${filePath}" points outside the workspace via a symlink and was rejected.`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Symlink bypass on new paths

High Severity

When the target path does not exist yet, safeRealpath falls back to resolve, which does not follow symlinks in existing parent segments. A path that stays lexically under the workspace can still pass both checks if a directory component is a symlink outside the workspace, so writeFile can create files on the host.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 33e1057. Configure here.

}

function resolvePath(filePath: string, cwd: string): string {
assertInsideWorkspace(filePath, cwd);
return isAbsolute(filePath) ? filePath : resolve(cwd, filePath);
}

Expand Down
Loading