diff --git a/typescript/agentkit/src/action-providers/twitter/twitterActionProvider.test.ts b/typescript/agentkit/src/action-providers/twitter/twitterActionProvider.test.ts index a054289a3..cdbaf8e73 100644 --- a/typescript/agentkit/src/action-providers/twitter/twitterActionProvider.test.ts +++ b/typescript/agentkit/src/action-providers/twitter/twitterActionProvider.test.ts @@ -1,6 +1,9 @@ import { TwitterApi, TwitterApiv2, TwitterApiv1 } from "twitter-api-v2"; import { TwitterActionProvider } from "./twitterActionProvider"; import { TweetUserMentionTimelineV2Paginator } from "twitter-api-v2"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; const MOCK_CONFIG = { apiKey: "test-api-key", @@ -16,7 +19,6 @@ const MOCK_TWEET = "Hello, world!"; const MOCK_TWEET_ID = "0123456789012345678"; const MOCK_TWEET_REPLY = "Hello again!"; const MOCK_MEDIA_ID = "987654321"; -const MOCK_FILE_PATH = "/path/to/image.jpg"; describe("TwitterActionProvider", () => { let mockClient: jest.Mocked; @@ -278,14 +280,27 @@ describe("TwitterActionProvider", () => { }); describe("Upload Media Action", () => { + let tmpDir: string; + let prevCwd: string; + const fileName = "image.jpg"; + beforeEach(() => { mockUploadMedia.mockResolvedValue(MOCK_MEDIA_ID); + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "twitter-upload-")); + prevCwd = process.cwd(); + process.chdir(tmpDir); + fs.writeFileSync(path.join(tmpDir, fileName), "x"); + }); + + afterEach(() => { + process.chdir(prevCwd); + fs.rmSync(tmpDir, { recursive: true, force: true }); }); it("should successfully upload media", async () => { - const response = await provider.uploadMedia({ filePath: MOCK_FILE_PATH }); + const response = await provider.uploadMedia({ filePath: fileName }); - expect(mockUploadMedia).toHaveBeenCalledWith(MOCK_FILE_PATH); + expect(mockUploadMedia).toHaveBeenCalledWith(fs.realpathSync(path.join(tmpDir, fileName))); expect(response).toContain("Successfully uploaded media to Twitter"); expect(response).toContain(MOCK_MEDIA_ID); }); @@ -294,12 +309,20 @@ describe("TwitterActionProvider", () => { const error = new Error("Invalid file format"); mockUploadMedia.mockRejectedValue(error); - const response = await provider.uploadMedia({ filePath: MOCK_FILE_PATH }); + const response = await provider.uploadMedia({ filePath: fileName }); - expect(mockUploadMedia).toHaveBeenCalledWith(MOCK_FILE_PATH); + expect(mockUploadMedia).toHaveBeenCalledWith(fs.realpathSync(path.join(tmpDir, fileName))); expect(response).toContain("Error uploading media to Twitter"); expect(response).toContain(error.message); }); + + it("should reject paths outside the working directory", async () => { + const response = await provider.uploadMedia({ filePath: "/etc/passwd" }); + + expect(mockUploadMedia).not.toHaveBeenCalled(); + expect(response).toContain("Error uploading media to Twitter"); + expect(response).toMatch(/working directory/i); + }); }); describe("Network Support", () => { diff --git a/typescript/agentkit/src/action-providers/twitter/twitterActionProvider.ts b/typescript/agentkit/src/action-providers/twitter/twitterActionProvider.ts index f2334feab..8eb5b92f5 100644 --- a/typescript/agentkit/src/action-providers/twitter/twitterActionProvider.ts +++ b/typescript/agentkit/src/action-providers/twitter/twitterActionProvider.ts @@ -10,6 +10,7 @@ import { TwitterPostTweetReplySchema, TwitterUploadMediaSchema, } from "./schemas"; +import { resolveSafeLocalMediaPath } from "./utils"; /** * Configuration options for the TwitterActionProvider. @@ -234,7 +235,8 @@ A failure response will return a message with the Twitter API request error: }) async uploadMedia(args: z.infer): Promise { try { - const mediaId = await this.getClient().v1.uploadMedia(args.filePath); + const safePath = resolveSafeLocalMediaPath(args.filePath); + const mediaId = await this.getClient().v1.uploadMedia(safePath); return `Successfully uploaded media to Twitter: ${mediaId}`; } catch (error) { return `Error uploading media to Twitter: ${error}`; diff --git a/typescript/agentkit/src/action-providers/twitter/utils.test.ts b/typescript/agentkit/src/action-providers/twitter/utils.test.ts new file mode 100644 index 000000000..6ad562465 --- /dev/null +++ b/typescript/agentkit/src/action-providers/twitter/utils.test.ts @@ -0,0 +1,23 @@ +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; + +import { resolveSafeLocalMediaPath } from "./utils"; + +describe("resolveSafeLocalMediaPath", () => { + it("allows files under cwd and rejects escapes", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "twitter-media-")); + const prev = process.cwd(); + try { + process.chdir(tmp); + const inside = path.join(tmp, "image.jpg"); + fs.writeFileSync(inside, "x"); + expect(resolveSafeLocalMediaPath("image.jpg")).toBe(fs.realpathSync(inside)); + expect(() => resolveSafeLocalMediaPath("../outside.jpg")).toThrow(/working directory/i); + expect(() => resolveSafeLocalMediaPath("/etc/passwd")).toThrow(/working directory/i); + } finally { + process.chdir(prev); + fs.rmSync(tmp, { recursive: true, force: true }); + } + }); +}); diff --git a/typescript/agentkit/src/action-providers/twitter/utils.ts b/typescript/agentkit/src/action-providers/twitter/utils.ts new file mode 100644 index 000000000..3ab11b39c --- /dev/null +++ b/typescript/agentkit/src/action-providers/twitter/utils.ts @@ -0,0 +1,20 @@ +import fs from "fs"; +import path from "path"; + +/** + * Resolve a local media path and require it to stay under process.cwd() + * (realpath), so agent-supplied paths cannot read arbitrary files for + * Twitter media upload. Twin of zora/flaunch resolveSafeLocalImagePath. + */ +export function resolveSafeLocalMediaPath(filePath: string): string { + const root = fs.realpathSync(process.cwd()); + const resolved = path.resolve(root, filePath); + if (resolved !== root && !resolved.startsWith(root + path.sep)) { + throw new Error("Local media path must be within the working directory"); + } + const real = fs.realpathSync(resolved); + if (real !== root && !real.startsWith(root + path.sep)) { + throw new Error("Local media path escapes the working directory"); + } + return real; +}