mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 05:47:39 +00:00
fix: guard remote media fetches with SSRF checks
This commit is contained in:
@@ -15,7 +15,12 @@ export class SsrFBlockedError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
type LookupFn = typeof dnsLookup;
|
||||
export type LookupFn = typeof dnsLookup;
|
||||
|
||||
export type SsrFPolicy = {
|
||||
allowPrivateNetwork?: boolean;
|
||||
allowedHostnames?: string[];
|
||||
};
|
||||
|
||||
const PRIVATE_IPV6_PREFIXES = ["fe80:", "fec0:", "fc", "fd"];
|
||||
const BLOCKED_HOSTNAMES = new Set(["localhost", "metadata.google.internal"]);
|
||||
@@ -28,6 +33,13 @@ function normalizeHostname(hostname: string): string {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeHostnameSet(values?: string[]): Set<string> {
|
||||
if (!values || values.length === 0) {
|
||||
return new Set<string>();
|
||||
}
|
||||
return new Set(values.map((value) => normalizeHostname(value)).filter(Boolean));
|
||||
}
|
||||
|
||||
function parseIpv4(address: string): number[] | null {
|
||||
const parts = address.split(".");
|
||||
if (parts.length !== 4) {
|
||||
@@ -206,31 +218,40 @@ export type PinnedHostname = {
|
||||
lookup: typeof dnsLookupCb;
|
||||
};
|
||||
|
||||
export async function resolvePinnedHostname(
|
||||
export async function resolvePinnedHostnameWithPolicy(
|
||||
hostname: string,
|
||||
lookupFn: LookupFn = dnsLookup,
|
||||
params: { lookupFn?: LookupFn; policy?: SsrFPolicy } = {},
|
||||
): Promise<PinnedHostname> {
|
||||
const normalized = normalizeHostname(hostname);
|
||||
if (!normalized) {
|
||||
throw new Error("Invalid hostname");
|
||||
}
|
||||
|
||||
if (isBlockedHostname(normalized)) {
|
||||
throw new SsrFBlockedError(`Blocked hostname: ${hostname}`);
|
||||
}
|
||||
|
||||
if (isPrivateIpAddress(normalized)) {
|
||||
throw new SsrFBlockedError("Blocked: private/internal IP address");
|
||||
const allowPrivateNetwork = Boolean(params.policy?.allowPrivateNetwork);
|
||||
const allowedHostnames = normalizeHostnameSet(params.policy?.allowedHostnames);
|
||||
const isExplicitAllowed = allowedHostnames.has(normalized);
|
||||
|
||||
if (!allowPrivateNetwork && !isExplicitAllowed) {
|
||||
if (isBlockedHostname(normalized)) {
|
||||
throw new SsrFBlockedError(`Blocked hostname: ${hostname}`);
|
||||
}
|
||||
|
||||
if (isPrivateIpAddress(normalized)) {
|
||||
throw new SsrFBlockedError("Blocked: private/internal IP address");
|
||||
}
|
||||
}
|
||||
|
||||
const lookupFn = params.lookupFn ?? dnsLookup;
|
||||
const results = await lookupFn(normalized, { all: true });
|
||||
if (results.length === 0) {
|
||||
throw new Error(`Unable to resolve hostname: ${hostname}`);
|
||||
}
|
||||
|
||||
for (const entry of results) {
|
||||
if (isPrivateIpAddress(entry.address)) {
|
||||
throw new SsrFBlockedError("Blocked: resolves to private/internal IP address");
|
||||
if (!allowPrivateNetwork && !isExplicitAllowed) {
|
||||
for (const entry of results) {
|
||||
if (isPrivateIpAddress(entry.address)) {
|
||||
throw new SsrFBlockedError("Blocked: resolves to private/internal IP address");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,6 +267,13 @@ export async function resolvePinnedHostname(
|
||||
};
|
||||
}
|
||||
|
||||
export async function resolvePinnedHostname(
|
||||
hostname: string,
|
||||
lookupFn: LookupFn = dnsLookup,
|
||||
): Promise<PinnedHostname> {
|
||||
return await resolvePinnedHostnameWithPolicy(hostname, { lookupFn });
|
||||
}
|
||||
|
||||
export function createPinnedDispatcher(pinned: PinnedHostname): Dispatcher {
|
||||
return new Agent({
|
||||
connect: {
|
||||
|
||||
Reference in New Issue
Block a user