86 lines
2.3 KiB
TypeScript
86 lines
2.3 KiB
TypeScript
import type { SsrFPolicy } from "../infra/net/ssrf.js";
|
|
|
|
function normalizeHostnameSuffix(value: string): string {
|
|
const trimmed = value.trim().toLowerCase();
|
|
if (!trimmed) {
|
|
return "";
|
|
}
|
|
if (trimmed === "*" || trimmed === "*.") {
|
|
return "*";
|
|
}
|
|
const withoutWildcard = trimmed.replace(/^\*\.?/, "");
|
|
const withoutLeadingDot = withoutWildcard.replace(/^\.+/, "");
|
|
return withoutLeadingDot.replace(/\.+$/, "");
|
|
}
|
|
|
|
function isHostnameAllowedBySuffixAllowlist(
|
|
hostname: string,
|
|
allowlist: readonly string[],
|
|
): boolean {
|
|
if (allowlist.includes("*")) {
|
|
return true;
|
|
}
|
|
const normalized = hostname.toLowerCase();
|
|
return allowlist.some((entry) => normalized === entry || normalized.endsWith(`.${entry}`));
|
|
}
|
|
|
|
export function normalizeHostnameSuffixAllowlist(
|
|
input?: readonly string[],
|
|
defaults?: readonly string[],
|
|
): string[] {
|
|
const source = input && input.length > 0 ? input : defaults;
|
|
if (!source || source.length === 0) {
|
|
return [];
|
|
}
|
|
const normalized = source.map(normalizeHostnameSuffix).filter(Boolean);
|
|
if (normalized.includes("*")) {
|
|
return ["*"];
|
|
}
|
|
return Array.from(new Set(normalized));
|
|
}
|
|
|
|
export function isHttpsUrlAllowedByHostnameSuffixAllowlist(
|
|
url: string,
|
|
allowlist: readonly string[],
|
|
): boolean {
|
|
try {
|
|
const parsed = new URL(url);
|
|
if (parsed.protocol !== "https:") {
|
|
return false;
|
|
}
|
|
return isHostnameAllowedBySuffixAllowlist(parsed.hostname, allowlist);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts suffix-style host allowlists (for example "example.com") into SSRF
|
|
* hostname allowlist patterns used by the shared fetch guard.
|
|
*
|
|
* Suffix semantics:
|
|
* - "example.com" allows "example.com" and "*.example.com"
|
|
* - "*" disables hostname allowlist restrictions
|
|
*/
|
|
export function buildHostnameAllowlistPolicyFromSuffixAllowlist(
|
|
allowHosts?: readonly string[],
|
|
): SsrFPolicy | undefined {
|
|
const normalizedAllowHosts = normalizeHostnameSuffixAllowlist(allowHosts);
|
|
if (normalizedAllowHosts.length === 0) {
|
|
return undefined;
|
|
}
|
|
const patterns = new Set<string>();
|
|
for (const normalized of normalizedAllowHosts) {
|
|
if (normalized === "*") {
|
|
return undefined;
|
|
}
|
|
patterns.add(normalized);
|
|
patterns.add(`*.${normalized}`);
|
|
}
|
|
|
|
if (patterns.size === 0) {
|
|
return undefined;
|
|
}
|
|
return { hostnameAllowlist: Array.from(patterns) };
|
|
}
|