mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 01:18:28 +00:00
fix(security): restrict tool gatewayUrl overrides
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { loadConfig, resolveGatewayPort } from "../../config/config.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
|
||||
|
||||
@@ -9,11 +10,77 @@ export type GatewayCallOptions = {
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
function canonicalizeToolGatewayWsUrl(raw: string): { origin: string; key: string } {
|
||||
const input = raw.trim();
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(input);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`invalid gatewayUrl: ${input} (${message})`, { cause: error });
|
||||
}
|
||||
|
||||
if (url.protocol !== "ws:" && url.protocol !== "wss:") {
|
||||
throw new Error(`invalid gatewayUrl protocol: ${url.protocol} (expected ws:// or wss://)`);
|
||||
}
|
||||
if (url.username || url.password) {
|
||||
throw new Error("invalid gatewayUrl: credentials are not allowed");
|
||||
}
|
||||
if (url.search || url.hash) {
|
||||
throw new Error("invalid gatewayUrl: query/hash not allowed");
|
||||
}
|
||||
// Agents/tools expect the gateway websocket on the origin, not arbitrary paths.
|
||||
if (url.pathname && url.pathname !== "/") {
|
||||
throw new Error("invalid gatewayUrl: path not allowed");
|
||||
}
|
||||
|
||||
const origin = url.origin;
|
||||
// Key: protocol + host only, lowercased. (host includes IPv6 brackets + port when present)
|
||||
const key = `${url.protocol}//${url.host.toLowerCase()}`;
|
||||
return { origin, key };
|
||||
}
|
||||
|
||||
function validateGatewayUrlOverrideForAgentTools(urlOverride: string): string {
|
||||
const cfg = loadConfig();
|
||||
const port = resolveGatewayPort(cfg);
|
||||
const allowed = new Set<string>([
|
||||
`ws://127.0.0.1:${port}`,
|
||||
`wss://127.0.0.1:${port}`,
|
||||
`ws://localhost:${port}`,
|
||||
`wss://localhost:${port}`,
|
||||
`ws://[::1]:${port}`,
|
||||
`wss://[::1]:${port}`,
|
||||
]);
|
||||
|
||||
const remoteUrl =
|
||||
typeof cfg.gateway?.remote?.url === "string" ? cfg.gateway.remote.url.trim() : "";
|
||||
if (remoteUrl) {
|
||||
try {
|
||||
const remote = canonicalizeToolGatewayWsUrl(remoteUrl);
|
||||
allowed.add(remote.key);
|
||||
} catch {
|
||||
// ignore: misconfigured remote url; tools should fall back to default resolution.
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = canonicalizeToolGatewayWsUrl(urlOverride);
|
||||
if (!allowed.has(parsed.key)) {
|
||||
throw new Error(
|
||||
[
|
||||
"gatewayUrl override blocked (SSRF hardening).",
|
||||
`Allowed: ws(s) loopback on port ${port} (127.0.0.1/localhost/[::1])`,
|
||||
"Or: configure gateway.remote.url and omit gatewayUrl.",
|
||||
].join(" "),
|
||||
);
|
||||
}
|
||||
return parsed.origin;
|
||||
}
|
||||
|
||||
export function resolveGatewayOptions(opts?: GatewayCallOptions) {
|
||||
// Prefer an explicit override; otherwise let callGateway choose based on config.
|
||||
const url =
|
||||
typeof opts?.gatewayUrl === "string" && opts.gatewayUrl.trim()
|
||||
? opts.gatewayUrl.trim()
|
||||
? validateGatewayUrlOverrideForAgentTools(opts.gatewayUrl)
|
||||
: undefined;
|
||||
const token =
|
||||
typeof opts?.gatewayToken === "string" && opts.gatewayToken.trim()
|
||||
|
||||
Reference in New Issue
Block a user