diff --git a/.gitignore b/.gitignore index 118e705838c..4278a24b0f6 100644 --- a/.gitignore +++ b/.gitignore @@ -89,6 +89,7 @@ USER.md !.agent/workflows/ /local/ package-lock.json +.claude/settings.local.json # Local iOS signing overrides apps/ios/LocalSigning.xcconfig diff --git a/CHANGELOG.md b/CHANGELOG.md index e1da32c2ef3..bef0a93e52f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Gateway/WebChat: block `sessions.patch` and `sessions.delete` for WebChat clients so session-store mutations stay restricted to non-WebChat operator flows. Thanks @allsmog for reporting. - Security/Feishu: prevent path traversal in Feishu inbound media temp-file writes by replacing key-derived temp filenames with UUID-based names. Thanks @allsmog for reporting. - LINE/Security: harden inbound media temp-file naming by using UUID-based temp paths for downloaded media instead of external message IDs. (#20792) Thanks @mbelinky. - Security/Media: harden local media ingestion against TOCTOU/symlink swap attacks by pinning reads to a single file descriptor with symlink rejection and inode/device verification in `saveMediaSource`. Thanks @dorjoos for reporting. diff --git a/src/gateway/server-methods/sessions.ts b/src/gateway/server-methods/sessions.ts index ffe69ba88f4..1643ae54ce9 100644 --- a/src/gateway/server-methods/sessions.ts +++ b/src/gateway/server-methods/sessions.ts @@ -231,7 +231,7 @@ export const sessionsHandlers: GatewayRequestHandlers = { } respond(true, { ok: true, key: resolved.key }, undefined); }, - "sessions.patch": async ({ params, respond, context }) => { + "sessions.patch": async ({ params, respond, context, client, isWebchatConnect }) => { if (!assertValidParams(params, validateSessionsPatchParams, "sessions.patch", respond)) { return; } @@ -240,6 +240,17 @@ export const sessionsHandlers: GatewayRequestHandlers = { if (!key) { return; } + if (client?.connect && isWebchatConnect(client.connect)) { + respond( + false, + undefined, + errorShape( + ErrorCodes.INVALID_REQUEST, + "webchat clients cannot patch sessions; use chat.send for session-scoped updates", + ), + ); + return; + } const { cfg, target, storePath } = resolveGatewaySessionTargetFromKey(key); const applied = await updateSessionStore(storePath, async (store) => { @@ -346,7 +357,7 @@ export const sessionsHandlers: GatewayRequestHandlers = { }); respond(true, { ok: true, key: target.canonicalKey, entry: next }, undefined); }, - "sessions.delete": async ({ params, respond }) => { + "sessions.delete": async ({ params, respond, client, isWebchatConnect }) => { if (!assertValidParams(params, validateSessionsDeleteParams, "sessions.delete", respond)) { return; } @@ -355,6 +366,17 @@ export const sessionsHandlers: GatewayRequestHandlers = { if (!key) { return; } + if (client?.connect && isWebchatConnect(client.connect)) { + respond( + false, + undefined, + errorShape( + ErrorCodes.INVALID_REQUEST, + "webchat clients cannot delete sessions; use chat.send for session-scoped updates", + ), + ); + return; + } const { cfg, target, storePath } = resolveGatewaySessionTargetFromKey(key); const mainKey = resolveMainSessionKey(cfg); diff --git a/src/gateway/server.sessions.gateway-server-sessions-a.e2e.test.ts b/src/gateway/server.sessions.gateway-server-sessions-a.e2e.test.ts index 9ad195d25d3..19750842956 100644 --- a/src/gateway/server.sessions.gateway-server-sessions-a.e2e.test.ts +++ b/src/gateway/server.sessions.gateway-server-sessions-a.e2e.test.ts @@ -2,7 +2,9 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; +import { WebSocket } from "ws"; import { DEFAULT_PROVIDER } from "../agents/defaults.js"; +import { GATEWAY_CLIENT_IDS, GATEWAY_CLIENT_MODES } from "./protocol/client-info.js"; import { startGatewayServerHarness, type GatewayServerHarness } from "./server.e2e-ws-harness.js"; import { createToolSummaryPreviewTranscriptLines } from "./session-preview.test-helpers.js"; import { @@ -742,4 +744,52 @@ describe("gateway server sessions", () => { ws.close(); }); + + test("webchat clients cannot patch or delete sessions", async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sessions-webchat-")); + const storePath = path.join(dir, "sessions.json"); + testState.sessionStorePath = storePath; + + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), + }, + "discord:group:dev": { + sessionId: "sess-group", + updatedAt: Date.now(), + }, + }, + }); + + const ws = new WebSocket(`ws://127.0.0.1:${harness.port}`, { + headers: { origin: `http://127.0.0.1:${harness.port}` }, + }); + await new Promise((resolve) => ws.once("open", resolve)); + await connectOk(ws, { + client: { + id: GATEWAY_CLIENT_IDS.WEBCHAT_UI, + version: "1.0.0", + platform: "test", + mode: GATEWAY_CLIENT_MODES.UI, + }, + scopes: ["operator.admin"], + }); + + const patched = await rpcReq(ws, "sessions.patch", { + key: "agent:main:discord:group:dev", + label: "should-fail", + }); + expect(patched.ok).toBe(false); + expect(patched.error?.message ?? "").toMatch(/webchat clients cannot patch sessions/i); + + const deleted = await rpcReq(ws, "sessions.delete", { + key: "agent:main:discord:group:dev", + }); + expect(deleted.ok).toBe(false); + expect(deleted.error?.message ?? "").toMatch(/webchat clients cannot delete sessions/i); + + ws.close(); + }); });