mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
security(gateway): block webchat session mutators (#20800)
* chore(ci): local claude settings gitignore * Gateway: block webchat session mutators * Changelog: note webchat session mutator guard * Changelog: credit report for webchat mutator guard
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -89,6 +89,7 @@ USER.md
|
|||||||
!.agent/workflows/
|
!.agent/workflows/
|
||||||
/local/
|
/local/
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
.claude/settings.local.json
|
||||||
|
|
||||||
# Local iOS signing overrides
|
# Local iOS signing overrides
|
||||||
apps/ios/LocalSigning.xcconfig
|
apps/ios/LocalSigning.xcconfig
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Fixes
|
### 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.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
|||||||
}
|
}
|
||||||
respond(true, { ok: true, key: resolved.key }, undefined);
|
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)) {
|
if (!assertValidParams(params, validateSessionsPatchParams, "sessions.patch", respond)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -240,6 +240,17 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
|||||||
if (!key) {
|
if (!key) {
|
||||||
return;
|
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 { cfg, target, storePath } = resolveGatewaySessionTargetFromKey(key);
|
||||||
const applied = await updateSessionStore(storePath, async (store) => {
|
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);
|
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)) {
|
if (!assertValidParams(params, validateSessionsDeleteParams, "sessions.delete", respond)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -355,6 +366,17 @@ export const sessionsHandlers: GatewayRequestHandlers = {
|
|||||||
if (!key) {
|
if (!key) {
|
||||||
return;
|
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 { cfg, target, storePath } = resolveGatewaySessionTargetFromKey(key);
|
||||||
const mainKey = resolveMainSessionKey(cfg);
|
const mainKey = resolveMainSessionKey(cfg);
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import fs from "node:fs/promises";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
import { WebSocket } from "ws";
|
||||||
import { DEFAULT_PROVIDER } from "../agents/defaults.js";
|
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 { startGatewayServerHarness, type GatewayServerHarness } from "./server.e2e-ws-harness.js";
|
||||||
import { createToolSummaryPreviewTranscriptLines } from "./session-preview.test-helpers.js";
|
import { createToolSummaryPreviewTranscriptLines } from "./session-preview.test-helpers.js";
|
||||||
import {
|
import {
|
||||||
@@ -742,4 +744,52 @@ describe("gateway server sessions", () => {
|
|||||||
|
|
||||||
ws.close();
|
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<void>((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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user