fix(security): cap Slack media downloads and validate Slack file URLs (#6639)

* Security: cap Slack media downloads and validate Slack file URLs

* Security: relax web media fetch cap for compression

* Fixes: sync pi-coding-agent options

* Fixes: align system prompt override type

* Slack: clarify fetchImpl assumptions

* fix: respect raw media fetch cap (#6639) (thanks @davidiach)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
David Iach
2026-02-02 10:48:07 +02:00
committed by GitHub
parent 521b121815
commit 4e4ed2ea17
6 changed files with 97 additions and 14 deletions

View File

@@ -4,7 +4,7 @@ import path from "node:path";
import sharp from "sharp";
import { afterEach, describe, expect, it, vi } from "vitest";
import { optimizeImageToPng } from "../media/image-ops.js";
import { loadWebMedia, optimizeImageToJpeg } from "./media.js";
import { loadWebMedia, loadWebMediaRaw, optimizeImageToJpeg } from "./media.js";
const tmpFiles: string[] = [];
@@ -106,6 +106,22 @@ describe("web media loading", () => {
fetchMock.mockRestore();
});
it("respects maxBytes for raw URL fetches", async () => {
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce({
ok: true,
body: true,
arrayBuffer: async () => Buffer.alloc(2048).buffer,
headers: { get: () => "image/png" },
status: 200,
} as Response);
await expect(loadWebMediaRaw("https://example.com/too-big.png", 1024)).rejects.toThrow(
/exceeds maxBytes 1024/i,
);
fetchMock.mockRestore();
});
it("uses content-disposition filename when available", async () => {
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce({
ok: true,

View File

@@ -200,7 +200,16 @@ async function loadWebMediaInternal(
};
if (/^https?:\/\//i.test(mediaUrl)) {
const fetched = await fetchRemoteMedia({ url: mediaUrl });
// Enforce a download cap during fetch to avoid unbounded memory usage.
// For optimized images, allow fetching larger payloads before compression.
const defaultFetchCap = maxBytesForKind("unknown");
const fetchCap =
maxBytes === undefined
? defaultFetchCap
: optimizeImages
? Math.max(maxBytes, defaultFetchCap)
: maxBytes;
const fetched = await fetchRemoteMedia({ url: mediaUrl, maxBytes: fetchCap });
const { buffer, contentType, fileName } = fetched;
const kind = mediaKindFromMime(contentType);
return await clampAndFinalize({ buffer, contentType, kind, fileName });