fix(slack): merge auth-header forwarding and html media guard

This commit is contained in:
Tak Hoffman
2026-03-01 11:19:14 -06:00
parent 37cd19f430
commit 389d35468c
3 changed files with 11 additions and 14 deletions

View File

@@ -83,6 +83,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Slack/Bot attachment-only messages: when `allowBots: true`, bot messages with empty `text` now include non-forwarded attachment `text`/`fallback` content so webhook alerts are not silently dropped. (#27616)
- Slack/Inbound media auth + HTML guard: keep Slack auth headers on forwarded shared attachment image downloads, and reject login/error HTML payloads (while allowing expected `.html` uploads) when resolving Slack media so auth failures do not silently pass as files. (#18642)
- Slack/Security ingress mismatch guard: drop slash-command and interaction payloads when app/team identifiers do not match the active Slack account context (including nested `team.id` interaction payloads), preventing cross-app or cross-workspace payload injection into system-event handling. (#29091) Thanks @Solvely-Colin.
- Cron/Failure alerts: add configurable repeated-failure alerting with per-job overrides and Web UI cron editor support (`inherit|disabled|custom` with threshold/cooldown/channel/target fields). (#24789) Thanks xbrak.
- Cron/Isolated model defaults: resolve isolated cron `subagents.model` (including object-form `primary`) through allowlist-aware model selection so isolated cron runs honor subagent model defaults unless explicitly overridden by job payload model. (#11474) Thanks @AnonO6.

View File

@@ -571,10 +571,11 @@ describe("resolveSlackAttachmentContent", () => {
},
],
});
expect(mockFetch).toHaveBeenCalledWith("https://files.slack.com/forwarded.jpg", {
headers: { Authorization: "Bearer xoxb-test-token" },
redirect: "manual",
});
const firstCall = mockFetch.mock.calls[0];
expect(firstCall?.[0]).toBe("https://files.slack.com/forwarded.jpg");
const firstInit = firstCall?.[1];
expect(firstInit?.redirect).toBe("manual");
expect(new Headers(firstInit?.headers).get("Authorization")).toBe("Bearer xoxb-test-token");
});
});

View File

@@ -1,7 +1,6 @@
import type { WebClient as SlackWebClient } from "@slack/web-api";
import { normalizeHostname } from "../../infra/net/hostname.js";
import type { FetchLike } from "../../media/fetch.js";
import { logWarn } from "../../logger.js";
import { fetchRemoteMedia } from "../../media/fetch.js";
import { saveMediaBuffer } from "../../media/store.js";
import type { SlackAttachment, SlackFile } from "../types.js";
@@ -70,11 +69,6 @@ function createSlackMediaFetch(token: string): FetchLike {
};
}
function looksLikeHtmlBuffer(buffer: Buffer): boolean {
const head = buffer.subarray(0, 512).toString("utf-8").replace(/^\s+/, "").toLowerCase();
return head.startsWith("<!doctype html") || head.startsWith("<html");
}
/**
* Fetches a URL with Authorization header, handling cross-origin redirects.
* Node.js fetch strips Authorization headers on cross-origin redirects for security.
@@ -131,6 +125,11 @@ function resolveSlackMediaMimetype(
return mime;
}
function looksLikeHtmlBuffer(buffer: Buffer): boolean {
const head = buffer.subarray(0, 512).toString("utf-8").replace(/^\s+/, "").toLowerCase();
return head.startsWith("<!doctype html") || head.startsWith("<html");
}
export type SlackMediaResult = {
path: string;
contentType?: string;
@@ -233,10 +232,6 @@ export async function resolveSlackMedia(params: {
if (!isExpectedHtml) {
const detectedMime = fetched.contentType?.split(";")[0]?.trim().toLowerCase();
if (detectedMime === "text/html" || looksLikeHtmlBuffer(fetched.buffer)) {
const fileId = file.name ?? file.id ?? "unknown";
logWarn(
`slack: received HTML instead of media for file ${fileId}; possible auth failure or expired URL`,
);
return null;
}
}