mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-02 20:00:21 +00:00
fix(plugin-sdk): prefer canonical private-network opt-in
This commit is contained in:
@@ -1,2 +1,2 @@
|
||||
2a53347c0f2e20ccda3c1f927170d40138396e62245d2e094875d373109af86b plugin-sdk-api-baseline.json
|
||||
b8f7fc3591b80694cd1f36552005116a71bae96f1e89c4bf85aaf841550fe053 plugin-sdk-api-baseline.jsonl
|
||||
eedd483d35cebcdf261d0b550185e57aeb23a36446c89f5c76a038d6e6d2651a plugin-sdk-api-baseline.json
|
||||
7713278ccd37a88115baac658ae9cb381bdaac8ad0bc2b7b79956b83819c9973 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
buildHostnameAllowlistPolicyFromSuffixAllowlist,
|
||||
fetchWithSsrFGuard,
|
||||
type SsrFPolicy,
|
||||
ssrfPolicyFromAllowPrivateNetwork,
|
||||
ssrfPolicyFromDangerouslyAllowPrivateNetwork,
|
||||
} from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
|
||||
const DEFAULT_FAL_BASE_URL = "https://fal.run";
|
||||
@@ -102,7 +102,7 @@ function resolveFalNetworkPolicy(params: {
|
||||
}
|
||||
|
||||
const hostPolicy = buildHostnameAllowlistPolicyFromSuffixAllowlist([hostSuffix]);
|
||||
const privateNetworkPolicy = ssrfPolicyFromAllowPrivateNetwork(true);
|
||||
const privateNetworkPolicy = ssrfPolicyFromDangerouslyAllowPrivateNetwork(true);
|
||||
const trustedHostPolicy = mergeSsrFPolicies(hostPolicy, privateNetworkPolicy);
|
||||
return {
|
||||
apiPolicy: trustedHostPolicy,
|
||||
|
||||
@@ -14,6 +14,7 @@ export {
|
||||
closeDispatcher,
|
||||
createPinnedDispatcher,
|
||||
resolvePinnedHostnameWithPolicy,
|
||||
ssrfPolicyFromDangerouslyAllowPrivateNetwork,
|
||||
ssrfPolicyFromAllowPrivateNetwork,
|
||||
type LookupFn,
|
||||
type SsrFPolicy,
|
||||
|
||||
@@ -181,7 +181,7 @@ async function addMatrixAccount(params: {
|
||||
name: params.name,
|
||||
avatarUrl: params.avatarUrl,
|
||||
homeserver: params.homeserver,
|
||||
allowPrivateNetwork: params.allowPrivateNetwork,
|
||||
dangerouslyAllowPrivateNetwork: params.allowPrivateNetwork,
|
||||
proxy: params.proxy,
|
||||
userId: params.userId,
|
||||
accessToken: params.accessToken,
|
||||
|
||||
@@ -7,6 +7,7 @@ export { isPrivateOrLoopbackHost } from "./private-network-host.js";
|
||||
export {
|
||||
assertHttpUrlTargetsPrivateNetwork,
|
||||
isPrivateNetworkOptInEnabled,
|
||||
ssrfPolicyFromDangerouslyAllowPrivateNetwork,
|
||||
ssrfPolicyFromAllowPrivateNetwork,
|
||||
type LookupFn,
|
||||
type SsrFPolicy,
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
type LookupFn,
|
||||
normalizeAccountId,
|
||||
normalizeOptionalAccountId,
|
||||
ssrfPolicyFromAllowPrivateNetwork,
|
||||
ssrfPolicyFromDangerouslyAllowPrivateNetwork,
|
||||
} from "./config-runtime-api.js";
|
||||
import type { MatrixAuth, MatrixResolvedConfig } from "./types.js";
|
||||
|
||||
@@ -360,7 +360,10 @@ function buildMatrixNetworkFields(params: {
|
||||
}
|
||||
return {
|
||||
...(params.allowPrivateNetwork
|
||||
? { allowPrivateNetwork: true, ssrfPolicy: ssrfPolicyFromAllowPrivateNetwork(true) }
|
||||
? {
|
||||
allowPrivateNetwork: true,
|
||||
ssrfPolicy: ssrfPolicyFromDangerouslyAllowPrivateNetwork(true),
|
||||
}
|
||||
: {}),
|
||||
...(dispatcherPolicy ? { dispatcherPolicy } : {}),
|
||||
};
|
||||
@@ -501,10 +504,15 @@ export function validateMatrixHomeserverUrl(
|
||||
|
||||
export async function resolveValidatedMatrixHomeserverUrl(
|
||||
homeserver: string,
|
||||
opts?: { allowPrivateNetwork?: boolean; lookupFn?: LookupFn },
|
||||
opts?: {
|
||||
dangerouslyAllowPrivateNetwork?: boolean;
|
||||
allowPrivateNetwork?: boolean;
|
||||
lookupFn?: LookupFn;
|
||||
},
|
||||
): Promise<string> {
|
||||
const normalized = validateMatrixHomeserverUrl(homeserver, opts);
|
||||
await assertHttpUrlTargetsPrivateNetwork(normalized, {
|
||||
dangerouslyAllowPrivateNetwork: opts?.dangerouslyAllowPrivateNetwork,
|
||||
allowPrivateNetwork: opts?.allowPrivateNetwork,
|
||||
lookupFn: opts?.lookupFn,
|
||||
errorMessage: MATRIX_HTTP_HOMESERVER_ERROR,
|
||||
@@ -699,7 +707,7 @@ export async function resolveMatrixAuth(params?: {
|
||||
})) ?? resolved.accessToken;
|
||||
const tokenAuthPassword = resolved.password;
|
||||
const homeserver = await resolveValidatedMatrixHomeserverUrl(resolved.homeserver, {
|
||||
allowPrivateNetwork: resolved.allowPrivateNetwork,
|
||||
dangerouslyAllowPrivateNetwork: resolved.allowPrivateNetwork,
|
||||
});
|
||||
let credentialsWriter: typeof import("../credentials-write.runtime.js") | undefined;
|
||||
const loadCredentialsWriter = async () => {
|
||||
|
||||
@@ -47,7 +47,7 @@ export async function createMatrixClient(params: {
|
||||
ensureMatrixSdkLoggingConfigured();
|
||||
const env = process.env;
|
||||
const homeserver = await resolveValidatedMatrixHomeserverUrl(params.homeserver, {
|
||||
allowPrivateNetwork: params.allowPrivateNetwork,
|
||||
dangerouslyAllowPrivateNetwork: params.allowPrivateNetwork,
|
||||
});
|
||||
const userId = params.userId?.trim() || "unknown";
|
||||
const matrixClientUserId = params.userId?.trim() || undefined;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { coerceSecretRef } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
||||
import {
|
||||
isPrivateNetworkOptInEnabled,
|
||||
ssrfPolicyFromAllowPrivateNetwork,
|
||||
ssrfPolicyFromDangerouslyAllowPrivateNetwork,
|
||||
} from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { resolveMatrixAccountStringValues } from "../auth-precedence.js";
|
||||
import { getMatrixScopedEnvVarNames } from "../env-vars.js";
|
||||
@@ -193,7 +193,10 @@ function buildMatrixNetworkFields(params: {
|
||||
}
|
||||
return {
|
||||
...(params.allowPrivateNetwork
|
||||
? { allowPrivateNetwork: true, ssrfPolicy: ssrfPolicyFromAllowPrivateNetwork(true) }
|
||||
? {
|
||||
allowPrivateNetwork: true,
|
||||
ssrfPolicy: ssrfPolicyFromDangerouslyAllowPrivateNetwork(true),
|
||||
}
|
||||
: {}),
|
||||
...(dispatcherPolicy ? { dispatcherPolicy } : {}),
|
||||
};
|
||||
@@ -276,9 +279,10 @@ export function resolveMatrixConfigForAccount(
|
||||
accountInitialSyncLimit ?? clampMatrixInitialSyncLimit(matrix.initialSyncLimit);
|
||||
const encryption =
|
||||
typeof account.encryption === "boolean" ? account.encryption : (matrix.encryption ?? false);
|
||||
const allowPrivateNetwork = isPrivateNetworkOptInEnabled(account) || isPrivateNetworkOptInEnabled(matrix)
|
||||
? true
|
||||
: undefined;
|
||||
const allowPrivateNetwork =
|
||||
isPrivateNetworkOptInEnabled(account) || isPrivateNetworkOptInEnabled(matrix)
|
||||
? true
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
homeserver: resolvedStrings.homeserver,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
|
||||
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import {
|
||||
type ChannelSetupDmPolicy,
|
||||
type ChannelSetupWizardAdapter,
|
||||
} from "openclaw/plugin-sdk/setup";
|
||||
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { requiresExplicitMatrixDefaultAccount } from "./account-selection.js";
|
||||
import { listMatrixDirectoryGroupsLive } from "./directory-live.js";
|
||||
import {
|
||||
@@ -336,7 +336,7 @@ async function runMatrixConfigure(params: {
|
||||
throw new Error("Matrix homeserver requires explicit private-network opt-in");
|
||||
}
|
||||
await resolveValidatedMatrixHomeserverUrl(homeserver, {
|
||||
allowPrivateNetwork,
|
||||
dangerouslyAllowPrivateNetwork: allowPrivateNetwork,
|
||||
});
|
||||
|
||||
let accessToken = existing.accessToken;
|
||||
|
||||
@@ -65,6 +65,7 @@ export {
|
||||
createPinnedDispatcher,
|
||||
isPrivateOrLoopbackHost,
|
||||
resolvePinnedHostnameWithPolicy,
|
||||
ssrfPolicyFromDangerouslyAllowPrivateNetwork,
|
||||
ssrfPolicyFromAllowPrivateNetwork,
|
||||
type LookupFn,
|
||||
type SsrFPolicy,
|
||||
|
||||
@@ -228,9 +228,11 @@ export function applyMatrixSetupAccountConfig(params: {
|
||||
enabled: true,
|
||||
homeserver: params.input.homeserver?.trim(),
|
||||
allowPrivateNetwork:
|
||||
typeof params.input.allowPrivateNetwork === "boolean"
|
||||
? params.input.allowPrivateNetwork
|
||||
: undefined,
|
||||
typeof params.input.dangerouslyAllowPrivateNetwork === "boolean"
|
||||
? params.input.dangerouslyAllowPrivateNetwork
|
||||
: typeof params.input.allowPrivateNetwork === "boolean"
|
||||
? params.input.allowPrivateNetwork
|
||||
: undefined,
|
||||
proxy: params.input.proxy?.trim() || undefined,
|
||||
userId: password && !userId ? null : userId,
|
||||
accessToken: accessToken || (password ? null : undefined),
|
||||
|
||||
@@ -105,6 +105,27 @@ describe("matrixSetupAdapter", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("stores canonical dangerous private-network opt-in from setup input", () => {
|
||||
const next = matrixSetupAdapter.applyAccountConfig({
|
||||
cfg: {} as CoreConfig,
|
||||
accountId: "ops",
|
||||
input: {
|
||||
homeserver: "http://matrix.internal:8008",
|
||||
accessToken: "ops-token",
|
||||
dangerouslyAllowPrivateNetwork: true,
|
||||
},
|
||||
}) as CoreConfig;
|
||||
|
||||
expect(next.channels?.matrix?.accounts?.ops).toMatchObject({
|
||||
enabled: true,
|
||||
homeserver: "http://matrix.internal:8008",
|
||||
accessToken: "ops-token",
|
||||
network: {
|
||||
dangerouslyAllowPrivateNetwork: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps top-level block streaming as a shared default when named accounts already exist", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
|
||||
@@ -93,7 +93,7 @@ async function validateSearxngBaseUrl(baseUrl: string, lookupFn?: LookupFn): Pro
|
||||
|
||||
if (parsed.protocol === "http:") {
|
||||
await assertHttpUrlTargetsPrivateNetwork(parsed.toString(), {
|
||||
allowPrivateNetwork: true,
|
||||
dangerouslyAllowPrivateNetwork: true,
|
||||
lookupFn,
|
||||
errorMessage:
|
||||
"SearXNG HTTP base URL must target a trusted private or loopback host. Use https:// for public hosts.",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
definePluginEntry,
|
||||
fetchWithSsrFGuard,
|
||||
ssrfPolicyFromAllowPrivateNetwork,
|
||||
ssrfPolicyFromDangerouslyAllowPrivateNetwork,
|
||||
type OpenClawConfig,
|
||||
type OpenClawPluginApi,
|
||||
} from "./api.js";
|
||||
@@ -105,7 +105,7 @@ export default definePluginEntry({
|
||||
body: JSON.stringify({ agent_id: agentId }),
|
||||
},
|
||||
timeoutMs: 3000,
|
||||
policy: ssrfPolicyFromAllowPrivateNetwork(true),
|
||||
policy: ssrfPolicyFromDangerouslyAllowPrivateNetwork(true),
|
||||
auditContext: "thread-ownership",
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { configureClient } from "./tlon-api.js";
|
||||
import { resolveTlonAccount } from "./types.js";
|
||||
import { authenticate } from "./urbit/auth.js";
|
||||
import { ssrfPolicyFromAllowPrivateNetwork } from "./urbit/context.js";
|
||||
import { ssrfPolicyFromDangerouslyAllowPrivateNetwork } from "./urbit/context.js";
|
||||
import { urbitFetch } from "./urbit/fetch.js";
|
||||
import {
|
||||
buildMediaStory,
|
||||
@@ -39,7 +39,9 @@ async function createHttpPokeApi(params: {
|
||||
ship: string;
|
||||
dangerouslyAllowPrivateNetwork?: boolean;
|
||||
}) {
|
||||
const ssrfPolicy = ssrfPolicyFromAllowPrivateNetwork(params.dangerouslyAllowPrivateNetwork);
|
||||
const ssrfPolicy = ssrfPolicyFromDangerouslyAllowPrivateNetwork(
|
||||
params.dangerouslyAllowPrivateNetwork,
|
||||
);
|
||||
const cookie = await authenticate(params.url, params.code, { ssrfPolicy });
|
||||
const channelId = `${Math.floor(Date.now() / 1000)}-${crypto.randomUUID()}`;
|
||||
const channelPath = `/~/channel/${channelId}`;
|
||||
@@ -197,7 +199,9 @@ export const tlonRuntimeOutbound: ChannelOutboundAdapter = {
|
||||
|
||||
export async function probeTlonAccount(account: ConfiguredTlonAccount) {
|
||||
try {
|
||||
const ssrfPolicy = ssrfPolicyFromAllowPrivateNetwork(account.dangerouslyAllowPrivateNetwork);
|
||||
const ssrfPolicy = ssrfPolicyFromDangerouslyAllowPrivateNetwork(
|
||||
account.dangerouslyAllowPrivateNetwork,
|
||||
);
|
||||
const cookie = await authenticate(account.url, account.code, { ssrfPolicy });
|
||||
const { response, release } = await urbitFetch({
|
||||
baseUrl: account.url,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createSettingsManager, type TlonSettingsStore } from "../settings.js";
|
||||
import { normalizeShip, parseChannelNest } from "../targets.js";
|
||||
import { resolveTlonAccount } from "../types.js";
|
||||
import { authenticate } from "../urbit/auth.js";
|
||||
import { ssrfPolicyFromAllowPrivateNetwork } from "../urbit/context.js";
|
||||
import { ssrfPolicyFromDangerouslyAllowPrivateNetwork } from "../urbit/context.js";
|
||||
import type { Foreigns, DmInvite } from "../urbit/foreigns.js";
|
||||
import { sendDm, sendGroupMessage } from "../urbit/send.js";
|
||||
import { UrbitSSEClient } from "../urbit/sse-client.js";
|
||||
@@ -73,7 +73,9 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise<v
|
||||
const botShipName = normalizeShip(account.ship);
|
||||
runtime.log?.(`[tlon] Starting monitor for ${botShipName}`);
|
||||
|
||||
const ssrfPolicy = ssrfPolicyFromAllowPrivateNetwork(account.dangerouslyAllowPrivateNetwork);
|
||||
const ssrfPolicy = ssrfPolicyFromDangerouslyAllowPrivateNetwork(
|
||||
account.dangerouslyAllowPrivateNetwork,
|
||||
);
|
||||
|
||||
// Store validated values for use in closures (TypeScript narrowing doesn't propagate)
|
||||
const accountUrl = account.url;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
import { authenticate } from "./urbit/auth.js";
|
||||
import { scryUrbitPath } from "./urbit/channel-ops.js";
|
||||
import { ssrfPolicyFromAllowPrivateNetwork } from "./urbit/context.js";
|
||||
import { ssrfPolicyFromDangerouslyAllowPrivateNetwork } from "./urbit/context.js";
|
||||
|
||||
type ClientConfig = {
|
||||
shipUrl: string;
|
||||
@@ -112,7 +112,7 @@ function sanitizeFileName(fileName: string): string {
|
||||
|
||||
async function getAuthCookie(config: ClientConfig): Promise<string> {
|
||||
return await authenticate(config.shipUrl, await config.getCode(), {
|
||||
ssrfPolicy: ssrfPolicyFromAllowPrivateNetwork(config.dangerouslyAllowPrivateNetwork),
|
||||
ssrfPolicy: ssrfPolicyFromDangerouslyAllowPrivateNetwork(config.dangerouslyAllowPrivateNetwork),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -121,7 +121,9 @@ async function scryJson<T>(config: ClientConfig, cookie: string, path: string):
|
||||
{
|
||||
baseUrl: config.shipUrl,
|
||||
cookie,
|
||||
ssrfPolicy: ssrfPolicyFromAllowPrivateNetwork(config.dangerouslyAllowPrivateNetwork),
|
||||
ssrfPolicy: ssrfPolicyFromDangerouslyAllowPrivateNetwork(
|
||||
config.dangerouslyAllowPrivateNetwork,
|
||||
),
|
||||
},
|
||||
{ path, auditContext: "tlon-storage-scry" },
|
||||
)) as T;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { SsrFPolicy } from "../../api.js";
|
||||
export { ssrfPolicyFromAllowPrivateNetwork } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
export {
|
||||
ssrfPolicyFromDangerouslyAllowPrivateNetwork,
|
||||
ssrfPolicyFromAllowPrivateNetwork,
|
||||
} from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { validateUrbitBaseUrl } from "./base-url.js";
|
||||
import { UrbitUrlError } from "./errors.js";
|
||||
|
||||
|
||||
@@ -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