VYPR
High severity7.7GHSA Advisory· Published May 5, 2026· Updated May 7, 2026

CVE-2026-43573

CVE-2026-43573

Description

OpenClaw before 2026.4.10 contains a server-side request forgery policy bypass vulnerability in existing-session browser interaction routes. Attackers can bypass SSRF navigation guards to interact with or navigate to unauthorized targets without policy enforcement.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
openclawnpm
< 2026.4.102026.4.10

Affected products

2
  • OpenClaw/OpenclawGHSA2 versions
    < 2026.4.10+ 1 more
    • (no CPE)range: < 2026.4.10
    • cpe:2.3:a:openclaw:openclaw:*:*:*:*:*:node.js:*:*range: <2026.4.10

Patches

1
daeb74920d5a

fix(browser): guard existing-session navigation (#64370)

https://github.com/openclaw/openclawAgustin RiveraApr 10, 2026via ghsa
3 files changed · +608 62
  • CHANGELOG.md+1 0 modified
    @@ -128,6 +128,7 @@ Docs: https://docs.openclaw.ai
     - Security/dependencies: pin axios to 1.15.0 and add a plugin install dependency denylist that blocks known malicious packages before install. (#63891) Thanks @mmaps.
     - Browser/security: apply three-phase interaction navigation guard to pressKey and type(submit) so delayed JS redirects from keypress cannot bypass SSRF policy. (#63889) Thanks @mmaps.
     
    +- Browser/security: guard existing-session Chrome MCP interaction routes with SSRF post-checks so delayed navigation from click, type, press, and evaluate cannot bypass the configured policy. (#64370) Thanks @eleqtrizit.
     ## 2026.4.9
     
     ### Changes
    
  • extensions/browser/src/browser/routes/agent.act.existing-session-navigation-guard.test.ts+385 0 added
    @@ -0,0 +1,385 @@
    +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
    +import { createBrowserRouteApp, createBrowserRouteResponse } from "./test-helpers.js";
    +import type { BrowserRequest } from "./types.js";
    +
    +const routeState = vi.hoisted(() => ({
    +  profileCtx: {
    +    profile: {
    +      driver: "existing-session" as const,
    +      name: "chrome-live",
    +    },
    +    ensureTabAvailable: vi.fn(async () => ({
    +      targetId: "7",
    +      url: "https://example.com",
    +    })),
    +  },
    +  tab: {
    +    targetId: "7",
    +    url: "https://example.com",
    +  },
    +}));
    +
    +const chromeMcpMocks = vi.hoisted(() => ({
    +  clickChromeMcpElement: vi.fn(async () => {}),
    +  dragChromeMcpElement: vi.fn(async () => {}),
    +  evaluateChromeMcpScript: vi.fn(async () => "https://example.com"),
    +  fillChromeMcpElement: vi.fn(async () => {}),
    +  fillChromeMcpForm: vi.fn(async () => {}),
    +  hoverChromeMcpElement: vi.fn(async () => {}),
    +  pressChromeMcpKey: vi.fn(async () => {}),
    +}));
    +
    +const navigationGuardMocks = vi.hoisted(() => ({
    +  assertBrowserNavigationAllowed: vi.fn(async () => {}),
    +  assertBrowserNavigationResultAllowed: vi.fn(async () => {}),
    +  withBrowserNavigationPolicy: vi.fn((ssrfPolicy?: unknown) => (ssrfPolicy ? { ssrfPolicy } : {})),
    +}));
    +
    +vi.mock("../chrome-mcp.js", () => ({
    +  clickChromeMcpElement: chromeMcpMocks.clickChromeMcpElement,
    +  closeChromeMcpTab: vi.fn(async () => {}),
    +  dragChromeMcpElement: chromeMcpMocks.dragChromeMcpElement,
    +  evaluateChromeMcpScript: chromeMcpMocks.evaluateChromeMcpScript,
    +  fillChromeMcpElement: chromeMcpMocks.fillChromeMcpElement,
    +  fillChromeMcpForm: chromeMcpMocks.fillChromeMcpForm,
    +  hoverChromeMcpElement: chromeMcpMocks.hoverChromeMcpElement,
    +  pressChromeMcpKey: chromeMcpMocks.pressChromeMcpKey,
    +  resizeChromeMcpPage: vi.fn(async () => {}),
    +}));
    +
    +vi.mock("../navigation-guard.js", () => navigationGuardMocks);
    +
    +vi.mock("./agent.shared.js", () => ({
    +  getPwAiModule: vi.fn(async () => null),
    +  handleRouteError: vi.fn(),
    +  readBody: vi.fn((req: BrowserRequest) => req.body ?? {}),
    +  requirePwAi: vi.fn(async () => {
    +    throw new Error("Playwright should not be used for existing-session tests");
    +  }),
    +  resolveProfileContext: vi.fn(() => routeState.profileCtx),
    +  resolveTargetIdFromBody: vi.fn((body: Record<string, unknown>) =>
    +    typeof body.targetId === "string" ? body.targetId : undefined,
    +  ),
    +  withPlaywrightRouteContext: vi.fn(),
    +  withRouteTabContext: vi.fn(async ({ run }: { run: (args: unknown) => Promise<void> }) => {
    +    await run({
    +      profileCtx: routeState.profileCtx,
    +      cdpUrl: "http://127.0.0.1:18800",
    +      tab: routeState.tab,
    +    });
    +  }),
    +}));
    +
    +const DEFAULT_SSRF_POLICY = { allowPrivateNetwork: false } as const;
    +
    +const { registerBrowserAgentActRoutes } = await import("./agent.act.js");
    +
    +function getActPostHandler(
    +  ssrfPolicy: { allowPrivateNetwork: false } | null = DEFAULT_SSRF_POLICY,
    +) {
    +  const { app, postHandlers } = createBrowserRouteApp();
    +  registerBrowserAgentActRoutes(app, {
    +    state: () => ({
    +      resolved: {
    +        evaluateEnabled: true,
    +        ssrfPolicy: ssrfPolicy ?? undefined,
    +      },
    +    }),
    +  } as never);
    +  const handler = postHandlers.get("/act");
    +  expect(handler).toBeTypeOf("function");
    +  return handler;
    +}
    +
    +describe("existing-session interaction navigation guard", () => {
    +  beforeEach(() => {
    +    vi.useFakeTimers();
    +    for (const fn of Object.values(chromeMcpMocks)) {
    +      fn.mockClear();
    +    }
    +    for (const fn of Object.values(navigationGuardMocks)) {
    +      fn.mockClear();
    +    }
    +    chromeMcpMocks.evaluateChromeMcpScript.mockResolvedValue("https://example.com");
    +  });
    +
    +  afterEach(() => {
    +    vi.useRealTimers();
    +  });
    +
    +  async function runAction(
    +    body: Record<string, unknown>,
    +    ssrfPolicy: { allowPrivateNetwork: false } | null = DEFAULT_SSRF_POLICY,
    +  ) {
    +    const handler = getActPostHandler(ssrfPolicy);
    +    const response = createBrowserRouteResponse();
    +    const pending = handler?.({ params: {}, query: {}, body }, response.res);
    +    await vi.runAllTimersAsync();
    +    await pending;
    +    return response;
    +  }
    +
    +  it("checks navigation after click and key-driven submit paths", async () => {
    +    const clickResponse = await runAction({ kind: "click", ref: "btn-1" });
    +    const typeResponse = await runAction({
    +      kind: "type",
    +      ref: "field-1",
    +      text: "hello",
    +      submit: true,
    +    });
    +
    +    expect(clickResponse.statusCode).toBe(200);
    +    expect(typeResponse.statusCode).toBe(200);
    +    expect(chromeMcpMocks.clickChromeMcpElement).toHaveBeenCalledOnce();
    +    expect(chromeMcpMocks.pressChromeMcpKey).toHaveBeenCalledWith(
    +      expect.objectContaining({ key: "Enter" }),
    +    );
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenCalledTimes(6);
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
    +      1,
    +      expect.objectContaining({ url: "https://example.com" }),
    +    );
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
    +      2,
    +      expect.objectContaining({ url: "https://example.com" }),
    +    );
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
    +      3,
    +      expect.objectContaining({ url: "https://example.com" }),
    +    );
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
    +      4,
    +      expect.objectContaining({ url: "https://example.com" }),
    +    );
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
    +      5,
    +      expect.objectContaining({ url: "https://example.com" }),
    +    );
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
    +      6,
    +      expect.objectContaining({ url: "https://example.com" }),
    +    );
    +  });
    +
    +  it("rechecks the page url after delayed navigation-triggering interactions", async () => {
    +    chromeMcpMocks.evaluateChromeMcpScript
    +      .mockResolvedValueOnce(42 as never)
    +      .mockResolvedValueOnce("https://example.com" as never)
    +      .mockResolvedValueOnce("http://169.254.169.254/latest/meta-data/" as never)
    +      .mockResolvedValueOnce("http://169.254.169.254/latest/meta-data/" as never);
    +
    +    const response = await runAction({ kind: "evaluate", fn: "() => document.title" });
    +
    +    expect(response.statusCode).toBe(200);
    +    expect(chromeMcpMocks.evaluateChromeMcpScript).toHaveBeenCalledTimes(4);
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
    +      1,
    +      expect.objectContaining({ url: "https://example.com" }),
    +    );
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
    +      2,
    +      expect.objectContaining({ url: "http://169.254.169.254/latest/meta-data/" }),
    +    );
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
    +      3,
    +      expect.objectContaining({ url: "http://169.254.169.254/latest/meta-data/" }),
    +    );
    +  });
    +
    +  it("fails closed when location probes never return a usable url", async () => {
    +    chromeMcpMocks.evaluateChromeMcpScript
    +      .mockResolvedValueOnce("result" as never)
    +      .mockResolvedValueOnce(undefined as never)
    +      .mockResolvedValueOnce(null as never)
    +      .mockResolvedValueOnce("   " as never);
    +
    +    const handler = getActPostHandler();
    +    const response = createBrowserRouteResponse();
    +    const pending =
    +      handler?.(
    +        { params: {}, query: {}, body: { kind: "evaluate", fn: "() => 1" } },
    +        response.res,
    +      ) ?? Promise.resolve();
    +    void pending.catch(() => {});
    +    const completion = (async () => {
    +      await vi.runAllTimersAsync();
    +      await pending;
    +    })();
    +
    +    await expect(completion).rejects.toThrow("Unable to verify stable post-interaction navigation");
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).not.toHaveBeenCalled();
    +  });
    +
    +  it("fails closed when a later post-action probe becomes unreadable", async () => {
    +    chromeMcpMocks.evaluateChromeMcpScript
    +      .mockResolvedValueOnce("result" as never) // action evaluate
    +      .mockResolvedValueOnce("https://example.com" as never) // location probe 1
    +      .mockResolvedValueOnce(undefined as never) // location probe 2 - unreadable
    +      .mockResolvedValueOnce(undefined as never) // location probe 3 - unreadable
    +      .mockResolvedValueOnce(undefined as never); // follow-up probe - still unreadable
    +
    +    const handler = getActPostHandler();
    +    const response = createBrowserRouteResponse();
    +    const pending =
    +      handler?.(
    +        { params: {}, query: {}, body: { kind: "evaluate", fn: "() => 1" } },
    +        response.res,
    +      ) ?? Promise.resolve();
    +    void pending.catch(() => {});
    +    const completion = (async () => {
    +      await vi.runAllTimersAsync();
    +      await pending;
    +    })();
    +
    +    await expect(completion).rejects.toThrow("Unable to verify stable post-interaction navigation");
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenCalledOnce();
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenCalledWith(
    +      expect.objectContaining({ url: "https://example.com" }),
    +    );
    +  });
    +
    +  it("confirms stability via follow-up probe when URL changes on the last loop iteration", async () => {
    +    // Probe 1 (action evaluate result): returns the action value
    +    // Location probe 1 (0ms): fails (context churn)
    +    // Location probe 2 (250ms): reads safe URL A
    +    // Location probe 3 (500ms): reads safe URL B (late navigation)
    +    // Follow-up probe (500ms later): reads URL B again → stable, success
    +    chromeMcpMocks.evaluateChromeMcpScript
    +      .mockResolvedValueOnce("result" as never) // action evaluate result
    +      .mockRejectedValueOnce(new Error("context churn") as never) // location probe 1 fails
    +      .mockResolvedValueOnce("https://example.com" as never) // location probe 2: URL A
    +      .mockResolvedValueOnce("https://safe-redirect.com" as never) // location probe 3: URL B (changed)
    +      .mockResolvedValueOnce("https://safe-redirect.com" as never); // follow-up: URL B again → stable
    +
    +    const response = await runAction({ kind: "evaluate", fn: "() => 1" });
    +
    +    expect(response.statusCode).toBe(200);
    +    // 1 action call + 5 location probes (3 in loop + 1 failed + 1 follow-up)
    +    expect(chromeMcpMocks.evaluateChromeMcpScript).toHaveBeenCalledTimes(5);
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenCalledTimes(3);
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
    +      1,
    +      expect.objectContaining({ url: "https://example.com" }),
    +    );
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
    +      2,
    +      expect.objectContaining({ url: "https://safe-redirect.com" }),
    +    );
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
    +      3,
    +      expect.objectContaining({ url: "https://safe-redirect.com" }),
    +    );
    +  });
    +
    +  it("keeps probing through the full window before declaring navigation stable", async () => {
    +    chromeMcpMocks.evaluateChromeMcpScript
    +      .mockResolvedValueOnce("result" as never) // action evaluate result
    +      .mockResolvedValueOnce("https://example.com" as never) // location probe 1
    +      .mockResolvedValueOnce("https://example.com" as never) // location probe 2
    +      .mockResolvedValueOnce("https://safe-redirect.com" as never) // location probe 3
    +      .mockResolvedValueOnce("https://safe-redirect.com" as never); // follow-up confirms late redirect
    +
    +    const response = await runAction({ kind: "evaluate", fn: "() => 1" });
    +
    +    expect(response.statusCode).toBe(200);
    +    expect(chromeMcpMocks.evaluateChromeMcpScript).toHaveBeenCalledTimes(5);
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenCalledTimes(4);
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
    +      1,
    +      expect.objectContaining({ url: "https://example.com" }),
    +    );
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
    +      2,
    +      expect.objectContaining({ url: "https://example.com" }),
    +    );
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
    +      3,
    +      expect.objectContaining({ url: "https://safe-redirect.com" }),
    +    );
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenNthCalledWith(
    +      4,
    +      expect.objectContaining({ url: "https://safe-redirect.com" }),
    +    );
    +  });
    +
    +  it("fails closed when follow-up probe sees yet another URL change", async () => {
    +    chromeMcpMocks.evaluateChromeMcpScript
    +      .mockResolvedValueOnce("result" as never) // action evaluate result
    +      .mockResolvedValueOnce("https://a.com" as never) // location probe 1
    +      .mockResolvedValueOnce("https://b.com" as never) // location probe 2: changed
    +      .mockResolvedValueOnce("https://c.com" as never) // location probe 3: changed again
    +      .mockResolvedValueOnce("https://d.com" as never); // follow-up: still changing
    +
    +    const handler = getActPostHandler();
    +    const response = createBrowserRouteResponse();
    +    const pending =
    +      handler?.(
    +        { params: {}, query: {}, body: { kind: "evaluate", fn: "() => 1" } },
    +        response.res,
    +      ) ?? Promise.resolve();
    +    void pending.catch(() => {});
    +    const completion = (async () => {
    +      await vi.runAllTimersAsync();
    +      await pending;
    +    })();
    +
    +    await expect(completion).rejects.toThrow("Unable to verify stable post-interaction navigation");
    +  });
    +
    +  it("fails closed when a probe error follows two stable reads", async () => {
    +    // Probes 1 + 2 match (sawStableAllowedUrl would be true), probe 3 throws.
    +    // Guard must NOT return success — the throw invalidates prior stability.
    +    chromeMcpMocks.evaluateChromeMcpScript
    +      .mockResolvedValueOnce("result" as never) // action evaluate result
    +      .mockResolvedValueOnce("https://example.com" as never) // location probe 1
    +      .mockResolvedValueOnce("https://example.com" as never) // location probe 2 → stable pair
    +      .mockRejectedValueOnce(new Error("context destroyed") as never) // location probe 3 → error
    +      .mockRejectedValueOnce(new Error("context destroyed") as never); // follow-up → still errored
    +
    +    const handler = getActPostHandler();
    +    const response = createBrowserRouteResponse();
    +    const pending =
    +      handler?.(
    +        { params: {}, query: {}, body: { kind: "evaluate", fn: "() => 1" } },
    +        response.res,
    +      ) ?? Promise.resolve();
    +    void pending.catch(() => {});
    +    const completion = (async () => {
    +      await vi.runAllTimersAsync();
    +      await pending;
    +    })();
    +
    +    await expect(completion).rejects.toThrow("Unable to verify stable post-interaction navigation");
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenCalledTimes(2);
    +  });
    +
    +  it("skips the guard when no SSRF policy is configured", async () => {
    +    const response = await runAction({ kind: "press", key: "Enter" }, null);
    +
    +    expect(response.statusCode).toBe(200);
    +    expect(chromeMcpMocks.pressChromeMcpKey).toHaveBeenCalledOnce();
    +    expect(chromeMcpMocks.evaluateChromeMcpScript).not.toHaveBeenCalled();
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).not.toHaveBeenCalled();
    +  });
    +
    +  it("still probes navigation when the interaction command throws", async () => {
    +    chromeMcpMocks.clickChromeMcpElement.mockImplementationOnce(() => {
    +      throw new Error("stale element");
    +    });
    +
    +    const handler = getActPostHandler();
    +    const response = createBrowserRouteResponse();
    +    const pending =
    +      handler?.({ params: {}, query: {}, body: { kind: "click", ref: "btn-1" } }, response.res) ??
    +      Promise.resolve();
    +    void pending.catch(() => {});
    +    const completion = (async () => {
    +      await vi.runAllTimersAsync();
    +      await pending;
    +    })();
    +
    +    await expect(completion).rejects.toThrow("stale element");
    +    expect(chromeMcpMocks.evaluateChromeMcpScript).toHaveBeenCalled();
    +    expect(navigationGuardMocks.assertBrowserNavigationResultAllowed).toHaveBeenCalled();
    +  });
    +});
    
  • extensions/browser/src/browser/routes/agent.act.ts+222 62 modified
    @@ -11,6 +11,11 @@ import {
       resizeChromeMcpPage,
     } from "../chrome-mcp.js";
     import type { BrowserActRequest } from "../client-actions.types.js";
    +import {
    +  assertBrowserNavigationResultAllowed,
    +  type BrowserNavigationPolicyOptions,
    +  withBrowserNavigationPolicy,
    +} from "../navigation-guard.js";
     import { getBrowserProfileCapabilities } from "../profile-capabilities.js";
     import type { BrowserRouteContext } from "../server-context.js";
     import { matchBrowserUrlPattern } from "../url-pattern.js";
    @@ -38,6 +43,118 @@ function sleep(ms: number): Promise<void> {
       return new Promise((resolve) => setTimeout(resolve, ms));
     }
     
    +const EXISTING_SESSION_INTERACTION_NAVIGATION_RECHECK_DELAYS_MS = [0, 250, 500] as const;
    +
    +async function readExistingSessionLocationHref(params: {
    +  profileName: string;
    +  userDataDir?: string;
    +  targetId: string;
    +}): Promise<string> {
    +  const currentUrl = await evaluateChromeMcpScript({
    +    profileName: params.profileName,
    +    userDataDir: params.userDataDir,
    +    targetId: params.targetId,
    +    fn: "() => window.location.href",
    +  });
    +  if (typeof currentUrl !== "string") {
    +    throw new Error("Location probe returned a non-string result");
    +  }
    +  const normalizedUrl = currentUrl.trim();
    +  if (!normalizedUrl) {
    +    throw new Error("Location probe returned an empty URL");
    +  }
    +  return normalizedUrl;
    +}
    +
    +async function assertExistingSessionPostInteractionNavigationAllowed(params: {
    +  profileName: string;
    +  userDataDir?: string;
    +  targetId: string;
    +  ssrfPolicy?: BrowserNavigationPolicyOptions["ssrfPolicy"];
    +}): Promise<void> {
    +  const ssrfPolicyOpts = withBrowserNavigationPolicy(params.ssrfPolicy);
    +  if (!ssrfPolicyOpts.ssrfPolicy) {
    +    return;
    +  }
    +
    +  let lastObservedUrl: string | undefined;
    +  let sawStableAllowedUrl = false;
    +  for (const delayMs of EXISTING_SESSION_INTERACTION_NAVIGATION_RECHECK_DELAYS_MS) {
    +    if (delayMs > 0) {
    +      await sleep(delayMs);
    +    }
    +    let currentUrl: string;
    +    try {
    +      currentUrl = await readExistingSessionLocationHref(params);
    +    } catch {
    +      sawStableAllowedUrl = false;
    +      continue;
    +    }
    +    await assertBrowserNavigationResultAllowed({
    +      url: currentUrl,
    +      ...ssrfPolicyOpts,
    +    });
    +    if (currentUrl === lastObservedUrl) {
    +      sawStableAllowedUrl = true;
    +    } else {
    +      sawStableAllowedUrl = false;
    +    }
    +    lastObservedUrl = currentUrl;
    +  }
    +
    +  if (sawStableAllowedUrl) {
    +    return;
    +  }
    +
    +  // If the loop exhausted without confirming stability but we did observe
    +  // at least one allowed URL, run a single follow-up probe so a late URL
    +  // transition that has already settled is not treated as a false failure.
    +  if (lastObservedUrl) {
    +    const lastDelay =
    +      EXISTING_SESSION_INTERACTION_NAVIGATION_RECHECK_DELAYS_MS[
    +        EXISTING_SESSION_INTERACTION_NAVIGATION_RECHECK_DELAYS_MS.length - 1
    +      ];
    +    await sleep(lastDelay);
    +    try {
    +      const followUpUrl = await readExistingSessionLocationHref(params);
    +      await assertBrowserNavigationResultAllowed({
    +        url: followUpUrl,
    +        ...ssrfPolicyOpts,
    +      });
    +      if (followUpUrl === lastObservedUrl) {
    +        return;
    +      }
    +    } catch {
    +      // Probe failed — fall through to throw
    +    }
    +  }
    +
    +  throw new Error("Unable to verify stable post-interaction navigation");
    +}
    +
    +async function runExistingSessionActionWithNavigationGuard<T>(params: {
    +  execute: () => Promise<T>;
    +  guard?: Parameters<typeof assertExistingSessionPostInteractionNavigationAllowed>[0];
    +}): Promise<T> {
    +  let actionError: unknown;
    +  let result: T | undefined;
    +  try {
    +    result = await params.execute();
    +  } catch (error) {
    +    actionError = error;
    +  }
    +
    +  if (params.guard) {
    +    await assertExistingSessionPostInteractionNavigationAllowed(params.guard);
    +  }
    +
    +  if (actionError) {
    +    throw actionError;
    +  }
    +
    +  return result as T;
    +}
    +
     function buildExistingSessionWaitPredicate(params: {
       text?: string;
       textGone?: string;
    @@ -250,6 +367,12 @@ export function registerBrowserAgentActRoutes(
             const isExistingSession = getBrowserProfileCapabilities(profileCtx.profile).usesChromeMcp;
             const profileName = profileCtx.profile.name;
             if (isExistingSession) {
    +          const existingSessionNavigationGuard = {
    +            profileName,
    +            userDataDir: profileCtx.profile.userDataDir,
    +            targetId: tab.targetId,
    +            ssrfPolicy,
    +          };
               const unsupportedMessage = getExistingSessionUnsupportedMessage(action);
               if (unsupportedMessage) {
                 return jsonActError(
    @@ -261,83 +384,116 @@ export function registerBrowserAgentActRoutes(
               }
               switch (action.kind) {
                 case "click":
    -              await clickChromeMcpElement({
    -                profileName,
    -                userDataDir: profileCtx.profile.userDataDir,
    -                targetId: tab.targetId,
    -                uid: action.ref!,
    -                doubleClick: action.doubleClick ?? false,
    +              await runExistingSessionActionWithNavigationGuard({
    +                execute: () =>
    +                  clickChromeMcpElement({
    +                    profileName,
    +                    userDataDir: profileCtx.profile.userDataDir,
    +                    targetId: tab.targetId,
    +                    uid: action.ref!,
    +                    doubleClick: action.doubleClick ?? false,
    +                  }),
    +                guard: existingSessionNavigationGuard,
                   });
                   return res.json({ ok: true, targetId: tab.targetId, url: tab.url });
                 case "type":
    -              await fillChromeMcpElement({
    -                profileName,
    -                userDataDir: profileCtx.profile.userDataDir,
    -                targetId: tab.targetId,
    -                uid: action.ref!,
    -                value: action.text,
    +              await runExistingSessionActionWithNavigationGuard({
    +                execute: async () => {
    +                  await fillChromeMcpElement({
    +                    profileName,
    +                    userDataDir: profileCtx.profile.userDataDir,
    +                    targetId: tab.targetId,
    +                    uid: action.ref!,
    +                    value: action.text,
    +                  });
    +                  if (action.submit) {
    +                    await pressChromeMcpKey({
    +                      profileName,
    +                      userDataDir: profileCtx.profile.userDataDir,
    +                      targetId: tab.targetId,
    +                      key: "Enter",
    +                    });
    +                  }
    +                },
    +                guard: existingSessionNavigationGuard,
                   });
    -              if (action.submit) {
    -                await pressChromeMcpKey({
    -                  profileName,
    -                  userDataDir: profileCtx.profile.userDataDir,
    -                  targetId: tab.targetId,
    -                  key: "Enter",
    -                });
    -              }
                   return res.json({ ok: true, targetId: tab.targetId });
                 case "press":
    -              await pressChromeMcpKey({
    -                profileName,
    -                userDataDir: profileCtx.profile.userDataDir,
    -                targetId: tab.targetId,
    -                key: action.key,
    +              await runExistingSessionActionWithNavigationGuard({
    +                execute: () =>
    +                  pressChromeMcpKey({
    +                    profileName,
    +                    userDataDir: profileCtx.profile.userDataDir,
    +                    targetId: tab.targetId,
    +                    key: action.key,
    +                  }),
    +                guard: existingSessionNavigationGuard,
                   });
                   return res.json({ ok: true, targetId: tab.targetId });
                 case "hover":
    -              await hoverChromeMcpElement({
    -                profileName,
    -                userDataDir: profileCtx.profile.userDataDir,
    -                targetId: tab.targetId,
    -                uid: action.ref!,
    +              await runExistingSessionActionWithNavigationGuard({
    +                execute: () =>
    +                  hoverChromeMcpElement({
    +                    profileName,
    +                    userDataDir: profileCtx.profile.userDataDir,
    +                    targetId: tab.targetId,
    +                    uid: action.ref!,
    +                  }),
    +                guard: existingSessionNavigationGuard,
                   });
                   return res.json({ ok: true, targetId: tab.targetId });
                 case "scrollIntoView":
    -              await evaluateChromeMcpScript({
    -                profileName,
    -                userDataDir: profileCtx.profile.userDataDir,
    -                targetId: tab.targetId,
    -                fn: `(el) => { el.scrollIntoView({ block: "center", inline: "center" }); return true; }`,
    -                args: [action.ref!],
    +              await runExistingSessionActionWithNavigationGuard({
    +                execute: () =>
    +                  evaluateChromeMcpScript({
    +                    profileName,
    +                    userDataDir: profileCtx.profile.userDataDir,
    +                    targetId: tab.targetId,
    +                    fn: `(el) => { el.scrollIntoView({ block: "center", inline: "center" }); return true; }`,
    +                    args: [action.ref!],
    +                  }),
    +                guard: existingSessionNavigationGuard,
                   });
                   return res.json({ ok: true, targetId: tab.targetId });
                 case "drag":
    -              await dragChromeMcpElement({
    -                profileName,
    -                userDataDir: profileCtx.profile.userDataDir,
    -                targetId: tab.targetId,
    -                fromUid: action.startRef!,
    -                toUid: action.endRef!,
    +              await runExistingSessionActionWithNavigationGuard({
    +                execute: () =>
    +                  dragChromeMcpElement({
    +                    profileName,
    +                    userDataDir: profileCtx.profile.userDataDir,
    +                    targetId: tab.targetId,
    +                    fromUid: action.startRef!,
    +                    toUid: action.endRef!,
    +                  }),
    +                guard: existingSessionNavigationGuard,
                   });
                   return res.json({ ok: true, targetId: tab.targetId });
                 case "select":
    -              await fillChromeMcpElement({
    -                profileName,
    -                userDataDir: profileCtx.profile.userDataDir,
    -                targetId: tab.targetId,
    -                uid: action.ref!,
    -                value: action.values[0] ?? "",
    +              await runExistingSessionActionWithNavigationGuard({
    +                execute: () =>
    +                  fillChromeMcpElement({
    +                    profileName,
    +                    userDataDir: profileCtx.profile.userDataDir,
    +                    targetId: tab.targetId,
    +                    uid: action.ref!,
    +                    value: action.values[0] ?? "",
    +                  }),
    +                guard: existingSessionNavigationGuard,
                   });
                   return res.json({ ok: true, targetId: tab.targetId });
                 case "fill":
    -              await fillChromeMcpForm({
    -                profileName,
    -                userDataDir: profileCtx.profile.userDataDir,
    -                targetId: tab.targetId,
    -                elements: action.fields.map((field) => ({
    -                  uid: field.ref,
    -                  value: String(field.value ?? ""),
    -                })),
    +              await runExistingSessionActionWithNavigationGuard({
    +                execute: () =>
    +                  fillChromeMcpForm({
    +                    profileName,
    +                    userDataDir: profileCtx.profile.userDataDir,
    +                    targetId: tab.targetId,
    +                    elements: action.fields.map((field) => ({
    +                      uid: field.ref,
    +                      value: String(field.value ?? ""),
    +                    })),
    +                  }),
    +                guard: existingSessionNavigationGuard,
                   });
                   return res.json({ ok: true, targetId: tab.targetId });
                 case "resize":
    @@ -365,12 +521,16 @@ export function registerBrowserAgentActRoutes(
                   });
                   return res.json({ ok: true, targetId: tab.targetId });
                 case "evaluate": {
    -              const result = await evaluateChromeMcpScript({
    -                profileName,
    -                userDataDir: profileCtx.profile.userDataDir,
    -                targetId: tab.targetId,
    -                fn: action.fn,
    -                args: action.ref ? [action.ref] : undefined,
    +              const result = await runExistingSessionActionWithNavigationGuard({
    +                execute: () =>
    +                  evaluateChromeMcpScript({
    +                    profileName,
    +                    userDataDir: profileCtx.profile.userDataDir,
    +                    targetId: tab.targetId,
    +                    fn: action.fn,
    +                    args: action.ref ? [action.ref] : undefined,
    +                  }),
    +                guard: existingSessionNavigationGuard,
                   });
                   return res.json({
                     ok: true,
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

8