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:
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user