mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-07 23:41:24 +00:00
fix(security): harden channel auth path checks and exec approval routing
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { createExecApprovalForwarder } from "./exec-approval-forwarder.js";
|
||||
@@ -40,14 +43,17 @@ function createForwarder(params: {
|
||||
resolveSessionTarget?: () => { channel: string; to: string } | null;
|
||||
}) {
|
||||
const deliver = params.deliver ?? vi.fn().mockResolvedValue([]);
|
||||
const forwarder = createExecApprovalForwarder({
|
||||
const deps: NonNullable<Parameters<typeof createExecApprovalForwarder>[0]> = {
|
||||
getConfig: () => params.cfg,
|
||||
deliver: deliver as unknown as NonNullable<
|
||||
NonNullable<Parameters<typeof createExecApprovalForwarder>[0]>["deliver"]
|
||||
>,
|
||||
nowMs: () => 1000,
|
||||
resolveSessionTarget: params.resolveSessionTarget ?? (() => null),
|
||||
});
|
||||
};
|
||||
if (params.resolveSessionTarget !== undefined) {
|
||||
deps.resolveSessionTarget = params.resolveSessionTarget;
|
||||
}
|
||||
const forwarder = createExecApprovalForwarder(deps);
|
||||
return { deliver, forwarder };
|
||||
}
|
||||
|
||||
@@ -212,6 +218,58 @@ describe("exec approval forwarder", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers turn-source routing over stale session last route", async () => {
|
||||
vi.useFakeTimers();
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-exec-approval-forwarder-test-"));
|
||||
try {
|
||||
const storePath = path.join(tmpDir, "sessions.json");
|
||||
fs.writeFileSync(
|
||||
storePath,
|
||||
JSON.stringify({
|
||||
"agent:main:main": {
|
||||
updatedAt: 1,
|
||||
channel: "slack",
|
||||
to: "U1",
|
||||
lastChannel: "slack",
|
||||
lastTo: "U1",
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const cfg = {
|
||||
session: { store: storePath },
|
||||
approvals: { exec: { enabled: true, mode: "session" } },
|
||||
} as OpenClawConfig;
|
||||
|
||||
const { deliver, forwarder } = createForwarder({ cfg });
|
||||
await expect(
|
||||
forwarder.handleRequested({
|
||||
...baseRequest,
|
||||
request: {
|
||||
...baseRequest.request,
|
||||
turnSourceChannel: "whatsapp",
|
||||
turnSourceTo: "+15555550123",
|
||||
turnSourceAccountId: "work",
|
||||
turnSourceThreadId: "1739201675.123",
|
||||
},
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
|
||||
expect(deliver).toHaveBeenCalledTimes(1);
|
||||
expect(deliver).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "whatsapp",
|
||||
to: "+15555550123",
|
||||
accountId: "work",
|
||||
threadId: "1739201675.123",
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("can forward resolved notices without pending cache when request payload is present", async () => {
|
||||
vi.useFakeTimers();
|
||||
const cfg = {
|
||||
|
||||
@@ -8,7 +8,11 @@ import type {
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { normalizeAccountId, parseAgentSessionKey } from "../routing/session-key.js";
|
||||
import { compileSafeRegex } from "../security/safe-regex.js";
|
||||
import { isDeliverableMessageChannel, normalizeMessageChannel } from "../utils/message-channel.js";
|
||||
import {
|
||||
isDeliverableMessageChannel,
|
||||
normalizeMessageChannel,
|
||||
type DeliverableMessageChannel,
|
||||
} from "../utils/message-channel.js";
|
||||
import type {
|
||||
ExecApprovalDecision,
|
||||
ExecApprovalRequest,
|
||||
@@ -209,6 +213,11 @@ function buildExpiredMessage(request: ExecApprovalRequest) {
|
||||
return `⏱️ Exec approval expired. ID: ${request.id}`;
|
||||
}
|
||||
|
||||
function normalizeTurnSourceChannel(value?: string | null): DeliverableMessageChannel | undefined {
|
||||
const normalized = value ? normalizeMessageChannel(value) : undefined;
|
||||
return normalized && isDeliverableMessageChannel(normalized) ? normalized : undefined;
|
||||
}
|
||||
|
||||
function defaultResolveSessionTarget(params: {
|
||||
cfg: OpenClawConfig;
|
||||
request: ExecApprovalRequest;
|
||||
@@ -225,7 +234,14 @@ function defaultResolveSessionTarget(params: {
|
||||
if (!entry) {
|
||||
return null;
|
||||
}
|
||||
const target = resolveSessionDeliveryTarget({ entry, requestedChannel: "last" });
|
||||
const target = resolveSessionDeliveryTarget({
|
||||
entry,
|
||||
requestedChannel: "last",
|
||||
turnSourceChannel: normalizeTurnSourceChannel(params.request.request.turnSourceChannel),
|
||||
turnSourceTo: params.request.request.turnSourceTo?.trim() || undefined,
|
||||
turnSourceAccountId: params.request.request.turnSourceAccountId?.trim() || undefined,
|
||||
turnSourceThreadId: params.request.request.turnSourceThreadId ?? undefined,
|
||||
});
|
||||
if (!target.channel || !target.to) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ export type ExecApprovalRequestPayload = {
|
||||
agentId?: string | null;
|
||||
resolvedPath?: string | null;
|
||||
sessionKey?: string | null;
|
||||
turnSourceChannel?: string | null;
|
||||
turnSourceTo?: string | null;
|
||||
turnSourceAccountId?: string | null;
|
||||
turnSourceThreadId?: string | number | null;
|
||||
};
|
||||
|
||||
export type ExecApprovalRequest = {
|
||||
|
||||
Reference in New Issue
Block a user