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.
| Package | Affected versions | Patched versions |
|---|---|---|
openclawnpm | < 2026.4.10 | 2026.4.10 |
Affected products
2Patches
1daeb74920d5afix(browser): guard existing-session navigation (#64370)
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- github.com/openclaw/openclaw/commit/daeb74920d5ad986cb600625180037e23221e93anvdPatchWEB
- github.com/advisories/GHSA-527m-976r-jf79ghsaADVISORY
- github.com/openclaw/openclaw/security/advisories/GHSA-527m-976r-jf79nvdVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-43573ghsaADVISORY
- www.vulncheck.com/advisories/openclaw-ssrf-policy-bypass-in-existing-session-browser-interaction-routesnvdThird Party AdvisoryWEB
- github.com/openclaw/openclaw/pull/64370ghsaWEB
News mentions
8- 30 ClawHub skills secretly turn AI agents into a crypto swarmThe Register Security · Apr 29, 2026
- 27th April – Threat Intelligence ReportCheck Point Research · Apr 27, 2026
- Agents that remember: introducing Agent MemoryCloudflare Blog · Apr 17, 2026
- The Increasing Role of AI in Vulnerability ResearchWordfence Blog · Apr 10, 2026
- 16th March – Threat Intelligence ReportCheck Point Research · Mar 16, 2026
- How AI Assistants are Moving the Security GoalpostsKrebs on Security · Mar 8, 2026
- Risky Business #827 -- Iranian cyber threat actors are down but not outRisky Business · Mar 4, 2026
- Risky Business #826 -- A week of AI mishaps and skulduggeryRisky Business · Feb 25, 2026