# Vulnerability Summary ## Overview This vulnerability involves enforcing restrictions on the ACP (Attachment Control Policy) attachment root directory. Specific manifestations include: - **Enforced restriction on ACP attachment root directory**: Ensures ACP attachments can only reside within permitted root directories. - **Hardening of ACP attachment root directory**: Strengthens the security of the ACP attachment root directory. - **Hardening of ACP attachment cache reading**: Enhances the security of reading from the ACP attachment cache. - **Clarification of ACP attachment skip logging**: Clearly defines log recording when ACP attachments are skipped. - **Preservation of ACP attachment paths**: Maintains the integrity of ACP attachment paths. ## Impact Scope - **Affected files**: - `src/auto-reply/reply/dispatch-attachment.test.ts` - `src/auto-reply/reply/dispatch-attachment.ts` - `src/media-understanding/attachments.cache.ts` ## Remediation - **Code changes**: - Added multiple test cases in `dispatch-attachment.test.ts` to ensure ACP attachments can only be within permitted root directories. - Added the `normalizeAttachmentPath` function in `dispatch-attachment.ts` to standardize attachment paths, ensuring they remain within allowed root directories. - Added the `MediaAttachmentCache` class in `attachments.cache.ts` to cache attachments and perform security checks during reading. ### POC Code ```typescript // src/auto-reply/reply/dispatch-attachment.test.ts it("skips ACP attachments outside allowed inbound roots", async () => { setReadyAcpResolution(); const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-accp")); const imagePath = path.join(tempDir, "outside-root.png"); try { await fs.writeFile(imagePath, "image-bytes"); managerMocks.runTurn.mockResolvedValue(undefined); await runDispatch({ bodyForAgent: " ", ctxOverrides: { MediaPath: imagePath, MediaType: "image/png", }, }); expect(managerMocks.runTurn).not.toHaveBeenCalled(); } finally { await fs.rm(tempDir, { recursive: true, force: true }); } }); it("skips file URL ACP attachments outside allowed inbound roots", async () => { setReadyAcpResolution(); const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-accp")); const imagePath = path.join(tempDir, "outside-root.png"); try { await fs.writeFile(imagePath, "image-bytes"); managerMocks.runTurn.mockResolvedValue(undefined); await runDispatch({ bodyForAgent: " ", ctxOverrides: { MediaPath: `file://${imagePath}`, MediaType: "image/png", }, }); expect(managerMocks.runTurn).not.toHaveBeenCalled(); } finally { await fs.rm(tempDir, { recursive: true, force: true }); } }); it("skips relative ACP attachment paths that resolve outside allowed inbound roots", async () => { setReadyAcpResolution(); const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-accp")); const imagePath = path.join(tempDir, "outside-root.png"); try { await fs.writeFile(imagePath, "image-bytes"); managerMocks.runTurn.mockResolvedValue(undefined); await runDispatch({ bodyForAgent: " ", ctxOverrides: { MediaPath: path.relative(process.cwd(), imagePath), MediaType: "image/png", }, }); expect(managerMocks.runTurn).not.toHaveBeenCalled(); } finally { await fs.rm(tempDir, { recursive: true, force: true }); } }); it("does not fall back to remote URLs when ACP local attachment paths are blocked", async () => { setReadyAcpResolution(); const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-accp")); const imagePath = path.join(tempDir, "outside-root.png"); const fetchSpy = vi.fn(); async () => { new Response(Buffer.from("remote-image"), { headers: { "content-type": "image/png", }, }); }; globalThis.fetch = withFetchPreconnect(fetchSpy as typeof fetch); try { await fs.writeFile(imagePath, "image-bytes"); managerMocks.runTurn.mockResolvedValue(undefined); await runDispatch({ bodyForAgent: " ", ctxOverrides: { MediaPath: imagePath, MediaUrl: "https://example.com/image.png", MediaType: "image/png", }, }); expect(fetchSpy).not.toHaveBeenCalled(); expect(managerMocks.runTurn).not.toHaveBeenCalled(); } finally { await fs.rm(tempDir, { recursive: true, force: true }); } }); it("skips ACP turns for non-image attachments when there is no text prompt", async () => { setReadyAcpResolution(); const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "dispatch-accp")); const imagePath = path.join(tempDir, "outside-root.png"); try { await fs.writeFile(imagePath, "image-bytes"); managerMocks.runTurn.mockResolvedValue(undefined); await runDispatch({ bodyForAgent: " ", ctxOverrides: { MediaPath: imagePath, MediaType: "image/png", }, }); expect(managerMocks.runTurn).not.toHaveBeenCalled(); } finally { await fs.rm(tempDir, { recursive: true, force: true }); } }); ``` ```typescript // src/auto-reply/reply/dispatch-attachment.ts const normalizeAttachmentPath = (ctx: FinalizedMsgContext, cfg: OpenIacConfig): Promise => { const mediaAttachments = normalizeAttachments(ctx); const cache = new MediaAttachmentCache(mediaAttachments,