From 0647481c7c91fb4cacf01979c32bb56cc551e93a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 21 Apr 2026 00:43:38 +0100 Subject: [PATCH] refactor: share ssrf policy merging --- extensions/comfy/workflow-runtime.ts | 30 +------------------- extensions/fal/image-generation-provider.ts | 30 +------------------- src/plugin-sdk/ssrf-policy.test.ts | 31 +++++++++++++++++++++ src/plugin-sdk/ssrf-policy.ts | 31 +++++++++++++++++++++ src/plugin-sdk/ssrf-runtime.ts | 1 + 5 files changed, 65 insertions(+), 58 deletions(-) diff --git a/extensions/comfy/workflow-runtime.ts b/extensions/comfy/workflow-runtime.ts index 44aec79f398..7434d03a946 100644 --- a/extensions/comfy/workflow-runtime.ts +++ b/extensions/comfy/workflow-runtime.ts @@ -14,6 +14,7 @@ import { buildHostnameAllowlistPolicyFromSuffixAllowlist, fetchWithSsrFGuard, isPrivateOrLoopbackHost, + mergeSsrFPolicies, ssrfPolicyFromDangerouslyAllowPrivateNetwork, type SsrFPolicy, } from "openclaw/plugin-sdk/ssrf-runtime"; @@ -102,35 +103,6 @@ function readConfigInteger(config: ComfyProviderConfig, key: string): number | u return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined; } -function mergeSsrFPolicies(...policies: Array): SsrFPolicy | undefined { - const merged: SsrFPolicy = {}; - for (const policy of policies) { - if (!policy) { - continue; - } - if (policy.allowPrivateNetwork) { - merged.allowPrivateNetwork = true; - } - if (policy.dangerouslyAllowPrivateNetwork) { - merged.dangerouslyAllowPrivateNetwork = true; - } - if (policy.allowRfc2544BenchmarkRange) { - merged.allowRfc2544BenchmarkRange = true; - } - if (policy.allowedHostnames?.length) { - merged.allowedHostnames = Array.from( - new Set([...(merged.allowedHostnames ?? []), ...policy.allowedHostnames]), - ); - } - if (policy.hostnameAllowlist?.length) { - merged.hostnameAllowlist = Array.from( - new Set([...(merged.hostnameAllowlist ?? []), ...policy.hostnameAllowlist]), - ); - } - } - return Object.keys(merged).length > 0 ? merged : undefined; -} - export function getComfyConfig(cfg?: OpenClawConfig): ComfyProviderConfig { const raw = cfg?.models?.providers?.comfy; return isRecord(raw) ? raw : {}; diff --git a/extensions/fal/image-generation-provider.ts b/extensions/fal/image-generation-provider.ts index 4c8352433e1..13261aaa842 100644 --- a/extensions/fal/image-generation-provider.ts +++ b/extensions/fal/image-generation-provider.ts @@ -11,6 +11,7 @@ import { import { buildHostnameAllowlistPolicyFromSuffixAllowlist, fetchWithSsrFGuard, + mergeSsrFPolicies, type SsrFPolicy, ssrfPolicyFromDangerouslyAllowPrivateNetwork, } from "openclaw/plugin-sdk/ssrf-runtime"; @@ -55,35 +56,6 @@ export function _setFalFetchGuardForTesting(impl: typeof fetchWithSsrFGuard | nu falFetchGuard = impl ?? fetchWithSsrFGuard; } -function mergeSsrFPolicies(...policies: Array): SsrFPolicy | undefined { - const merged: SsrFPolicy = {}; - for (const policy of policies) { - if (!policy) { - continue; - } - if (policy.allowPrivateNetwork) { - merged.allowPrivateNetwork = true; - } - if (policy.dangerouslyAllowPrivateNetwork) { - merged.dangerouslyAllowPrivateNetwork = true; - } - if (policy.allowRfc2544BenchmarkRange) { - merged.allowRfc2544BenchmarkRange = true; - } - if (policy.allowedHostnames?.length) { - merged.allowedHostnames = Array.from( - new Set([...(merged.allowedHostnames ?? []), ...policy.allowedHostnames]), - ); - } - if (policy.hostnameAllowlist?.length) { - merged.hostnameAllowlist = Array.from( - new Set([...(merged.hostnameAllowlist ?? []), ...policy.hostnameAllowlist]), - ); - } - } - return Object.keys(merged).length > 0 ? merged : undefined; -} - function matchesTrustedHostSuffix(hostname: string, trustedSuffix: string): boolean { const normalizedHost = normalizeLowercaseStringOrEmpty(hostname); const normalizedSuffix = normalizeLowercaseStringOrEmpty(trustedSuffix); diff --git a/src/plugin-sdk/ssrf-policy.test.ts b/src/plugin-sdk/ssrf-policy.test.ts index 4d5dd7052d1..8ff09dccaaa 100644 --- a/src/plugin-sdk/ssrf-policy.test.ts +++ b/src/plugin-sdk/ssrf-policy.test.ts @@ -6,6 +6,7 @@ import { hasLegacyFlatAllowPrivateNetworkAlias, isPrivateNetworkOptInEnabled, isHttpsUrlAllowedByHostnameSuffixAllowlist, + mergeSsrFPolicies, migrateLegacyFlatAllowPrivateNetworkAlias, normalizeHostnameSuffixAllowlist, ssrfPolicyFromDangerouslyAllowPrivateNetwork, @@ -130,6 +131,36 @@ describe("ssrfPolicyFromPrivateNetworkOptIn", () => { }); }); +describe("mergeSsrFPolicies", () => { + it("returns undefined when no policy contributes values", () => { + expect(mergeSsrFPolicies(undefined, {})).toBeUndefined(); + }); + + it("merges boolean flags and dedupes host allowlists", () => { + expect( + mergeSsrFPolicies( + { + allowPrivateNetwork: true, + allowedHostnames: ["api.example.com"], + hostnameAllowlist: ["downloads.example.com"], + }, + { + dangerouslyAllowPrivateNetwork: true, + allowRfc2544BenchmarkRange: true, + allowedHostnames: ["api.example.com", "cdn.example.com"], + hostnameAllowlist: ["downloads.example.com", "assets.example.com"], + }, + ), + ).toEqual({ + allowPrivateNetwork: true, + dangerouslyAllowPrivateNetwork: true, + allowRfc2544BenchmarkRange: true, + allowedHostnames: ["api.example.com", "cdn.example.com"], + hostnameAllowlist: ["downloads.example.com", "assets.example.com"], + }); + }); +}); + describe("legacy private-network alias helpers", () => { it("detects the flat allowPrivateNetwork alias", () => { expect(hasLegacyFlatAllowPrivateNetworkAlias({ allowPrivateNetwork: true })).toBe(true); diff --git a/src/plugin-sdk/ssrf-policy.ts b/src/plugin-sdk/ssrf-policy.ts index 04c5d1a9dc1..9e0465a5e79 100644 --- a/src/plugin-sdk/ssrf-policy.ts +++ b/src/plugin-sdk/ssrf-policy.ts @@ -60,6 +60,37 @@ export function ssrfPolicyFromDangerouslyAllowPrivateNetwork( return ssrfPolicyFromPrivateNetworkOptIn(dangerouslyAllowPrivateNetwork); } +export function mergeSsrFPolicies( + ...policies: Array +): SsrFPolicy | undefined { + const merged: SsrFPolicy = {}; + for (const policy of policies) { + if (!policy) { + continue; + } + if (policy.allowPrivateNetwork) { + merged.allowPrivateNetwork = true; + } + if (policy.dangerouslyAllowPrivateNetwork) { + merged.dangerouslyAllowPrivateNetwork = true; + } + if (policy.allowRfc2544BenchmarkRange) { + merged.allowRfc2544BenchmarkRange = true; + } + if (policy.allowedHostnames?.length) { + merged.allowedHostnames = Array.from( + new Set([...(merged.allowedHostnames ?? []), ...policy.allowedHostnames]), + ); + } + if (policy.hostnameAllowlist?.length) { + merged.hostnameAllowlist = Array.from( + new Set([...(merged.hostnameAllowlist ?? []), ...policy.hostnameAllowlist]), + ); + } + } + return Object.keys(merged).length > 0 ? merged : undefined; +} + export function hasLegacyFlatAllowPrivateNetworkAlias(value: unknown): boolean { const entry = asNullableRecord(value); return Boolean(entry && Object.prototype.hasOwnProperty.call(entry, "allowPrivateNetwork")); diff --git a/src/plugin-sdk/ssrf-runtime.ts b/src/plugin-sdk/ssrf-runtime.ts index 119c6b82410..cfe7171c1d4 100644 --- a/src/plugin-sdk/ssrf-runtime.ts +++ b/src/plugin-sdk/ssrf-runtime.ts @@ -19,6 +19,7 @@ export { createLegacyPrivateNetworkDoctorContract, hasLegacyFlatAllowPrivateNetworkAlias, isPrivateNetworkOptInEnabled, + mergeSsrFPolicies, migrateLegacyFlatAllowPrivateNetworkAlias, ssrfPolicyFromDangerouslyAllowPrivateNetwork, ssrfPolicyFromPrivateNetworkOptIn,