mirror of https://github.com/openclaw/openclaw.git
185 lines
5.4 KiB
TypeScript
185 lines
5.4 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import type { LookupFn } from "../infra/net/ssrf.js";
|
|
import {
|
|
assertHttpUrlTargetsPrivateNetwork,
|
|
buildHostnameAllowlistPolicyFromSuffixAllowlist,
|
|
isHttpsUrlAllowedByHostnameSuffixAllowlist,
|
|
normalizeHostnameSuffixAllowlist,
|
|
ssrfPolicyFromAllowPrivateNetwork,
|
|
} from "./ssrf-policy.js";
|
|
|
|
function createLookupFn(addresses: Array<{ address: string; family: number }>): LookupFn {
|
|
return vi.fn(async (_hostname: string, options?: unknown) => {
|
|
if (typeof options === "number" || !options || !(options as { all?: boolean }).all) {
|
|
return addresses[0];
|
|
}
|
|
return addresses;
|
|
}) as unknown as LookupFn;
|
|
}
|
|
|
|
describe("ssrfPolicyFromAllowPrivateNetwork", () => {
|
|
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(ssrfPolicyFromAllowPrivateNetwork(input)).toEqual(expected);
|
|
});
|
|
});
|
|
|
|
describe("assertHttpUrlTargetsPrivateNetwork", () => {
|
|
it.each([
|
|
{
|
|
name: "allows https targets without private-network checks",
|
|
url: "https://matrix.example.org",
|
|
policy: {
|
|
allowPrivateNetwork: false,
|
|
},
|
|
outcome: "resolve",
|
|
},
|
|
{
|
|
name: "allows internal DNS names only when they resolve exclusively to private IPs",
|
|
url: "http://matrix-synapse:8008",
|
|
policy: {
|
|
allowPrivateNetwork: true,
|
|
lookupFn: createLookupFn([{ address: "10.0.0.5", family: 4 }]),
|
|
},
|
|
outcome: "resolve",
|
|
},
|
|
{
|
|
name: "rejects cleartext public hosts even when private-network access is enabled",
|
|
url: "http://matrix.example.org:8008",
|
|
policy: {
|
|
allowPrivateNetwork: true,
|
|
lookupFn: createLookupFn([{ address: "93.184.216.34", family: 4 }]),
|
|
errorMessage:
|
|
"Matrix homeserver must use https:// unless it targets a private or loopback host",
|
|
},
|
|
outcome: "reject",
|
|
expectedError:
|
|
"Matrix homeserver must use https:// unless it targets a private or loopback host",
|
|
},
|
|
])("$name", async ({ url, policy, outcome, expectedError }) => {
|
|
const result = assertHttpUrlTargetsPrivateNetwork(url, policy);
|
|
if (outcome === "reject") {
|
|
await expect(result).rejects.toThrow(expectedError);
|
|
return;
|
|
}
|
|
await expect(result).resolves.toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("normalizeHostnameSuffixAllowlist", () => {
|
|
it.each([
|
|
{
|
|
name: "uses defaults when input is missing",
|
|
input: undefined,
|
|
defaults: ["GRAPH.MICROSOFT.COM"],
|
|
expected: ["graph.microsoft.com"],
|
|
},
|
|
{
|
|
name: "normalizes wildcard prefixes and deduplicates",
|
|
input: ["*.TrafficManager.NET", ".trafficmanager.net.", " * ", "x"],
|
|
defaults: undefined,
|
|
expected: ["*"],
|
|
},
|
|
])("$name", ({ input, defaults, expected }) => {
|
|
expect(normalizeHostnameSuffixAllowlist(input, defaults)).toEqual(expected);
|
|
});
|
|
});
|
|
|
|
describe("isHttpsUrlAllowedByHostnameSuffixAllowlist", () => {
|
|
it.each([
|
|
{
|
|
name: "requires https",
|
|
url: "http://a.example.com/x",
|
|
allowlist: ["example.com"],
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "supports exact match",
|
|
url: "https://example.com/x",
|
|
allowlist: ["example.com"],
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "supports suffix match",
|
|
url: "https://a.example.com/x",
|
|
allowlist: ["example.com"],
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "rejects non-matching hosts",
|
|
url: "https://evil.com/x",
|
|
allowlist: ["example.com"],
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "supports wildcard allowlist",
|
|
url: "https://evil.com/x",
|
|
allowlist: ["*"],
|
|
expected: true,
|
|
},
|
|
])("$name", ({ url, allowlist, expected }) => {
|
|
expect(isHttpsUrlAllowedByHostnameSuffixAllowlist(url, allowlist)).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe("buildHostnameAllowlistPolicyFromSuffixAllowlist", () => {
|
|
it.each([
|
|
{
|
|
name: "returns undefined when allowHosts is empty",
|
|
input: undefined,
|
|
expected: undefined,
|
|
},
|
|
{
|
|
name: "returns undefined for an explicit empty list",
|
|
input: [],
|
|
expected: undefined,
|
|
},
|
|
{
|
|
name: "returns undefined when wildcard host is present",
|
|
input: ["*"],
|
|
expected: undefined,
|
|
},
|
|
{
|
|
name: "returns undefined when wildcard is mixed with concrete hosts",
|
|
input: ["example.com", "*"],
|
|
expected: undefined,
|
|
},
|
|
{
|
|
name: "expands a suffix entry to exact + wildcard hostname allowlist patterns",
|
|
input: ["sharepoint.com"],
|
|
expected: {
|
|
hostnameAllowlist: ["sharepoint.com", "*.sharepoint.com"],
|
|
},
|
|
},
|
|
{
|
|
name: "normalizes wildcard prefixes, leading/trailing dots, and deduplicates patterns",
|
|
input: ["*.TrafficManager.NET", ".trafficmanager.net.", " blob.core.windows.net "],
|
|
expected: {
|
|
hostnameAllowlist: [
|
|
"trafficmanager.net",
|
|
"*.trafficmanager.net",
|
|
"blob.core.windows.net",
|
|
"*.blob.core.windows.net",
|
|
],
|
|
},
|
|
},
|
|
])("$name", ({ input, expected }) => {
|
|
expect(buildHostnameAllowlistPolicyFromSuffixAllowlist(input)).toEqual(expected);
|
|
});
|
|
});
|