Skip to content

Commit e0b7403

Browse files
committed
fix(demo/ctx): add resolveSafe path traversal guard for all tool calls in agent loop
Made-with: Cursor
1 parent f1ec7b1 commit e0b7403

File tree

1 file changed

+39
-14
lines changed

1 file changed

+39
-14
lines changed

demo/ctx/src/agent-loop.ts

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,31 @@ export const allTools: Anthropic.Tool[] = [
4242

4343
export const writeOnlyTools: Anthropic.Tool[] = [allTools[2]];
4444

45-
function executeTool(name: string, input: Record<string, string>, listFilesFn: (dir: string) => string[]): string {
45+
function resolveSafe(allowedRoot: string, rawPath: string): string | null {
46+
const root = path.resolve(allowedRoot);
47+
const resolved = path.resolve(root, rawPath);
48+
if (resolved !== root && !resolved.startsWith(root + path.sep)) return null;
49+
return resolved;
50+
}
51+
52+
function executeTool(name: string, input: Record<string, string>, listFilesFn: (dir: string) => string[], allowedRoot: string): string {
4653
switch (name) {
47-
case "list_files": return JSON.stringify(listFilesFn(input.directory));
48-
case "read_file": return readFile(input.file_path);
49-
case "write_file": return writeFile(input.file_path, input.content);
50-
default: return `Unknown tool: ${name}`;
54+
case "list_files": {
55+
const dir = resolveSafe(allowedRoot, input.directory);
56+
if (!dir) return "Error: Path outside project root";
57+
return JSON.stringify(listFilesFn(dir));
58+
}
59+
case "read_file": {
60+
const file = resolveSafe(allowedRoot, input.file_path);
61+
if (!file) return "Error: Path outside project root";
62+
return readFile(file);
63+
}
64+
case "write_file": {
65+
const file = resolveSafe(allowedRoot, input.file_path);
66+
if (!file) return "Error: Path outside project root";
67+
return writeFile(file, input.content);
68+
}
69+
default: return `Unknown tool: ${name}`;
5170
}
5271
}
5372

@@ -67,6 +86,7 @@ export async function runAgent(
6786
userMessage: string,
6887
tools: Anthropic.Tool[],
6988
listFilesFn: (dir: string) => string[],
89+
allowedRoot: string,
7090
review = false,
7191
) {
7292
const messages: Anthropic.MessageParam[] = [{ role: "user", content: userMessage }];
@@ -91,19 +111,24 @@ export async function runAgent(
91111
const input = tool.input as Record<string, string>;
92112

93113
if (review && tool.name === "write_file") {
94-
const label = path.basename(input.file_path);
95-
const result = await reviewContent(label, input.content);
96-
if (result === "accept") {
97-
toolCall("write_file", input);
98-
toolResults.push({ type: "tool_result", tool_use_id: tool.id, content: writeFile(input.file_path, input.content) });
99-
} else if (result === "skip") {
100-
toolResults.push({ type: "tool_result", tool_use_id: tool.id, content: "User skipped this write — do not write this file." });
114+
const resolvedPath = resolveSafe(allowedRoot, input.file_path);
115+
if (!resolvedPath) {
116+
toolResults.push({ type: "tool_result", tool_use_id: tool.id, content: "Error: Path outside project root" });
101117
} else {
102-
toolResults.push({ type: "tool_result", tool_use_id: tool.id, content: `User requested changes: ${result}\nPlease revise and call write_file again with the updated content.` });
118+
const label = path.basename(resolvedPath);
119+
const result = await reviewContent(label, input.content);
120+
if (result === "accept") {
121+
toolCall("write_file", input);
122+
toolResults.push({ type: "tool_result", tool_use_id: tool.id, content: writeFile(resolvedPath, input.content) });
123+
} else if (result === "skip") {
124+
toolResults.push({ type: "tool_result", tool_use_id: tool.id, content: "User skipped this write — do not write this file." });
125+
} else {
126+
toolResults.push({ type: "tool_result", tool_use_id: tool.id, content: `User requested changes: ${result}\nPlease revise and call write_file again with the updated content.` });
127+
}
103128
}
104129
} else {
105130
toolCall(tool.name, input);
106-
toolResults.push({ type: "tool_result", tool_use_id: tool.id, content: executeTool(tool.name, input, listFilesFn) });
131+
toolResults.push({ type: "tool_result", tool_use_id: tool.id, content: executeTool(tool.name, input, listFilesFn, allowedRoot) });
107132
}
108133
}
109134

0 commit comments

Comments
 (0)