mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-26 00:21:59 +00:00
fix(security): prevent gatewayUrl SSRF
This commit is contained in:
@@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Security/Exec approvals: prevent safeBins allowlist bypass via shell expansion (host exec allowlist mode only; not enabled by default). Thanks @christos-eth.
|
||||
- Security/Gateway: block `system.execApprovals.*` via `node.invoke` (use `exec.approvals.node.*` instead). Thanks @christos-eth.
|
||||
- Security/Exec: harden PATH handling by disabling project-local `node_modules/.bin` bootstrapping by default, disallowing node-host `PATH` overrides, and spawning ACP servers via the current executable by default. Thanks @akhmittra.
|
||||
- Security/Gateway: prevent SSRF by ignoring user-provided `gatewayUrl` tool inputs (gateway URL must come from config). Thanks @p80n-sec.
|
||||
- CLI: fix lazy core command registration so top-level maintenance commands (`doctor`, `dashboard`, `reset`, `uninstall`) resolve correctly instead of exposing a non-functional `maintenance` placeholder command.
|
||||
- Telegram: when `channels.telegram.commands.native` is `false`, exclude plugin commands from `setMyCommands` menu registration while keeping plugin slash handlers callable. (#15132) Thanks @Glucksberg.
|
||||
- Security/Agents: scope CLI process cleanup to owned child PIDs to avoid killing unrelated processes on shared hosts. Thanks @aether-ai-agent.
|
||||
|
||||
@@ -20,7 +20,7 @@ describe("gateway tool defaults", () => {
|
||||
expect(opts.url).toBeUndefined();
|
||||
});
|
||||
|
||||
it("passes through explicit overrides", async () => {
|
||||
it("accepts allowlisted gatewayUrl overrides (SSRF hardening)", async () => {
|
||||
callGatewayMock.mockResolvedValueOnce({ ok: true });
|
||||
await callGatewayTool(
|
||||
"health",
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ChannelOutboundAdapter, ChannelPlugin } from "../../channels/plugi
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||
import { createIMessageTestPlugin } from "../../test-utils/imessage-test-plugin.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
|
||||
import { sendMessage, sendPoll } from "./message.js";
|
||||
|
||||
const setRegistry = (registry: ReturnType<typeof createTestRegistry>) => {
|
||||
@@ -172,6 +173,56 @@ describe("sendPoll channel normalization", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("gateway url override hardening", () => {
|
||||
beforeEach(() => {
|
||||
callGatewayMock.mockReset();
|
||||
setRegistry(emptyRegistry);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setRegistry(emptyRegistry);
|
||||
});
|
||||
|
||||
it("drops gateway url overrides in backend mode (SSRF hardening)", async () => {
|
||||
setRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "mattermost",
|
||||
source: "test",
|
||||
plugin: {
|
||||
...createMattermostLikePlugin({ onSendText: () => {} }),
|
||||
outbound: { deliveryMode: "gateway" },
|
||||
},
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
callGatewayMock.mockResolvedValueOnce({ messageId: "m1" });
|
||||
await sendMessage({
|
||||
cfg: {},
|
||||
to: "channel:town-square",
|
||||
content: "hi",
|
||||
channel: "mattermost",
|
||||
gateway: {
|
||||
url: "ws://169.254.169.254:80/latest/meta-data/",
|
||||
token: "t",
|
||||
timeoutMs: 5000,
|
||||
clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
|
||||
clientDisplayName: "agent",
|
||||
mode: GATEWAY_CLIENT_MODES.BACKEND,
|
||||
},
|
||||
});
|
||||
|
||||
expect(callGatewayMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: undefined,
|
||||
token: "t",
|
||||
timeoutMs: 5000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const emptyRegistry = createTestRegistry([]);
|
||||
|
||||
const createMSTeamsOutbound = (opts?: { includePoll?: boolean }): ChannelOutboundAdapter => ({
|
||||
|
||||
@@ -102,8 +102,15 @@ export type MessagePollResult = {
|
||||
};
|
||||
|
||||
function resolveGatewayOptions(opts?: MessageGatewayOptions) {
|
||||
// Security: backend callers (tools/agents) must not accept user-controlled gateway URLs.
|
||||
// Use config-derived gateway target only.
|
||||
const url =
|
||||
opts?.mode === GATEWAY_CLIENT_MODES.BACKEND ||
|
||||
opts?.clientName === GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT
|
||||
? undefined
|
||||
: opts?.url;
|
||||
return {
|
||||
url: opts?.url,
|
||||
url,
|
||||
token: opts?.token,
|
||||
timeoutMs:
|
||||
typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs)
|
||||
|
||||
Reference in New Issue
Block a user