fix(browser): relax default hostname SSRF guard

This commit is contained in:
Ayaan Zaidi
2026-04-14 12:05:41 +05:30
parent a743b30b8b
commit bf1d49093a
4 changed files with 22 additions and 6 deletions

View File

@@ -128,6 +128,18 @@ describe("browser navigation guard", () => {
expect(lookupFn).not.toHaveBeenCalled();
});
it("allows hostname navigation when the default strict policy object is present", async () => {
const lookupFn = createLookupFn("93.184.216.34");
await expect(
assertBrowserNavigationAllowed({
url: "https://example.com",
lookupFn,
ssrfPolicy: {},
}),
).resolves.toBeUndefined();
expect(lookupFn).toHaveBeenCalledWith("example.com", { all: true });
});
it("allows explicitly allowed hostnames in strict mode", async () => {
const lookupFn = createLookupFn("93.184.216.34");
await expect(
@@ -300,8 +312,11 @@ describe("browser navigation guard", () => {
).resolves.toBeUndefined();
});
it("treats default browser SSRF mode as requiring redirect-hop inspection", () => {
expect(requiresInspectableBrowserNavigationRedirects()).toBe(true);
it("requires redirect-hop inspection only in explicit strict mode", () => {
expect(requiresInspectableBrowserNavigationRedirects()).toBe(false);
expect(
requiresInspectableBrowserNavigationRedirects({ dangerouslyAllowPrivateNetwork: false }),
).toBe(true);
expect(requiresInspectableBrowserNavigationRedirects({ allowPrivateNetwork: true })).toBe(
false,
);

View File

@@ -43,7 +43,7 @@ export function withBrowserNavigationPolicy(
}
export function requiresInspectableBrowserNavigationRedirects(ssrfPolicy?: SsrFPolicy): boolean {
return !isPrivateNetworkAllowedByPolicy(ssrfPolicy);
return ssrfPolicy?.dangerouslyAllowPrivateNetwork === false;
}
export function requiresInspectableBrowserNavigationRedirectsForUrl(
@@ -122,6 +122,7 @@ export async function assertBrowserNavigationAllowed(
// the same address that passed policy checks.
if (
opts.ssrfPolicy &&
opts.ssrfPolicy.dangerouslyAllowPrivateNetwork === false &&
!isPrivateNetworkAllowedByPolicy(opts.ssrfPolicy) &&
!isIpLiteralHostname(parsed.hostname) &&
!isExplicitlyAllowedBrowserHostname(parsed.hostname, opts.ssrfPolicy)

View File

@@ -42,7 +42,7 @@ describe("browser tab routes attachOnly loopback profiles", () => {
{
id: "PAGE-1",
title: "WordPress",
url: "https://example.test/wp-login.php",
url: "https://example.com/wp-login.php",
webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/PAGE-1",
type: "page",
},
@@ -73,7 +73,7 @@ describe("browser tab routes attachOnly loopback profiles", () => {
{
targetId: "PAGE-1",
title: "WordPress",
url: "",
url: "https://example.com/wp-login.php",
wsUrl: "ws://127.0.0.1:9222/devtools/page/PAGE-1",
type: "page",
},

View File

@@ -80,7 +80,7 @@ describe("browser remote profile fallback and attachOnly behavior", () => {
it("fails closed for remote tab opens in strict mode without Playwright", async () => {
vi.spyOn(deps.pwAiModule, "getPwAiModule").mockResolvedValue(null);
const { state, remote, fetchMock } = deps.createRemoteRouteHarness();
state.resolved.ssrfPolicy = {};
state.resolved.ssrfPolicy = { dangerouslyAllowPrivateNetwork: false };
await expect(remote.openTab("https://example.com")).rejects.toBeInstanceOf(
deps.InvalidBrowserNavigationUrlError,