fix(browser): auto-allowlist configured CDP hostnames in SSRF policy

This commit is contained in:
HansY
2026-04-17 16:45:02 +00:00
committed by Peter Steinberger
parent a4a34edd21
commit e90c89cf8b
2 changed files with 89 additions and 3 deletions

View File

@@ -343,6 +343,52 @@ describe("browser config", () => {
});
});
it("auto-allowlists hostnames from user-configured profile cdpUrls", () => {
const resolved = resolveBrowserConfig({
profiles: {
remote: {
color: "#123456",
cdpUrl: "http://172.29.128.1:9223",
},
},
});
expect(resolved.ssrfPolicy).toEqual({
allowedHostnames: ["172.29.128.1"],
});
});
it("merges configured profile cdpUrl hostnames with existing ssrfPolicy allowedHostnames", () => {
const resolved = resolveBrowserConfig({
ssrfPolicy: {
allowedHostnames: ["metadata.internal"],
},
profiles: {
remote: {
color: "#123456",
cdpUrl: "http://172.29.128.1:9223",
},
},
});
expect(resolved.ssrfPolicy?.allowedHostnames?.toSorted()).toEqual(
["172.29.128.1", "metadata.internal"].toSorted(),
);
});
it("does not duplicate hostnames already in allowedHostnames", () => {
const resolved = resolveBrowserConfig({
ssrfPolicy: {
allowedHostnames: ["172.29.128.1"],
},
profiles: {
remote: {
color: "#123456",
cdpUrl: "http://172.29.128.1:9223",
},
},
});
expect(resolved.ssrfPolicy?.allowedHostnames).toEqual(["172.29.128.1"]);
});
it("resolves existing-session profiles without cdpPort or cdpUrl", () => {
const resolved = resolveBrowserConfig({
profiles: {

View File

@@ -126,11 +126,27 @@ function resolveCdpPortRangeStart(
const normalizeStringList = normalizeOptionalTrimmedStringList;
function resolveBrowserSsrFPolicy(cfg: BrowserConfig | undefined): SsrFPolicy | undefined {
function mergeAllowedHostnames(
base: string[] | undefined,
extra: readonly string[],
): string[] | undefined {
if (extra.length === 0) {
return base;
}
return Array.from(new Set([...(base ?? []), ...extra]));
}
function resolveBrowserSsrFPolicy(
cfg: BrowserConfig | undefined,
extraAllowedHostnames: readonly string[] = [],
): SsrFPolicy | undefined {
const rawPolicy = cfg?.ssrfPolicy as BrowserSsrFPolicyCompat | undefined;
const allowPrivateNetwork = rawPolicy?.allowPrivateNetwork;
const dangerouslyAllowPrivateNetwork = rawPolicy?.dangerouslyAllowPrivateNetwork;
const allowedHostnames = normalizeStringList(rawPolicy?.allowedHostnames);
const allowedHostnames = mergeAllowedHostnames(
normalizeStringList(rawPolicy?.allowedHostnames),
extraAllowedHostnames,
);
const hostnameAllowlist = normalizeStringList(rawPolicy?.hostnameAllowlist);
const hasExplicitPrivateSetting =
allowPrivateNetwork !== undefined || dangerouslyAllowPrivateNetwork !== undefined;
@@ -159,6 +175,30 @@ function resolveBrowserSsrFPolicy(cfg: BrowserConfig | undefined): SsrFPolicy |
};
}
function collectConfiguredCdpHostnames(cfg: BrowserConfig | undefined): string[] {
const hostnames = new Set<string>();
const addHostnameFromUrl = (rawUrl: string | undefined): void => {
const trimmed = rawUrl?.trim() ?? "";
if (!trimmed) {
return;
}
try {
const hostname = new URL(trimmed).hostname;
if (hostname) {
hostnames.add(hostname);
}
} catch {
// Ignore unparseable URLs; they will be rejected elsewhere with a proper error.
}
};
addHostnameFromUrl(cfg?.cdpUrl);
for (const profile of Object.values(cfg?.profiles ?? {})) {
addHostnameFromUrl(profile?.cdpUrl);
}
return Array.from(hostnames);
}
function ensureDefaultProfile(
profiles: Record<string, BrowserProfileConfig> | undefined,
defaultColor: string,
@@ -293,7 +333,7 @@ export function resolveBrowserConfig(
attachOnly,
defaultProfile,
profiles,
ssrfPolicy: resolveBrowserSsrFPolicy(cfg),
ssrfPolicy: resolveBrowserSsrFPolicy(cfg, collectConfiguredCdpHostnames(cfg)),
extraArgs,
};
}