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 <noreply@anthropic.com>
This commit is contained in:
parent
2cd5dc1a1d
commit
e47b03807c
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
|
|
|
|||
|
|
@ -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<string | nul
|
|||
const chatFile = join(cursorDir, "chat.md");
|
||||
|
||||
try {
|
||||
// Security check: verify chatFile is not a symlink and stays under workspacePath
|
||||
// Security check: reject symlinks to prevent path traversal
|
||||
const dirStats = await lstat(cursorDir);
|
||||
if (dirStats.isSymbolicLink()) {
|
||||
return null; // Reject symlinked .cursor directory
|
||||
}
|
||||
|
||||
const lstats = await lstat(chatFile);
|
||||
if (lstats.isSymbolicLink()) {
|
||||
return null; // Reject symlinks to prevent path traversal
|
||||
return null; // Reject symlinked chat file
|
||||
}
|
||||
|
||||
// Verify the resolved path stays under workspacePath
|
||||
|
|
@ -177,8 +182,14 @@ function createCursorAgent(): Agent {
|
|||
// Read system prompt from file or inline config
|
||||
if (config.systemPromptFile) {
|
||||
try {
|
||||
const systemContent = readFileSync(config.systemPromptFile, "utf-8");
|
||||
promptText = systemContent.trim();
|
||||
// Security check: reject symlinks to prevent path traversal attacks
|
||||
const lstats = lstatSync(config.systemPromptFile);
|
||||
if (lstats.isSymbolicLink()) {
|
||||
// Skip symlinked system prompt files
|
||||
} else {
|
||||
const systemContent = readFileSync(config.systemPromptFile, "utf-8");
|
||||
promptText = systemContent.trim();
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist or can't be read - continue without system prompt
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue