/** *ReturnstrueifthegivenIPv4orIPv6addressisinaprivate,loopback, *orlink-localrangethatmustneverbereachedviaconsentuploads.
*/
export function isPrivateOrReservedIP(ip: string): boolean { // Handle IPv4-mapped IPv6 first (e.g., ::ffff:127.0.0.1, ::ffff:10.0.0.1) const ipv4MappedMatch = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(ip); if (ipv4MappedMatch) { return isPrivateOrReservedIP(ipv4MappedMatch[1]);
}
// IPv4 checks const v4Parts = ip.split("."); if (v4Parts.length === 4) { const octets = v4Parts.map(Number); // Validate all octets are integers in 0-255 if (octets.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) { returnfalse;
} const [a, b] = octets; // 10.0.0.0/8 if (a === 10) { returntrue;
} // 172.16.0.0/12 if (a === 172 && b >= 16 && b <= 31) { returntrue;
} // 192.168.0.0/16 if (a === 192 && b === 168) { returntrue;
} // 127.0.0.0/8 (loopback) if (a === 127) { returntrue;
} // 169.254.0.0/16 (link-local) if (a === 169 && b === 254) { returntrue;
} // 0.0.0.0/8 if (a === 0) { returntrue;
}
}
// IPv6 checks const normalized = normalizeLowercaseStringOrEmpty(ip); // ::1 loopback if (normalized === "::1") { returntrue;
} // fe80::/10 link-local if (normalized.startsWith("fe80:") || normalized.startsWith("fe80")) { returntrue;
} // fc00::/7 unique-local (fc00:: and fd00::) if (normalized.startsWith("fc") || normalized.startsWith("fd")) { returntrue;
} // :: unspecified if (normalized === "::") { returntrue;
}
returnfalse;
}
/** *ValidatethataconsentuploadURLissafetoPUTto. *Checks: *1.ProtocolisHTTPS *2.Hostnamematchestheconsentuploadallowlist *3.ResolvedIPisnotinaprivate/reservedrange(anti-SSRF) * *@throwsErroriftheURLfailsvalidation
*/
export async function validateConsentUploadUrl(
url: string,
opts?: {
allowlist?: readonly string[];
resolveFn?: (hostname: string) => Promise<{ address: string } | { address: string }[]>;
},
): Promise<void> {
let parsed: URL; try {
parsed = new URL(url);
} catch { thrownew Error("Consent upload URL is not a valid URL");
}
// 1. Protocol check if (parsed.protocol !== "https:") { thrownew Error(`Consent upload URL must use HTTPS, got ${parsed.protocol}`);
}
// 2. Hostname allowlist check const hostname = normalizeLowercaseStringOrEmpty(parsed.hostname); const allowlist = opts?.allowlist ?? CONSENT_UPLOAD_HOST_ALLOWLIST; const hostAllowed = allowlist.some(
(entry) => hostname === entry || hostname.endsWith(`.${entry}`),
); if (!hostAllowed) { thrownew Error(`Consent upload URL hostname "${hostname}" is not in the allowed domains`);
}
// 3. DNS resolution — reject private/reserved IPs. // Check all resolved addresses to avoid SSRF bypass via mixed public/private answers. const resolveFn = opts?.resolveFn ?? ((name: string) => lookup(name, { all: true }));
let resolved: { address: string }[]; try { const result = await resolveFn(hostname);
resolved = Array.isArray(result) ? result : [result];
} catch { thrownew Error(`Failed to resolve consent upload URL hostname "${hostname}"`);
}
for (const entry of resolved) { if (isPrivateOrReservedIP(entry.address)) { thrownew Error(`Consent upload URL resolves to a private/reserved IP (${entry.address})`);
}
}
}
export interface FileConsentCardParams {
filename: string;
description?: string;
sizeInBytes: number; /** Custom context data to include in the card (passed back in the invoke) */
context?: Record<string, unknown>;
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.