From e47b03807c1b27d15c2fefa22d620efa750ee78f Mon Sep 17 00:00:00 2001 From: harshitsinghbhandari <24b4506@iitb.ac.in> Date: Fri, 10 Apr 2026 12:16:37 +0530 Subject: [PATCH] fix(agent-cursor): address Cursor Bugbot security findings 1. Add symlink check for .cursor directory in extractCursorSummary to match getCursorSessionMtime behavior (prevents path traversal) 2. Add vitest alias for @aoagents/ao-plugin-agent-cursor in CLI tests (fixes missing module resolution in tests) 3. Add lstatSync check before readFileSync in getLaunchCommand to reject symlinked systemPromptFile paths (security hardening) 4. Add test coverage for symlink rejection behavior Co-Authored-By: Claude --- packages/cli/vitest.config.ts | 4 ++++ .../plugins/agent-cursor/src/index.test.ts | 15 ++++++++++++- packages/plugins/agent-cursor/src/index.ts | 21 ++++++++++++++----- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/packages/cli/vitest.config.ts b/packages/cli/vitest.config.ts index 6c9b37434..cf0b05683 100644 --- a/packages/cli/vitest.config.ts +++ b/packages/cli/vitest.config.ts @@ -47,6 +47,10 @@ export default defineConfig({ find: "@aoagents/ao-plugin-agent-opencode", replacement: resolve(__dirname, "../plugins/agent-opencode/src/index.ts"), }, + { + find: "@aoagents/ao-plugin-agent-cursor", + replacement: resolve(__dirname, "../plugins/agent-cursor/src/index.ts"), + }, { find: "@aoagents/ao-plugin-scm-github", replacement: resolve(__dirname, "../plugins/scm-github/src/index.ts"), diff --git a/packages/plugins/agent-cursor/src/index.test.ts b/packages/plugins/agent-cursor/src/index.test.ts index 550da2d87..a962667b3 100644 --- a/packages/plugins/agent-cursor/src/index.test.ts +++ b/packages/plugins/agent-cursor/src/index.test.ts @@ -14,8 +14,9 @@ vi.mock("node:fs/promises", async (importOriginal) => { }); // Mock fs (sync) for getLaunchCommand systemPromptFile tests -const { mockReadFileSync } = vi.hoisted(() => ({ +const { mockReadFileSync, mockLstatSync } = vi.hoisted(() => ({ mockReadFileSync: vi.fn(), + mockLstatSync: vi.fn().mockReturnValue({ isSymbolicLink: () => false }), })); vi.mock("node:fs", async (importOriginal) => { @@ -23,6 +24,7 @@ vi.mock("node:fs", async (importOriginal) => { return { ...actual, readFileSync: mockReadFileSync, + lstatSync: mockLstatSync, }; }); @@ -256,6 +258,17 @@ describe("getLaunchCommand", () => { ); expect(cmd).toBe("agent -- 'Do the task'"); }); + + it("rejects symlinked systemPromptFile for security", () => { + mockLstatSync.mockReturnValueOnce({ isSymbolicLink: () => true }); + mockReadFileSync.mockReturnValueOnce("Should not be read"); + const cmd = agent.getLaunchCommand( + makeLaunchConfig({ systemPromptFile: "/path/to/symlink.txt", prompt: "Do the task" }), + ); + // Should skip the symlinked file and only include the prompt + expect(cmd).toBe("agent -- 'Do the task'"); + expect(mockReadFileSync).not.toHaveBeenCalled(); + }); }); // ========================================================================= diff --git a/packages/plugins/agent-cursor/src/index.ts b/packages/plugins/agent-cursor/src/index.ts index 407276f6b..0af97d495 100644 --- a/packages/plugins/agent-cursor/src/index.ts +++ b/packages/plugins/agent-cursor/src/index.ts @@ -24,7 +24,7 @@ import { import { execFile, execFileSync } from "node:child_process"; import { promisify } from "node:util"; import { stat, access, readFile, lstat } from "node:fs/promises"; -import { readFileSync, constants } from "node:fs"; +import { readFileSync, lstatSync, constants } from "node:fs"; import { join, resolve } from "node:path"; const execFileAsync = promisify(execFile); @@ -101,10 +101,15 @@ async function extractCursorSummary(workspacePath: string): Promise