diff --git a/CHANGELOG.md b/CHANGELOG.md index d976e541ef6..67f4f88ce77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -601,6 +601,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Models/provider config precedence: prefer exact `models.providers.` matches before normalized provider aliases in embedded model resolution, preventing alias/canonical key collisions from applying the wrong provider `api`, `baseUrl`, or headers. (#35934) thanks @RealKai42. +- Network/fetch guard redirect auth stripping: switch cross-origin redirect handling in `fetchWithSsrFGuard` from a narrow sensitive-header denylist to a safe-header allowlist so custom auth headers like `X-Api-Key` and `Private-Token` no longer leak on origin changes. Thanks @Rickidevs for reporting. - Logging/Subsystem console timestamps: route subsystem console timestamp rendering through `formatConsoleTimestamp(...)` so `pretty` and timestamp-prefix output use local timezone formatting consistently instead of inline UTC `toISOString()` paths. (#25970) Thanks @openperf. - Feishu/Multi-account + reply reliability: add `channels.feishu.defaultAccount` outbound routing support with schema validation, prevent inbound preview text from leaking into prompt system events, keep quoted-message extraction text-first (post/interactive/file placeholders instead of raw JSON), route Feishu video sends as `msg_type: "file"`, and avoid websocket event blocking by using non-blocking event handling in monitor dispatch. Landed from contributor PRs #31209, #29610, #30432, #30331, and #29501. Thanks @stakeswky, @hclsys, @bmendonca3, @patrick-yingxi-pan, and @zwffff. - Feishu/Target routing + replies + dedupe: normalize provider-prefixed targets (`feishu:`/`lark:`), prefer configured `channels.feishu.defaultAccount` for tool execution, honor Feishu outbound `renderMode` in adapter text/caption sends, fall back to normal send when reply targets are withdrawn/deleted, and add synchronous in-memory dedupe guard for concurrent duplicate inbound events. Landed from contributor PRs #30428, #30438, #29958, #30444, and #29463. Thanks @bmendonca3 and @Yaxuan42. diff --git a/src/infra/net/fetch-guard.ssrf.test.ts b/src/infra/net/fetch-guard.ssrf.test.ts index 4e6410c4b36..04144224cd6 100644 --- a/src/infra/net/fetch-guard.ssrf.test.ts +++ b/src/infra/net/fetch-guard.ssrf.test.ts @@ -154,7 +154,12 @@ describe("fetchWithSsrFGuard hardening", () => { "Proxy-Authorization": "Basic c2VjcmV0", Cookie: "session=abc", Cookie2: "legacy=1", + "X-Api-Key": "custom-secret", + "Private-Token": "private-secret", "X-Trace": "1", + Accept: "application/json", + "Content-Type": "application/json", + "User-Agent": "OpenClaw-Test/1.0", }, }, }); @@ -164,7 +169,12 @@ describe("fetchWithSsrFGuard hardening", () => { expect(headers.get("proxy-authorization")).toBeNull(); expect(headers.get("cookie")).toBeNull(); expect(headers.get("cookie2")).toBeNull(); - expect(headers.get("x-trace")).toBe("1"); + expect(headers.get("x-api-key")).toBeNull(); + expect(headers.get("private-token")).toBeNull(); + expect(headers.get("x-trace")).toBeNull(); + expect(headers.get("accept")).toBe("application/json"); + expect(headers.get("content-type")).toBe("application/json"); + expect(headers.get("user-agent")).toBe("OpenClaw-Test/1.0"); await result.release(); }); diff --git a/src/infra/net/fetch-guard.ts b/src/infra/net/fetch-guard.ts index ded0c5fae21..faf81754209 100644 --- a/src/infra/net/fetch-guard.ts +++ b/src/infra/net/fetch-guard.ts @@ -52,12 +52,21 @@ type GuardedFetchPresetOptions = Omit< >; const DEFAULT_MAX_REDIRECTS = 3; -const CROSS_ORIGIN_REDIRECT_SENSITIVE_HEADERS = [ - "authorization", - "proxy-authorization", - "cookie", - "cookie2", -]; +const CROSS_ORIGIN_REDIRECT_SAFE_HEADERS = new Set([ + "accept", + "accept-encoding", + "accept-language", + "cache-control", + "content-language", + "content-type", + "if-match", + "if-modified-since", + "if-none-match", + "if-unmodified-since", + "pragma", + "range", + "user-agent", +]); export function withStrictGuardedFetchMode(params: GuardedFetchPresetOptions): GuardedFetchOptions { return { ...params, mode: GUARDED_FETCH_MODE.STRICT }; @@ -87,9 +96,12 @@ function stripSensitiveHeadersForCrossOriginRedirect(init?: RequestInit): Reques if (!init?.headers) { return init; } - const headers = new Headers(init.headers); - for (const header of CROSS_ORIGIN_REDIRECT_SENSITIVE_HEADERS) { - headers.delete(header); + const incoming = new Headers(init.headers); + const headers = new Headers(); + for (const [key, value] of incoming.entries()) { + if (CROSS_ORIGIN_REDIRECT_SAFE_HEADERS.has(key.toLowerCase())) { + headers.set(key, value); + } } return { ...init, headers }; }