mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:10:49 +00:00
fix: isolate browser proxy routing
Co-authored-by: Sanjays2402 <Sanjays2402@users.noreply.github.com>
This commit is contained in:
53
extensions/browser/src/browser/browser-proxy-mode.test.ts
Normal file
53
extensions/browser/src/browser/browser-proxy-mode.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
hasChromeProxyControlArg,
|
||||
hasExplicitChromeProxyRoutingArg,
|
||||
omitChromeProxyEnv,
|
||||
resolveBrowserNavigationProxyMode,
|
||||
} from "./browser-proxy-mode.js";
|
||||
|
||||
describe("browser proxy mode", () => {
|
||||
it("detects Chrome proxy-routing args separately from direct proxy controls", () => {
|
||||
expect(hasChromeProxyControlArg(["--no-proxy-server"])).toBe(true);
|
||||
expect(hasExplicitChromeProxyRoutingArg(["--no-proxy-server"])).toBe(false);
|
||||
expect(hasExplicitChromeProxyRoutingArg(["--proxy-server=http://127.0.0.1:7890"])).toBe(true);
|
||||
expect(hasExplicitChromeProxyRoutingArg(["--proxy-pac-url", "http://proxy.test/pac"])).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("removes proxy env before launching managed Chrome", () => {
|
||||
const env = omitChromeProxyEnv({
|
||||
HTTP_PROXY: "http://proxy.test:8080",
|
||||
HTTPS_PROXY: "http://proxy.test:8443",
|
||||
ALL_PROXY: "socks5://proxy.test:1080",
|
||||
NO_PROXY: "localhost",
|
||||
PATH: "/usr/bin",
|
||||
http_proxy: "http://lower.test:8080",
|
||||
no_proxy: "127.0.0.1",
|
||||
});
|
||||
expect(env).toEqual({ PATH: "/usr/bin" });
|
||||
});
|
||||
|
||||
it("marks only managed local Chrome with explicit proxy routing as proxy-routed", () => {
|
||||
const resolved = { extraArgs: ["--proxy-server=http://127.0.0.1:7890"] };
|
||||
expect(
|
||||
resolveBrowserNavigationProxyMode({
|
||||
resolved,
|
||||
profile: { driver: "openclaw", cdpIsLoopback: true },
|
||||
}),
|
||||
).toBe("explicit-browser-proxy");
|
||||
expect(
|
||||
resolveBrowserNavigationProxyMode({
|
||||
resolved,
|
||||
profile: { driver: "existing-session", cdpIsLoopback: true },
|
||||
}),
|
||||
).toBe("direct");
|
||||
expect(
|
||||
resolveBrowserNavigationProxyMode({
|
||||
resolved,
|
||||
profile: { driver: "openclaw", cdpIsLoopback: false },
|
||||
}),
|
||||
).toBe("direct");
|
||||
});
|
||||
});
|
||||
55
extensions/browser/src/browser/browser-proxy-mode.ts
Normal file
55
extensions/browser/src/browser/browser-proxy-mode.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { ResolvedBrowserConfig, ResolvedBrowserProfile } from "./config.js";
|
||||
import type { BrowserNavigationProxyMode } from "./navigation-guard.js";
|
||||
|
||||
const PROXY_ROUTING_CHROME_ARGS = new Set([
|
||||
"--proxy-auto-detect",
|
||||
"--proxy-pac-url",
|
||||
"--proxy-server",
|
||||
]);
|
||||
|
||||
const PROXY_CONTROL_CHROME_ARGS = new Set(["--no-proxy-server", ...PROXY_ROUTING_CHROME_ARGS]);
|
||||
|
||||
export const CHROME_PROXY_ENV_KEYS = [
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"ALL_PROXY",
|
||||
"NO_PROXY",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"all_proxy",
|
||||
"no_proxy",
|
||||
] as const;
|
||||
|
||||
function chromeArgName(arg: string): string {
|
||||
return arg.trim().split("=", 1)[0]?.toLowerCase() ?? "";
|
||||
}
|
||||
|
||||
export function hasChromeProxyControlArg(args: readonly string[]): boolean {
|
||||
return args.some((arg) => PROXY_CONTROL_CHROME_ARGS.has(chromeArgName(arg)));
|
||||
}
|
||||
|
||||
export function hasExplicitChromeProxyRoutingArg(args: readonly string[]): boolean {
|
||||
return args.some((arg) => PROXY_ROUTING_CHROME_ARGS.has(chromeArgName(arg)));
|
||||
}
|
||||
|
||||
export function omitChromeProxyEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||
const next: NodeJS.ProcessEnv = { ...env };
|
||||
for (const key of CHROME_PROXY_ENV_KEYS) {
|
||||
delete next[key];
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function resolveBrowserNavigationProxyMode(params: {
|
||||
resolved: Pick<ResolvedBrowserConfig, "extraArgs">;
|
||||
profile: Pick<ResolvedBrowserProfile, "cdpIsLoopback" | "driver">;
|
||||
}): BrowserNavigationProxyMode {
|
||||
if (
|
||||
params.profile.driver === "openclaw" &&
|
||||
params.profile.cdpIsLoopback &&
|
||||
hasExplicitChromeProxyRoutingArg(params.resolved.extraArgs)
|
||||
) {
|
||||
return "explicit-browser-proxy";
|
||||
}
|
||||
return "direct";
|
||||
}
|
||||
@@ -224,6 +224,16 @@ describe("chrome.ts internal", () => {
|
||||
});
|
||||
expect(args).toContain("--proxy-server=http://localhost:3128");
|
||||
expect(args).toContain("--mute-audio");
|
||||
expect(args).not.toContain("--no-proxy-server");
|
||||
});
|
||||
|
||||
it("launches managed Chrome direct by default", () => {
|
||||
const args = buildOpenClawChromeLaunchArgs({
|
||||
resolved: baseResolved(),
|
||||
profile: baseProfile,
|
||||
userDataDir: "/tmp/foo",
|
||||
});
|
||||
expect(args).toContain("--no-proxy-server");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -354,6 +364,9 @@ describe("chrome.ts internal", () => {
|
||||
spawnCalls += 1;
|
||||
return makeFakeProc();
|
||||
});
|
||||
vi.stubEnv("HTTP_PROXY", "http://proxy.test:8080");
|
||||
vi.stubEnv("HTTPS_PROXY", "http://proxy.test:8443");
|
||||
vi.stubEnv("NO_PROXY", "localhost");
|
||||
|
||||
// Set up a real HTTP server impersonating Chrome's /json/version.
|
||||
await withMockChromeCdpServer({
|
||||
@@ -364,6 +377,10 @@ describe("chrome.ts internal", () => {
|
||||
const running = await launchOpenClawChrome(makeResolved(), profile);
|
||||
expect(running.pid).toBe(4242);
|
||||
expect(spawnCalls).toBeGreaterThanOrEqual(1);
|
||||
const spawnOptions = spawnMock.mock.calls[0]?.[2] as { env?: NodeJS.ProcessEnv };
|
||||
expect(spawnOptions.env?.HTTP_PROXY).toBeUndefined();
|
||||
expect(spawnOptions.env?.HTTPS_PROXY).toBeUndefined();
|
||||
expect(spawnOptions.env?.NO_PROXY).toBeUndefined();
|
||||
// Cleanup.
|
||||
running.proc.kill?.("SIGTERM");
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { SsrFPolicy } from "../infra/net/ssrf.js";
|
||||
import { ensurePortAvailable } from "../infra/ports.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { CONFIG_DIR } from "../utils.js";
|
||||
import { hasChromeProxyControlArg, omitChromeProxyEnv } from "./browser-proxy-mode.js";
|
||||
import {
|
||||
CHROME_BOOTSTRAP_EXIT_POLL_MS,
|
||||
CHROME_BOOTSTRAP_EXIT_TIMEOUT_MS,
|
||||
@@ -132,6 +133,9 @@ export function buildOpenClawChromeLaunchArgs(params: {
|
||||
if (process.platform === "linux") {
|
||||
args.push("--disable-dev-shm-usage");
|
||||
}
|
||||
if (!hasChromeProxyControlArg(resolved.extraArgs)) {
|
||||
args.push("--no-proxy-server");
|
||||
}
|
||||
if (resolved.extraArgs.length > 0) {
|
||||
args.push(...resolved.extraArgs);
|
||||
}
|
||||
@@ -293,7 +297,7 @@ export async function launchOpenClawChrome(
|
||||
// the tuple overload resolution varies across @types/node versions.
|
||||
const preparedSpawn = prepareOomScoreAdjustedSpawn(exe.path, args, {
|
||||
env: {
|
||||
...process.env,
|
||||
...omitChromeProxyEnv(process.env),
|
||||
// Reduce accidental sharing with the user's env.
|
||||
HOME: os.homedir(),
|
||||
},
|
||||
|
||||
@@ -207,7 +207,7 @@ describe("browser navigation guard", () => {
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("blocks strict policy navigation when env proxy is configured", async () => {
|
||||
it("allows public navigation when only Gateway env proxy is configured", async () => {
|
||||
vi.stubEnv("HTTP_PROXY", "http://127.0.0.1:7890");
|
||||
const lookupFn = createLookupFn("93.184.216.34");
|
||||
await expect(
|
||||
@@ -215,16 +215,29 @@ describe("browser navigation guard", () => {
|
||||
url: "https://example.com",
|
||||
lookupFn,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(InvalidBrowserNavigationUrlError);
|
||||
).resolves.toBeUndefined();
|
||||
expect(lookupFn).toHaveBeenCalledWith("example.com", { all: true });
|
||||
});
|
||||
|
||||
it("allows env proxy navigation when private-network mode is explicitly enabled", async () => {
|
||||
vi.stubEnv("HTTP_PROXY", "http://127.0.0.1:7890");
|
||||
it("blocks explicit browser proxy routing in strict SSRF mode", async () => {
|
||||
const lookupFn = createLookupFn("93.184.216.34");
|
||||
await expect(
|
||||
assertBrowserNavigationAllowed({
|
||||
url: "https://example.com",
|
||||
lookupFn,
|
||||
browserProxyMode: "explicit-browser-proxy",
|
||||
}),
|
||||
).rejects.toBeInstanceOf(InvalidBrowserNavigationUrlError);
|
||||
expect(lookupFn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows explicit browser proxy routing when private-network mode is enabled", async () => {
|
||||
const lookupFn = createLookupFn("93.184.216.34");
|
||||
await expect(
|
||||
assertBrowserNavigationAllowed({
|
||||
url: "https://example.com",
|
||||
lookupFn,
|
||||
browserProxyMode: "explicit-browser-proxy",
|
||||
ssrfPolicy: { dangerouslyAllowPrivateNetwork: true },
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
matchesHostnameAllowlist,
|
||||
normalizeHostname,
|
||||
} from "openclaw/plugin-sdk/browser-security-runtime";
|
||||
import { hasProxyEnvConfigured } from "../infra/net/proxy-env.js";
|
||||
import {
|
||||
isPrivateNetworkAllowedByPolicy,
|
||||
resolvePinnedHostnameWithPolicy,
|
||||
@@ -32,8 +31,11 @@ export class InvalidBrowserNavigationUrlError extends Error {
|
||||
|
||||
export type BrowserNavigationPolicyOptions = {
|
||||
ssrfPolicy?: SsrFPolicy;
|
||||
browserProxyMode?: BrowserNavigationProxyMode;
|
||||
};
|
||||
|
||||
export type BrowserNavigationProxyMode = "direct" | "explicit-browser-proxy";
|
||||
|
||||
export type BrowserNavigationRequestLike = {
|
||||
url(): string;
|
||||
redirectedFrom(): BrowserNavigationRequestLike | null;
|
||||
@@ -41,8 +43,14 @@ export type BrowserNavigationRequestLike = {
|
||||
|
||||
export function withBrowserNavigationPolicy(
|
||||
ssrfPolicy?: SsrFPolicy,
|
||||
opts?: { browserProxyMode?: BrowserNavigationProxyMode },
|
||||
): BrowserNavigationPolicyOptions {
|
||||
return ssrfPolicy ? { ssrfPolicy } : {};
|
||||
return {
|
||||
...(ssrfPolicy ? { ssrfPolicy } : {}),
|
||||
...(opts?.browserProxyMode && opts.browserProxyMode !== "direct"
|
||||
? { browserProxyMode: opts.browserProxyMode }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function requiresInspectableBrowserNavigationRedirects(ssrfPolicy?: SsrFPolicy): boolean {
|
||||
@@ -109,13 +117,15 @@ export async function assertBrowserNavigationAllowed(
|
||||
);
|
||||
}
|
||||
|
||||
// Browser network stacks may apply env proxy routing at connect-time, which
|
||||
// can bypass strict destination-binding intent from pre-navigation DNS checks.
|
||||
// In strict mode, fail closed unless private-network navigation is explicitly
|
||||
// enabled by policy.
|
||||
if (hasProxyEnvConfigured() && !isPrivateNetworkAllowedByPolicy(opts.ssrfPolicy)) {
|
||||
// Browser proxy routing hides the final connect target from this process.
|
||||
// Only block when the browser profile is known to be proxy-routed; Gateway
|
||||
// provider proxy env alone is not proof of browser page proxy behavior.
|
||||
if (
|
||||
opts.browserProxyMode === "explicit-browser-proxy" &&
|
||||
!isPrivateNetworkAllowedByPolicy(opts.ssrfPolicy)
|
||||
) {
|
||||
throw new InvalidBrowserNavigationUrlError(
|
||||
"Navigation blocked: strict browser SSRF policy cannot be enforced while env proxy variables are set",
|
||||
"Navigation blocked: strict browser SSRF policy cannot be enforced while this browser profile is proxy-routed",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -188,6 +198,7 @@ export async function assertBrowserNavigationRedirectChainAllowed(
|
||||
url,
|
||||
lookupFn: opts.lookupFn,
|
||||
ssrfPolicy: opts.ssrfPolicy,
|
||||
browserProxyMode: opts.browserProxyMode,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
assertBrowserNavigationAllowed,
|
||||
assertBrowserNavigationRedirectChainAllowed,
|
||||
assertBrowserNavigationResultAllowed,
|
||||
type BrowserNavigationPolicyOptions,
|
||||
InvalidBrowserNavigationUrlError,
|
||||
withBrowserNavigationPolicy,
|
||||
} from "./navigation-guard.js";
|
||||
@@ -747,14 +748,17 @@ async function closeBlockedNavigationTarget(opts: {
|
||||
await opts.page.close().catch(() => {});
|
||||
}
|
||||
|
||||
export async function assertPageNavigationCompletedSafely(opts: {
|
||||
cdpUrl: string;
|
||||
page: Page;
|
||||
response: Response | null;
|
||||
ssrfPolicy?: SsrFPolicy;
|
||||
targetId?: string;
|
||||
}): Promise<void> {
|
||||
const navigationPolicy = withBrowserNavigationPolicy(opts.ssrfPolicy);
|
||||
export async function assertPageNavigationCompletedSafely(
|
||||
opts: {
|
||||
cdpUrl: string;
|
||||
page: Page;
|
||||
response: Response | null;
|
||||
targetId?: string;
|
||||
} & BrowserNavigationPolicyOptions,
|
||||
): Promise<void> {
|
||||
const navigationPolicy = withBrowserNavigationPolicy(opts.ssrfPolicy, {
|
||||
browserProxyMode: opts.browserProxyMode,
|
||||
});
|
||||
try {
|
||||
await assertBrowserNavigationRedirectChainAllowed({
|
||||
request: opts.response?.request(),
|
||||
@@ -776,15 +780,18 @@ export async function assertPageNavigationCompletedSafely(opts: {
|
||||
}
|
||||
}
|
||||
|
||||
export async function gotoPageWithNavigationGuard(opts: {
|
||||
cdpUrl: string;
|
||||
page: Page;
|
||||
url: string;
|
||||
timeoutMs: number;
|
||||
ssrfPolicy?: SsrFPolicy;
|
||||
targetId?: string;
|
||||
}): Promise<Response | null> {
|
||||
const navigationPolicy = withBrowserNavigationPolicy(opts.ssrfPolicy);
|
||||
export async function gotoPageWithNavigationGuard(
|
||||
opts: {
|
||||
cdpUrl: string;
|
||||
page: Page;
|
||||
url: string;
|
||||
timeoutMs: number;
|
||||
targetId?: string;
|
||||
} & BrowserNavigationPolicyOptions,
|
||||
): Promise<Response | null> {
|
||||
const navigationPolicy = withBrowserNavigationPolicy(opts.ssrfPolicy, {
|
||||
browserProxyMode: opts.browserProxyMode,
|
||||
});
|
||||
let blockedError: unknown = null;
|
||||
|
||||
const handler = async (route: Route, request: Request) => {
|
||||
@@ -1102,11 +1109,12 @@ export async function listPagesViaPlaywright(opts: {
|
||||
* Used for remote profiles where HTTP-based /json/new is ephemeral.
|
||||
* Returns the new page's targetId and metadata.
|
||||
*/
|
||||
export async function createPageViaPlaywright(opts: {
|
||||
cdpUrl: string;
|
||||
url: string;
|
||||
ssrfPolicy?: SsrFPolicy;
|
||||
}): Promise<{
|
||||
export async function createPageViaPlaywright(
|
||||
opts: {
|
||||
cdpUrl: string;
|
||||
url: string;
|
||||
} & BrowserNavigationPolicyOptions,
|
||||
): Promise<{
|
||||
targetId: string;
|
||||
title: string;
|
||||
url: string;
|
||||
@@ -1125,7 +1133,9 @@ export async function createPageViaPlaywright(opts: {
|
||||
// Navigate to the URL
|
||||
const targetUrl = opts.url.trim() || "about:blank";
|
||||
if (targetUrl !== "about:blank") {
|
||||
const navigationPolicy = withBrowserNavigationPolicy(opts.ssrfPolicy);
|
||||
const navigationPolicy = withBrowserNavigationPolicy(opts.ssrfPolicy, {
|
||||
browserProxyMode: opts.browserProxyMode,
|
||||
});
|
||||
await assertBrowserNavigationAllowed({
|
||||
url: targetUrl,
|
||||
...navigationPolicy,
|
||||
@@ -1138,6 +1148,7 @@ export async function createPageViaPlaywright(opts: {
|
||||
url: targetUrl,
|
||||
timeoutMs: 30_000,
|
||||
ssrfPolicy: opts.ssrfPolicy,
|
||||
browserProxyMode: opts.browserProxyMode,
|
||||
targetId: createdTargetId ?? undefined,
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -1150,6 +1161,7 @@ export async function createPageViaPlaywright(opts: {
|
||||
page,
|
||||
response,
|
||||
ssrfPolicy: opts.ssrfPolicy,
|
||||
browserProxyMode: opts.browserProxyMode,
|
||||
targetId: createdTargetId ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@ import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
|
||||
import type { Page } from "playwright-core";
|
||||
import type { SsrFPolicy } from "../infra/net/ssrf.js";
|
||||
import { type AriaSnapshotNode, formatAriaSnapshot, type RawAXNode } from "./cdp.js";
|
||||
import { assertBrowserNavigationAllowed, withBrowserNavigationPolicy } from "./navigation-guard.js";
|
||||
import {
|
||||
assertBrowserNavigationAllowed,
|
||||
type BrowserNavigationPolicyOptions,
|
||||
withBrowserNavigationPolicy,
|
||||
} from "./navigation-guard.js";
|
||||
import {
|
||||
buildRoleSnapshotFromAiSnapshot,
|
||||
buildRoleSnapshotFromAriaSnapshot,
|
||||
@@ -241,6 +245,7 @@ export async function navigateViaPlaywright(opts: {
|
||||
url: string;
|
||||
timeoutMs?: number;
|
||||
ssrfPolicy?: SsrFPolicy;
|
||||
browserProxyMode?: BrowserNavigationPolicyOptions["browserProxyMode"];
|
||||
}): Promise<{ url: string }> {
|
||||
const isRetryableNavigateError = (err: unknown): boolean => {
|
||||
const msg =
|
||||
@@ -261,7 +266,9 @@ export async function navigateViaPlaywright(opts: {
|
||||
}
|
||||
await assertBrowserNavigationAllowed({
|
||||
url,
|
||||
...withBrowserNavigationPolicy(opts.ssrfPolicy),
|
||||
...withBrowserNavigationPolicy(opts.ssrfPolicy, {
|
||||
browserProxyMode: opts.browserProxyMode,
|
||||
}),
|
||||
});
|
||||
const timeout = Math.max(1000, Math.min(120_000, opts.timeoutMs ?? 20_000));
|
||||
let page = await getPageForTargetId(opts);
|
||||
@@ -273,6 +280,7 @@ export async function navigateViaPlaywright(opts: {
|
||||
url,
|
||||
timeoutMs: timeout,
|
||||
ssrfPolicy: opts.ssrfPolicy,
|
||||
browserProxyMode: opts.browserProxyMode,
|
||||
targetId: opts.targetId,
|
||||
});
|
||||
let response;
|
||||
@@ -298,6 +306,7 @@ export async function navigateViaPlaywright(opts: {
|
||||
page,
|
||||
response,
|
||||
ssrfPolicy: opts.ssrfPolicy,
|
||||
browserProxyMode: opts.browserProxyMode,
|
||||
targetId: opts.targetId,
|
||||
});
|
||||
const finalUrl = page.url();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import path from "node:path";
|
||||
import { ensureMediaDir, saveMediaBuffer } from "../../media/store.js";
|
||||
import { resolveBrowserNavigationProxyMode } from "../browser-proxy-mode.js";
|
||||
import { captureScreenshot, snapshotAria } from "../cdp.js";
|
||||
import {
|
||||
evaluateChromeMcpScript,
|
||||
@@ -23,7 +24,7 @@ import {
|
||||
DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE,
|
||||
normalizeBrowserScreenshot,
|
||||
} from "../screenshot.js";
|
||||
import type { BrowserRouteContext } from "../server-context.js";
|
||||
import type { BrowserRouteContext, ProfileContext } from "../server-context.js";
|
||||
import {
|
||||
getPwAiModule,
|
||||
handleRouteError,
|
||||
@@ -45,6 +46,15 @@ import { asyncBrowserRoute, jsonError, toBoolean, toNumber, toStringOrEmpty } fr
|
||||
|
||||
const CHROME_MCP_OVERLAY_ATTR = "data-openclaw-mcp-overlay";
|
||||
|
||||
function browserNavigationPolicyForProfile(ctx: BrowserRouteContext, profileCtx: ProfileContext) {
|
||||
return withBrowserNavigationPolicy(ctx.state().resolved.ssrfPolicy, {
|
||||
browserProxyMode: resolveBrowserNavigationProxyMode({
|
||||
resolved: ctx.state().resolved,
|
||||
profile: profileCtx.profile,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async function collectChromeMcpSnapshotUrls(params: {
|
||||
profileName: string;
|
||||
userDataDir?: string;
|
||||
@@ -251,7 +261,7 @@ export function registerBrowserAgentSnapshotRoutes(
|
||||
targetId,
|
||||
run: async ({ profileCtx, tab, cdpUrl }) => {
|
||||
if (getBrowserProfileCapabilities(profileCtx.profile).usesChromeMcp) {
|
||||
const ssrfPolicyOpts = withBrowserNavigationPolicy(ctx.state().resolved.ssrfPolicy);
|
||||
const ssrfPolicyOpts = browserNavigationPolicyForProfile(ctx, profileCtx);
|
||||
await assertBrowserNavigationAllowed({ url, ...ssrfPolicyOpts });
|
||||
const result = await navigateChromeMcpPage({
|
||||
profileName: profileCtx.profile.name,
|
||||
@@ -270,7 +280,7 @@ export function registerBrowserAgentSnapshotRoutes(
|
||||
cdpUrl,
|
||||
targetId: tab.targetId,
|
||||
url,
|
||||
...withBrowserNavigationPolicy(ctx.state().resolved.ssrfPolicy),
|
||||
...browserNavigationPolicyForProfile(ctx, profileCtx),
|
||||
});
|
||||
const currentTargetId = await resolveTargetIdAfterNavigate({
|
||||
oldTargetId: tab.targetId,
|
||||
@@ -346,7 +356,7 @@ export function registerBrowserAgentSnapshotRoutes(
|
||||
targetId,
|
||||
run: async ({ profileCtx, tab, cdpUrl }) => {
|
||||
if (getBrowserProfileCapabilities(profileCtx.profile).usesChromeMcp) {
|
||||
const ssrfPolicyOpts = withBrowserNavigationPolicy(ctx.state().resolved.ssrfPolicy);
|
||||
const ssrfPolicyOpts = browserNavigationPolicyForProfile(ctx, profileCtx);
|
||||
if (element) {
|
||||
return jsonError(res, 400, EXISTING_SESSION_LIMITS.snapshot.screenshotElement);
|
||||
}
|
||||
@@ -508,7 +518,7 @@ export function registerBrowserAgentSnapshotRoutes(
|
||||
return jsonError(res, 400, "labels/mode=efficient require format=ai");
|
||||
}
|
||||
if (getBrowserProfileCapabilities(profileCtx.profile).usesChromeMcp) {
|
||||
const ssrfPolicyOpts = withBrowserNavigationPolicy(ctx.state().resolved.ssrfPolicy);
|
||||
const ssrfPolicyOpts = browserNavigationPolicyForProfile(ctx, profileCtx);
|
||||
if (plan.selectorValue || plan.frameSelectorValue) {
|
||||
return jsonError(res, 400, EXISTING_SESSION_LIMITS.snapshot.snapshotSelector);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveBrowserNavigationProxyMode } from "../browser-proxy-mode.js";
|
||||
import {
|
||||
BrowserProfileUnavailableError,
|
||||
BrowserTabNotFoundError,
|
||||
@@ -32,6 +33,15 @@ function resolveTabsProfileContext(
|
||||
return profileCtx;
|
||||
}
|
||||
|
||||
function browserNavigationPolicyForProfile(ctx: BrowserRouteContext, profileCtx: ProfileContext) {
|
||||
return withBrowserNavigationPolicy(ctx.state().resolved.ssrfPolicy, {
|
||||
browserProxyMode: resolveBrowserNavigationProxyMode({
|
||||
resolved: ctx.state().resolved,
|
||||
profile: profileCtx.profile,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function handleTabsRouteError(
|
||||
ctx: BrowserRouteContext,
|
||||
res: BrowserResponse,
|
||||
@@ -188,7 +198,7 @@ export function registerBrowserTabRoutes(app: BrowserRouteRegistrar, ctx: Browse
|
||||
run: async (profileCtx) => {
|
||||
await assertBrowserNavigationAllowed({
|
||||
url,
|
||||
...withBrowserNavigationPolicy(ctx.state().resolved.ssrfPolicy),
|
||||
...browserNavigationPolicyForProfile(ctx, profileCtx),
|
||||
});
|
||||
await profileCtx.ensureBrowserAvailable();
|
||||
const tab = await profileCtx.openTab(url, { label });
|
||||
@@ -223,7 +233,7 @@ export function registerBrowserTabRoutes(app: BrowserRouteRegistrar, ctx: Browse
|
||||
if (!tab) {
|
||||
throw new BrowserTabNotFoundError({ input: id });
|
||||
}
|
||||
const ssrfPolicyOpts = withBrowserNavigationPolicy(ctx.state().resolved.ssrfPolicy);
|
||||
const ssrfPolicyOpts = browserNavigationPolicyForProfile(ctx, profileCtx);
|
||||
if (ssrfPolicyOpts.ssrfPolicy) {
|
||||
await assertBrowserNavigationResultAllowed({
|
||||
url: tab.url,
|
||||
@@ -331,7 +341,7 @@ export function registerBrowserTabRoutes(app: BrowserRouteRegistrar, ctx: Browse
|
||||
if (!target) {
|
||||
throw new BrowserTabNotFoundError();
|
||||
}
|
||||
const ssrfPolicyOpts = withBrowserNavigationPolicy(ctx.state().resolved.ssrfPolicy);
|
||||
const ssrfPolicyOpts = browserNavigationPolicyForProfile(ctx, profileCtx);
|
||||
if (ssrfPolicyOpts.ssrfPolicy) {
|
||||
await assertBrowserNavigationResultAllowed({
|
||||
url: target.url,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveBrowserNavigationProxyMode } from "./browser-proxy-mode.js";
|
||||
import { resolveCdpControlPolicy } from "./cdp-reachability-policy.js";
|
||||
import { CDP_JSON_NEW_TIMEOUT_MS } from "./cdp-timeouts.js";
|
||||
import {
|
||||
@@ -132,6 +133,13 @@ export function createProfileTabOps({
|
||||
const cdpHttpBase = normalizeCdpHttpBaseForJsonEndpoints(profile.cdpUrl);
|
||||
const capabilities = getBrowserProfileCapabilities(profile);
|
||||
const getCdpControlPolicy = () => resolveCdpControlPolicy(profile, state().resolved.ssrfPolicy);
|
||||
const getNavigationPolicy = () =>
|
||||
withBrowserNavigationPolicy(state().resolved.ssrfPolicy, {
|
||||
browserProxyMode: resolveBrowserNavigationProxyMode({
|
||||
resolved: state().resolved,
|
||||
profile,
|
||||
}),
|
||||
});
|
||||
|
||||
const readTabs = async (): Promise<BrowserTab[]> => {
|
||||
if (capabilities.usesChromeMcp) {
|
||||
@@ -218,7 +226,7 @@ export function createProfileTabOps({
|
||||
};
|
||||
|
||||
const openTab = async (url: string, opts?: { label?: string }): Promise<BrowserTab> => {
|
||||
const ssrfPolicyOpts = withBrowserNavigationPolicy(state().resolved.ssrfPolicy);
|
||||
const ssrfPolicyOpts = getNavigationPolicy();
|
||||
|
||||
if (capabilities.usesChromeMcp) {
|
||||
await assertBrowserNavigationAllowed({ url, ...ssrfPolicyOpts });
|
||||
@@ -261,6 +269,7 @@ export function createProfileTabOps({
|
||||
);
|
||||
}
|
||||
|
||||
await assertBrowserNavigationAllowed({ url, ...ssrfPolicyOpts });
|
||||
const createdViaCdp = await createTargetViaCdp({
|
||||
cdpUrl: profile.cdpUrl,
|
||||
url,
|
||||
@@ -293,7 +302,6 @@ export function createProfileTabOps({
|
||||
|
||||
const encoded = encodeURIComponent(url);
|
||||
const endpointUrl = new URL(appendCdpPath(cdpHttpBase, "/json/new"));
|
||||
await assertBrowserNavigationAllowed({ url, ...ssrfPolicyOpts });
|
||||
const endpoint = endpointUrl.search
|
||||
? (() => {
|
||||
endpointUrl.searchParams.set("url", url);
|
||||
|
||||
Reference in New Issue
Block a user