diff --git a/CHANGELOG.md b/CHANGELOG.md index 08d6618ff48..ecdee6ac2dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,7 @@ Docs: https://docs.openclaw.ai - Sandbox/Bootstrap context boundary hardening: reject symlink/hardlink alias bootstrap seed files that resolve outside the source workspace and switch post-compaction `AGENTS.md` context reads to boundary-verified file opens, preventing host file content from being injected via workspace aliasing. Thanks @tdjackey for reporting. - Browser/Security output boundary hardening: replace check-then-rename output commits with root-bound fd-verified writes, unify install/skills canonical path-boundary checks, and add regression coverage for symlink-rebind race paths across browser output and shared fs-safe write flows. Thanks @tdjackey for reporting. - Security/Webhook request hardening: enforce auth-before-body parsing for BlueBubbles and Google Chat webhook handlers, add strict pre-auth body/time budgets for webhook auth paths (including LINE signature verification), and add shared in-flight/request guardrails plus regression tests/lint checks to prevent reintroducing unauthenticated slow-body DoS patterns. Thanks @GCXWLP for reporting. +- MSTeams/attachment auth fallback: keep guarded redirect ownership in dispatcher-mode fetch paths while preserving scope fallback retries for transient non-auth failures during media downloads. (#25955) Thanks @bmendonca3. - Gateway/Security hardening: tie loopback-origin dev allowance to actual local socket clients (not Host header claims), add explicit warnings/metrics when `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback` accepts websocket origins, harden safe-regex detection for quantified ambiguous alternation patterns (for example `(a|aa)+`), and bound large regex-evaluation inputs for session-filter and log-redaction paths. - Tests/Sandbox + archive portability: use junction-compatible directory-link setup on Windows and explicit file-symlink platform guards in symlink escape tests where unprivileged file symlinks are unavailable, reducing false Windows CI failures while preserving traversal checks on supported paths. (#28747) Thanks @arosstale. - Security/Skills archive extraction: unify tar extraction safety checks across tar.gz and tar.bz2 install flows, enforce tar compressed-size limits, and fail closed if tar.bz2 archives change between preflight and extraction to prevent bypasses of entry-type/size guardrails. Thanks @GCXWLP for reporting. diff --git a/extensions/msteams/src/attachments.test.ts b/extensions/msteams/src/attachments.test.ts index 81802477344..a924c14a603 100644 --- a/extensions/msteams/src/attachments.test.ts +++ b/extensions/msteams/src/attachments.test.ts @@ -164,7 +164,13 @@ const IMAGE_ATTACHMENT = { contentType: CONTENT_TYPE_IMAGE_PNG, contentUrl: TEST const PNG_BUFFER = Buffer.from("png"); const PNG_BASE64 = PNG_BUFFER.toString("base64"); const PDF_BUFFER = Buffer.from("pdf"); -const createTokenProvider = () => ({ getAccessToken: vi.fn(async () => "token") }); +const createTokenProvider = ( + tokenOrResolver: string | ((scope: string) => string | Promise) = "token", +) => ({ + getAccessToken: vi.fn(async (scope: string) => + typeof tokenOrResolver === "function" ? await tokenOrResolver(scope) : tokenOrResolver, + ), +}); const asSingleItemArray = (value: T) => [value]; const withLabel = (label: string, fields: T): T & LabeledCase => ({ label, @@ -742,6 +748,31 @@ describe("msteams attachments", () => { expect(fetchMock.mock.calls.map(([calledUrl]) => String(calledUrl))).toContain(redirectedUrl); }); + it("continues scope fallback after non-auth failure and succeeds on later scope", async () => { + let authAttempt = 0; + const tokenProvider = createTokenProvider((scope) => `token:${scope}`); + const fetchMock = vi.fn(async (_url: string, opts?: RequestInit) => { + const auth = new Headers(opts?.headers).get("Authorization"); + if (!auth) { + return createTextResponse("unauthorized", 401); + } + authAttempt += 1; + if (authAttempt === 1) { + return createTextResponse("upstream transient", 500); + } + return createBufferResponse(PNG_BUFFER, CONTENT_TYPE_IMAGE_PNG); + }); + + const media = await downloadAttachmentsWithFetch( + createImageAttachments(TEST_URL_IMAGE), + fetchMock, + { tokenProvider, authAllowHosts: [TEST_HOST] }, + ); + + expectAttachmentMediaLength(media, 1); + expect(tokenProvider.getAccessToken).toHaveBeenCalledTimes(2); + }); + it("skips urls outside the allowlist", async () => { const fetchMock = vi.fn(); const media = await downloadAttachmentsWithFetch( diff --git a/extensions/msteams/src/attachments/download.ts b/extensions/msteams/src/attachments/download.ts index cce522d2183..697a090f881 100644 --- a/extensions/msteams/src/attachments/download.ts +++ b/extensions/msteams/src/attachments/download.ts @@ -86,6 +86,10 @@ function scopeCandidatesForUrl(url: string): string[] { } } +function isRedirectStatus(status: number): boolean { + return status === 301 || status === 302 || status === 303 || status === 307 || status === 308; +} + async function fetchWithAuthFallback(params: { url: string; tokenProvider?: MSTeamsAccessTokenProvider; @@ -132,11 +136,14 @@ async function fetchWithAuthFallback(params: { if (authAttempt.ok) { return authAttempt; } - if (authAttempt.status !== 401 && authAttempt.status !== 403) { - // Non-auth failures (including redirects in guarded fetch mode) should - // be handled by the caller's redirect/error policy. + if (isRedirectStatus(authAttempt.status)) { + // Redirects in guarded fetch mode must propagate to the outer guard. return authAttempt; } + if (authAttempt.status !== 401 && authAttempt.status !== 403) { + // Preserve legacy scope fallback semantics for non-auth failures. + continue; + } } catch { // Try the next scope. }