mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:40:42 +00:00
fix(plugin-sdk): prefer canonical private-network opt-in
This commit is contained in:
@@ -84,6 +84,8 @@ export type ChannelSetupInput = {
|
||||
audience?: string;
|
||||
useEnv?: boolean;
|
||||
homeserver?: string;
|
||||
dangerouslyAllowPrivateNetwork?: boolean;
|
||||
/** Compatibility alias for legacy setup callers; prefer dangerouslyAllowPrivateNetwork. */
|
||||
allowPrivateNetwork?: boolean;
|
||||
proxy?: string;
|
||||
userId?: string;
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
isHttpsUrlAllowedByHostnameSuffixAllowlist,
|
||||
migrateLegacyFlatAllowPrivateNetworkAlias,
|
||||
normalizeHostnameSuffixAllowlist,
|
||||
ssrfPolicyFromDangerouslyAllowPrivateNetwork,
|
||||
ssrfPolicyFromAllowPrivateNetwork,
|
||||
ssrfPolicyFromPrivateNetworkOptIn,
|
||||
} from "./ssrf-policy.js";
|
||||
@@ -21,6 +22,28 @@ function createLookupFn(addresses: Array<{ address: string; family: number }>):
|
||||
}) as unknown as LookupFn;
|
||||
}
|
||||
|
||||
describe("ssrfPolicyFromDangerouslyAllowPrivateNetwork", () => {
|
||||
it.each([
|
||||
{
|
||||
name: "returns undefined for missing input",
|
||||
input: undefined,
|
||||
expected: undefined,
|
||||
},
|
||||
{
|
||||
name: "returns undefined when private-network access is disabled",
|
||||
input: false,
|
||||
expected: undefined,
|
||||
},
|
||||
{
|
||||
name: "returns an explicit allow-private-network policy when enabled",
|
||||
input: true,
|
||||
expected: { allowPrivateNetwork: true },
|
||||
},
|
||||
])("$name", ({ input, expected }) => {
|
||||
expect(ssrfPolicyFromDangerouslyAllowPrivateNetwork(input)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ssrfPolicyFromAllowPrivateNetwork", () => {
|
||||
it.each([
|
||||
{
|
||||
@@ -180,7 +203,7 @@ describe("assertHttpUrlTargetsPrivateNetwork", () => {
|
||||
name: "allows https targets without private-network checks",
|
||||
url: "https://matrix.example.org",
|
||||
policy: {
|
||||
allowPrivateNetwork: false,
|
||||
dangerouslyAllowPrivateNetwork: false,
|
||||
},
|
||||
outcome: "resolve",
|
||||
},
|
||||
@@ -188,7 +211,7 @@ describe("assertHttpUrlTargetsPrivateNetwork", () => {
|
||||
name: "allows internal DNS names only when they resolve exclusively to private IPs",
|
||||
url: "http://matrix-synapse:8008",
|
||||
policy: {
|
||||
allowPrivateNetwork: true,
|
||||
dangerouslyAllowPrivateNetwork: true,
|
||||
lookupFn: createLookupFn([{ address: "10.0.0.5", family: 4 }]),
|
||||
},
|
||||
outcome: "resolve",
|
||||
@@ -197,7 +220,7 @@ describe("assertHttpUrlTargetsPrivateNetwork", () => {
|
||||
name: "rejects cleartext public hosts even when private-network access is enabled",
|
||||
url: "http://matrix.example.org:8008",
|
||||
policy: {
|
||||
allowPrivateNetwork: true,
|
||||
dangerouslyAllowPrivateNetwork: true,
|
||||
lookupFn: createLookupFn([{ address: "93.184.216.34", family: 4 }]),
|
||||
errorMessage:
|
||||
"Matrix homeserver must use https:// unless it targets a private or loopback host",
|
||||
@@ -214,6 +237,16 @@ describe("assertHttpUrlTargetsPrivateNetwork", () => {
|
||||
}
|
||||
await expect(result).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("prefers the canonical flag when both canonical and legacy flags are present", async () => {
|
||||
await expect(
|
||||
assertHttpUrlTargetsPrivateNetwork("http://matrix-synapse:8008", {
|
||||
dangerouslyAllowPrivateNetwork: false,
|
||||
allowPrivateNetwork: true,
|
||||
lookupFn: createLookupFn([{ address: "10.0.0.5", family: 4 }]),
|
||||
}),
|
||||
).rejects.toThrow("HTTP URL must target a trusted private/internal host");
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeHostnameSuffixAllowlist", () => {
|
||||
|
||||
@@ -15,8 +15,9 @@ export type PrivateNetworkOptInInput =
|
||||
| undefined
|
||||
| Pick<SsrFPolicy, "allowPrivateNetwork" | "dangerouslyAllowPrivateNetwork">
|
||||
| {
|
||||
allowPrivateNetwork?: boolean | null;
|
||||
dangerouslyAllowPrivateNetwork?: boolean | null;
|
||||
/** Compatibility alias for legacy callers; prefer dangerouslyAllowPrivateNetwork. */
|
||||
allowPrivateNetwork?: boolean | null;
|
||||
network?:
|
||||
| Pick<SsrFPolicy, "allowPrivateNetwork" | "dangerouslyAllowPrivateNetwork">
|
||||
| null
|
||||
@@ -52,6 +53,12 @@ export function ssrfPolicyFromPrivateNetworkOptIn(
|
||||
return isPrivateNetworkOptInEnabled(input) ? { allowPrivateNetwork: true } : undefined;
|
||||
}
|
||||
|
||||
export function ssrfPolicyFromDangerouslyAllowPrivateNetwork(
|
||||
dangerouslyAllowPrivateNetwork: boolean | null | undefined,
|
||||
): SsrFPolicy | undefined {
|
||||
return ssrfPolicyFromPrivateNetworkOptIn(dangerouslyAllowPrivateNetwork);
|
||||
}
|
||||
|
||||
export function hasLegacyFlatAllowPrivateNetworkAlias(value: unknown): boolean {
|
||||
const entry = asRecord(value);
|
||||
return Boolean(entry && Object.prototype.hasOwnProperty.call(entry, "allowPrivateNetwork"));
|
||||
@@ -103,12 +110,13 @@ export function migrateLegacyFlatAllowPrivateNetworkAlias(params: {
|
||||
export function ssrfPolicyFromAllowPrivateNetwork(
|
||||
allowPrivateNetwork: boolean | null | undefined,
|
||||
): SsrFPolicy | undefined {
|
||||
return ssrfPolicyFromPrivateNetworkOptIn(allowPrivateNetwork);
|
||||
return ssrfPolicyFromDangerouslyAllowPrivateNetwork(allowPrivateNetwork);
|
||||
}
|
||||
|
||||
export async function assertHttpUrlTargetsPrivateNetwork(
|
||||
url: string,
|
||||
params: {
|
||||
dangerouslyAllowPrivateNetwork?: boolean | null;
|
||||
allowPrivateNetwork?: boolean | null;
|
||||
lookupFn?: LookupFn;
|
||||
errorMessage?: string;
|
||||
@@ -131,15 +139,20 @@ export async function assertHttpUrlTargetsPrivateNetwork(
|
||||
return;
|
||||
}
|
||||
|
||||
if (params.allowPrivateNetwork !== true) {
|
||||
const allowPrivateNetwork =
|
||||
typeof params.dangerouslyAllowPrivateNetwork === "boolean"
|
||||
? params.dangerouslyAllowPrivateNetwork
|
||||
: params.allowPrivateNetwork;
|
||||
|
||||
if (allowPrivateNetwork !== true) {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// allowPrivateNetwork is an opt-in for trusted private/internal targets, not
|
||||
// a blanket exemption for cleartext public internet hosts.
|
||||
// Private-network opt-in is for trusted private/internal targets, not a
|
||||
// blanket exemption for cleartext public internet hosts.
|
||||
const pinned = await resolvePinnedHostnameWithPolicy(hostname, {
|
||||
lookupFn: params.lookupFn,
|
||||
policy: ssrfPolicyFromAllowPrivateNetwork(true),
|
||||
policy: ssrfPolicyFromDangerouslyAllowPrivateNetwork(true),
|
||||
});
|
||||
if (!pinned.addresses.every((address) => isPrivateIpAddress(address))) {
|
||||
throw new Error(errorMessage);
|
||||
|
||||
@@ -18,6 +18,7 @@ export {
|
||||
hasLegacyFlatAllowPrivateNetworkAlias,
|
||||
isPrivateNetworkOptInEnabled,
|
||||
migrateLegacyFlatAllowPrivateNetworkAlias,
|
||||
ssrfPolicyFromDangerouslyAllowPrivateNetwork,
|
||||
ssrfPolicyFromPrivateNetworkOptIn,
|
||||
ssrfPolicyFromAllowPrivateNetwork,
|
||||
} from "./ssrf-policy.js";
|
||||
|
||||
@@ -5,4 +5,5 @@ export { definePluginEntry } from "./plugin-entry.js";
|
||||
export type { OpenClawConfig } from "../config/config.js";
|
||||
export type { OpenClawPluginApi } from "../plugins/types.js";
|
||||
export { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js";
|
||||
export { ssrfPolicyFromDangerouslyAllowPrivateNetwork } from "./ssrf-policy.js";
|
||||
export { ssrfPolicyFromAllowPrivateNetwork } from "./ssrf-policy.js";
|
||||
|
||||
@@ -56,7 +56,7 @@ const RUNTIME_API_EXPORT_GUARDS: Record<string, readonly string[]> = {
|
||||
'export * from "./src/account-selection.js";',
|
||||
'export * from "./src/env-vars.js";',
|
||||
'export * from "./src/storage-paths.js";',
|
||||
'export { assertHttpUrlTargetsPrivateNetwork, closeDispatcher, createPinnedDispatcher, resolvePinnedHostnameWithPolicy, ssrfPolicyFromAllowPrivateNetwork, type LookupFn, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime";',
|
||||
'export { assertHttpUrlTargetsPrivateNetwork, closeDispatcher, createPinnedDispatcher, resolvePinnedHostnameWithPolicy, ssrfPolicyFromDangerouslyAllowPrivateNetwork, ssrfPolicyFromAllowPrivateNetwork, type LookupFn, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime";',
|
||||
'export { setMatrixThreadBindingIdleTimeoutBySessionKey, setMatrixThreadBindingMaxAgeBySessionKey } from "./src/matrix/thread-bindings-shared.js";',
|
||||
'export { setMatrixRuntime } from "./src/runtime.js";',
|
||||
'export { writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";',
|
||||
|
||||
@@ -662,6 +662,7 @@ describe("plugin-sdk subpath exports", () => {
|
||||
"resolvePinnedHostnameWithPolicy",
|
||||
"formatErrorMessage",
|
||||
"assertHttpUrlTargetsPrivateNetwork",
|
||||
"ssrfPolicyFromDangerouslyAllowPrivateNetwork",
|
||||
"ssrfPolicyFromAllowPrivateNetwork",
|
||||
]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user