mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 14:51:42 +00:00
fix(security): enforce msteams redirect allowlist checks
This commit is contained in:
@@ -459,6 +459,88 @@ describe("msteams attachments", () => {
|
||||
|
||||
expect(media.media).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("blocks SharePoint redirects to hosts outside allowHosts", async () => {
|
||||
const { downloadMSTeamsGraphMedia } = await load();
|
||||
const shareUrl = "https://contoso.sharepoint.com/site/file";
|
||||
const escapedUrl = "https://evil.example/internal.pdf";
|
||||
fetchRemoteMediaMock.mockImplementationOnce(async (params) => {
|
||||
const fetchFn = params.fetchImpl ?? fetch;
|
||||
let currentUrl = params.url;
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
const res = await fetchFn(currentUrl, { redirect: "manual" });
|
||||
if ([301, 302, 303, 307, 308].includes(res.status)) {
|
||||
const location = res.headers.get("location");
|
||||
if (!location) {
|
||||
throw new Error("redirect missing location");
|
||||
}
|
||||
currentUrl = new URL(location, currentUrl).toString();
|
||||
continue;
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
return {
|
||||
buffer: Buffer.from(await res.arrayBuffer()),
|
||||
contentType: res.headers.get("content-type") ?? undefined,
|
||||
fileName: params.filePathHint,
|
||||
};
|
||||
}
|
||||
throw new Error("too many redirects");
|
||||
});
|
||||
|
||||
const fetchMock = vi.fn(async (url: string) => {
|
||||
if (url.endsWith("/hostedContents")) {
|
||||
return new Response(JSON.stringify({ value: [] }), { status: 200 });
|
||||
}
|
||||
if (url.endsWith("/attachments")) {
|
||||
return new Response(JSON.stringify({ value: [] }), { status: 200 });
|
||||
}
|
||||
if (url.endsWith("/messages/123")) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
attachments: [
|
||||
{
|
||||
id: "ref-1",
|
||||
contentType: "reference",
|
||||
contentUrl: shareUrl,
|
||||
name: "report.pdf",
|
||||
},
|
||||
],
|
||||
}),
|
||||
{ status: 200 },
|
||||
);
|
||||
}
|
||||
if (url.startsWith("https://graph.microsoft.com/v1.0/shares/")) {
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: { location: escapedUrl },
|
||||
});
|
||||
}
|
||||
if (url === escapedUrl) {
|
||||
return new Response(Buffer.from("should-not-be-fetched"), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/pdf" },
|
||||
});
|
||||
}
|
||||
return new Response("not found", { status: 404 });
|
||||
});
|
||||
|
||||
const media = await downloadMSTeamsGraphMedia({
|
||||
messageUrl: "https://graph.microsoft.com/v1.0/chats/19%3Achat/messages/123",
|
||||
tokenProvider: { getAccessToken: vi.fn(async () => "token") },
|
||||
maxBytes: 1024 * 1024,
|
||||
allowHosts: ["graph.microsoft.com", "contoso.sharepoint.com"],
|
||||
fetchFn: fetchMock as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
expect(media.media).toHaveLength(0);
|
||||
const calledUrls = fetchMock.mock.calls.map((call) => String(call[0]));
|
||||
expect(
|
||||
calledUrls.some((url) => url.startsWith("https://graph.microsoft.com/v1.0/shares/")),
|
||||
).toBe(true);
|
||||
expect(calledUrls).not.toContain(escapedUrl);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildMSTeamsMediaPayload", () => {
|
||||
|
||||
Reference in New Issue
Block a user