mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 05:37:41 +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/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/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/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.
|
- 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.
|
- 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.
|
- 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();
|
expect(opts.url).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passes through explicit overrides", async () => {
|
it("accepts allowlisted gatewayUrl overrides (SSRF hardening)", async () => {
|
||||||
callGatewayMock.mockResolvedValueOnce({ ok: true });
|
callGatewayMock.mockResolvedValueOnce({ ok: true });
|
||||||
await callGatewayTool(
|
await callGatewayTool(
|
||||||
"health",
|
"health",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { ChannelOutboundAdapter, ChannelPlugin } from "../../channels/plugi
|
|||||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||||
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
|
import { createTestRegistry } from "../../test-utils/channel-plugins.js";
|
||||||
import { createIMessageTestPlugin } from "../../test-utils/imessage-test-plugin.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";
|
import { sendMessage, sendPoll } from "./message.js";
|
||||||
|
|
||||||
const setRegistry = (registry: ReturnType<typeof createTestRegistry>) => {
|
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 emptyRegistry = createTestRegistry([]);
|
||||||
|
|
||||||
const createMSTeamsOutbound = (opts?: { includePoll?: boolean }): ChannelOutboundAdapter => ({
|
const createMSTeamsOutbound = (opts?: { includePoll?: boolean }): ChannelOutboundAdapter => ({
|
||||||
|
|||||||
@@ -102,8 +102,15 @@ export type MessagePollResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function resolveGatewayOptions(opts?: MessageGatewayOptions) {
|
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 {
|
return {
|
||||||
url: opts?.url,
|
url,
|
||||||
token: opts?.token,
|
token: opts?.token,
|
||||||
timeoutMs:
|
timeoutMs:
|
||||||
typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs)
|
typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs)
|
||||||
|
|||||||
Reference in New Issue
Block a user