diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fc7ca016ac..a0245d95475 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai - Agents/Subagents: restore announce-chain delivery to agent injection, defer nested announce output until descendant follow-up content is ready, and prevent descendant deferrals from consuming announce retry budget so deep chains do not drop final completions. (#22223) Thanks @tyler6204. - Gateway/Auth: require `gateway.trustedProxies` to include a loopback proxy address when `auth.mode="trusted-proxy"` and `bind="loopback"`, preventing same-host proxy misconfiguration from silently blocking auth. (#22082, follow-up to #20097) thanks @mbelinky. - Security/OpenClawKit/UI: prevent injected inbound user context metadata blocks from leaking into chat history in TUI, webchat, and macOS surfaces by stripping all untrusted metadata prefixes at display boundaries. (#22142) Thanks @Mellowambience, @vincentkoc. +- Security/OpenClawKit/UI: strip inbound metadata blocks from user messages in TUI rendering while preserving user-authored content. (#22345) Thanks @kansodata, @vincentkoc. - Security/OpenClawKit/UI: prevent inbound metadata leaks and reply-tag streaming artifacts in TUI rendering by stripping untrusted metadata prefixes at display boundaries. (#22346) Thanks @akramcodez, @vincentkoc. - Agents/System Prompt: label allowlisted senders as authorized senders to avoid implying ownership. Thanks @thewilloftheshadow. - Agents/Tool display: fix exec cwd suffix inference so `pushd ... && popd ... && ` does not keep stale `(in )` context in summaries. (#21925) thanks @Lukavyi. diff --git a/extensions/msteams/src/messenger.test.ts b/extensions/msteams/src/messenger.test.ts index 977af0c9666..cbd562ae3ad 100644 --- a/extensions/msteams/src/messenger.test.ts +++ b/extensions/msteams/src/messenger.test.ts @@ -16,6 +16,7 @@ vi.mock("./graph-upload.js", async () => { }; }); +import { resolvePreferredOpenClawTmpDir } from "../../../src/infra/tmp-openclaw-dir.js"; import { type MSTeamsAdapter, renderReplyPayloadsToMessages, @@ -178,7 +179,7 @@ describe("msteams messenger", () => { }); it("preserves parsed mentions when appending OneDrive fallback file links", async () => { - const tmpDir = await mkdtemp(path.join(os.tmpdir(), "msteams-mention-")); + const tmpDir = await mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), "msteams-mention-")); const localFile = path.join(tmpDir, "note.txt"); await writeFile(localFile, "hello"); diff --git a/src/tui/tui-formatters.test.ts b/src/tui/tui-formatters.test.ts index 13368748fec..1daf7903e83 100644 --- a/src/tui/tui-formatters.test.ts +++ b/src/tui/tui-formatters.test.ts @@ -95,6 +95,56 @@ describe("extractTextFromMessage", () => { expect(text).toBe("[binary data omitted]"); }); + + it("strips leading inbound metadata blocks for user messages", () => { + const text = extractTextFromMessage({ + role: "user", + content: `Conversation info (untrusted metadata): +\`\`\`json +{ + "message_id": "abc123" +} +\`\`\` + +Sender (untrusted metadata): +\`\`\`json +{ + "label": "Someone" +} +\`\`\` + +Actual user message`, + }); + + expect(text).toBe("Actual user message"); + }); + + it("keeps metadata-like blocks for non-user messages", () => { + const text = extractTextFromMessage({ + role: "assistant", + content: `Conversation info (untrusted metadata): +\`\`\`json +{"message_id":"abc123"} +\`\`\` + +Assistant body`, + }); + + expect(text).toContain("Conversation info (untrusted metadata):"); + expect(text).toContain("Assistant body"); + }); + + it("does not strip metadata-like blocks that are not a leading prefix", () => { + const text = extractTextFromMessage({ + role: "user", + content: + 'Hello world\nConversation info (untrusted metadata):\n```json\n{"message_id":"123"}\n```\n\nFollow-up', + }); + + expect(text).toBe( + 'Hello world\nConversation info (untrusted metadata):\n```json\n{"message_id":"123"}\n```\n\nFollow-up', + ); + }); }); describe("extractThinkingFromMessage", () => { diff --git a/src/tui/tui-formatters.ts b/src/tui/tui-formatters.ts index 804d8ca4a5b..d4bca178b66 100644 --- a/src/tui/tui-formatters.ts +++ b/src/tui/tui-formatters.ts @@ -1,4 +1,5 @@ import { formatRawAssistantErrorForUi } from "../agents/pi-embedded-helpers.js"; +import { stripInboundMetadataBlocks } from "../shared/chat-envelope.js"; import { stripAnsi } from "../terminal/ansi.js"; import { formatTokenCount } from "../utils/usage-format.js"; @@ -273,6 +274,9 @@ export function extractTextFromMessage( const record = message as Record; const text = extractTextBlocks(record.content, opts); if (text) { + if (record.role === "user") { + return stripInboundMetadataBlocks(text); + } return text; } diff --git a/src/web/media.test.ts b/src/web/media.test.ts index ea50ecd1c73..a2395d6817c 100644 --- a/src/web/media.test.ts +++ b/src/web/media.test.ts @@ -6,6 +6,7 @@ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest import { resolveStateDir } from "../config/paths.js"; import { sendVoiceMessageDiscord } from "../discord/send.js"; import * as ssrf from "../infra/net/ssrf.js"; +import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; import { optimizeImageToPng } from "../media/image-ops.js"; import { captureEnv } from "../test-utils/env.js"; import { @@ -50,7 +51,9 @@ async function createLargeTestJpeg(): Promise<{ buffer: Buffer; file: string }> } beforeAll(async () => { - fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-media-test-")); + fixtureRoot = await fs.mkdtemp( + path.join(resolvePreferredOpenClawTmpDir(), "openclaw-media-test-"), + ); largeJpegBuffer = await sharp({ create: { width: 400, @@ -334,7 +337,9 @@ describe("local media root guard", () => { }); it("allows local paths under an explicit root", async () => { - const result = await loadWebMedia(tinyPngFile, 1024 * 1024, { localRoots: [os.tmpdir()] }); + const result = await loadWebMedia(tinyPngFile, 1024 * 1024, { + localRoots: [resolvePreferredOpenClawTmpDir()], + }); expect(result.kind).toBe("image"); });