VYPR
Medium severity6.1NVD Advisory· Published May 18, 2026· Updated May 19, 2026

CVE-2026-45243

CVE-2026-45243

Description

Summarize prior to 0.15.1 contains a missing authorization vulnerability in the content script window.postMessage bridge that allows malicious pages to perform unauthorized operations on automation artifacts. Attackers can simulate runtime messages with spoofed sender identifiers to list, read, create, overwrite, or delete automation artifacts scoped to the affected tab without proper authorization checks.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A missing authorization bug in Summarize's content script bridge lets any page script list, read, create, or delete tab-scoped automation artifacts.

Vulnerability

Summarize versions prior to 0.15.1 have a missing authorization vulnerability in the content script window.postMessage bridge used for automation artifacts. The content script automation.content.ts is installed on <all_urls> and listens for message events with source: "summarize-artifacts" from any page. It then forwards attacker-controlled action and payload values to the background worker as automation:artifacts messages. The background worker accepts artifact operations based solely on sender.tab.id without verifying the message origin or requiring user approval [1][4].

Exploitation

An attacker needs only the ability to execute JavaScript in a page that has the Summarize extension loaded. There is no need for authentication, write access, or user interaction beyond the normal page load. The attacker can simulate runtime messages with a spoofed sender identifier (source: "summarize-artifacts") via window.postMessage and control the action and payload fields to perform any supported artifact operation on the tab's automation artifacts [1].

Impact

A successful attacker can list, read, create, overwrite, or delete automation artifacts scoped to the affected tab. This enables unauthorized data access (information disclosure), potential data corruption, and loss of availability for those artifacts. The compromise occurs at the privilege level of the extension storage accessible from the tab, without any active trusted BrowserJS run or nonce [1][4].

Mitigation

The vulnerability is fixed in Summarize version 0.15.1 (released 2026-05-18) by removing the page-visible window.postMessage bridge and moving artifact RPCs to the chrome.runtime.onUserScriptMessage path with userScripts messaging, requiring the tab to be explicitly armed before the background worker accepts artifact messages [1][2][3]. Users should update to 0.15.1 or later.

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

1
357544063af5

[security] fix(extension): guard automation artifacts bridge (#222)

https://github.com/steipete/summarizeHinotobiMay 13, 2026via nvd-ref
8 files changed · +328 84
  • apps/chrome-extension/src/automation/native-input-guard.ts+72 9 modified
    @@ -4,7 +4,13 @@ export type NativeInputArmMessage = {
       enabled: boolean;
     };
     
    -export function updateNativeInputArmedTabs(args: {
    +export type ArtifactsArmMessage = {
    +  type: "automation:artifacts-arm";
    +  tabId: number;
    +  enabled: boolean;
    +};
    +
    +export function updateArmedTabs(args: {
       armedTabs: Set<number>;
       senderHasTab: boolean;
       tabId?: number;
    @@ -17,28 +23,85 @@ export function updateNativeInputArmedTabs(args: {
       return true;
     }
     
    -export function getNativeInputGuardError(args: {
    +export function updateNativeInputArmedTabs(args: {
    +  armedTabs: Set<number>;
    +  senderHasTab: boolean;
    +  tabId?: number;
    +  enabled?: boolean;
    +}): boolean {
    +  return updateArmedTabs(args);
    +}
    +
    +export function getArmedTabGuardError(args: {
       armedTabs: Set<number>;
       senderTabId?: number;
    +  feature: string;
     }): string | null {
    -  const { armedTabs, senderTabId } = args;
    +  const { armedTabs, senderTabId, feature } = args;
       if (typeof senderTabId !== "number") return "Missing sender tab";
    -  if (!armedTabs.has(senderTabId)) return "Native input not armed for this tab";
    +  if (!armedTabs.has(senderTabId)) return `${feature} not armed for this tab`;
       return null;
     }
     
    -export async function withNativeInputArmedTab<T>(args: {
    +export function getNativeInputGuardError(args: {
    +  armedTabs: Set<number>;
    +  senderTabId?: number;
    +}): string | null {
    +  return getArmedTabGuardError({ ...args, feature: "Native input" });
    +}
    +
    +export function getArtifactsGuardError(args: {
    +  armedTabs: Set<number>;
    +  senderTabId?: number;
    +}): string | null {
    +  return getArmedTabGuardError({ ...args, feature: "Artifacts bridge" });
    +}
    +
    +export async function withArmedTab<T, TMessage extends { tabId: number; enabled: boolean }>(args: {
       enabled: boolean;
       tabId: number;
    -  sendMessage: (message: NativeInputArmMessage) => Promise<unknown>;
    +  armMessage: (input: { tabId: number; enabled: boolean }) => TMessage;
    +  sendMessage: (message: TMessage) => Promise<unknown>;
       run: () => Promise<T>;
     }): Promise<T> {
    -  const { enabled, tabId, sendMessage, run } = args;
    +  const { enabled, tabId, armMessage, sendMessage, run } = args;
       if (!enabled) return run();
    -  await sendMessage({ type: "automation:native-input-arm", tabId, enabled: true });
    +  await sendMessage(armMessage({ tabId, enabled: true }));
       try {
         return await run();
       } finally {
    -    void sendMessage({ type: "automation:native-input-arm", tabId, enabled: false });
    +    await sendMessage(armMessage({ tabId, enabled: false }));
       }
     }
    +
    +export async function withNativeInputArmedTab<T>(args: {
    +  enabled: boolean;
    +  tabId: number;
    +  sendMessage: (message: NativeInputArmMessage) => Promise<unknown>;
    +  run: () => Promise<T>;
    +}): Promise<T> {
    +  return withArmedTab({
    +    ...args,
    +    armMessage: ({ tabId, enabled }) => ({
    +      type: "automation:native-input-arm" as const,
    +      tabId,
    +      enabled,
    +    }),
    +  });
    +}
    +
    +export async function withArtifactsArmedTab<T>(args: {
    +  enabled: boolean;
    +  tabId: number;
    +  sendMessage: (message: ArtifactsArmMessage) => Promise<unknown>;
    +  run: () => Promise<T>;
    +}): Promise<T> {
    +  return withArmedTab({
    +    ...args,
    +    armMessage: ({ tabId, enabled }) => ({
    +      type: "automation:artifacts-arm" as const,
    +      tabId,
    +      enabled,
    +    }),
    +  });
    +}
    
  • apps/chrome-extension/src/automation/repl.ts+36 30 modified
    @@ -5,7 +5,7 @@ import {
       parseArtifact,
       upsertArtifact,
     } from "./artifacts-store";
    -import { withNativeInputArmedTab } from "./native-input-guard";
    +import { withArtifactsArmedTab, withNativeInputArmedTab } from "./native-input-guard";
     import { executeNavigateTool } from "./navigate";
     import { listSkills } from "./skills-store";
     import { buildUserScriptsGuidance, getUserScriptsStatus } from "./userscripts";
    @@ -229,17 +229,17 @@ async function runBrowserJs(
     
             const sendArtifactRpc = (action, payload) => {
               return new Promise((resolve, reject) => {
    -            const requestId = \`\${Date.now()}-\${Math.random().toString(36).slice(2)}\`
    -            const handler = (event) => {
    -              if (event.source !== window) return
    -              const msg = event.data || {}
    -              if (msg?.source !== 'summarize-artifacts' || msg.requestId !== requestId) return
    -              window.removeEventListener('message', handler)
    -              if (msg.ok) resolve(msg.result)
    -              else reject(new Error(msg.error || 'Artifact operation failed'))
    -            }
    -            window.addEventListener('message', handler)
    -            window.postMessage({ source: 'summarize-artifacts', requestId, action, payload }, '*')
    +            chrome.runtime.sendMessage(
    +              { source: 'summarize-artifacts', type: 'automation:artifacts', action, payload },
    +              (response) => {
    +                if (chrome.runtime.lastError) {
    +                  reject(new Error(chrome.runtime.lastError.message || 'Artifact operation failed'))
    +                  return
    +                }
    +                if (response?.ok) resolve(response.result)
    +                else reject(new Error(response?.error || 'Artifact operation failed'))
    +              }
    +            )
               })
             }
     
    @@ -305,35 +305,41 @@ async function runBrowserJs(
       try {
         await userScripts.configureWorld?.({
           worldId: "summarize-browserjs",
    -      messaging: false,
    +      messaging: true,
           csp: "script-src 'unsafe-eval' 'unsafe-inline'; connect-src 'none'; img-src 'none'; media-src 'none'; frame-src 'none'; font-src 'none'; object-src 'none'; default-src 'none';",
         });
       } catch {
         // ignore
       }
     
       try {
    -    return await withNativeInputArmedTab({
    -      enabled: nativeInputEnabled,
    +    return await withArtifactsArmedTab({
    +      enabled: true,
           tabId: tab.id,
           sendMessage: (message) => chrome.runtime.sendMessage(message),
    -      run: async () => {
    -        const results = await userScripts.execute({
    -          target: { tabId: tab.id },
    -          world: "USER_SCRIPT",
    -          worldId: "summarize-browserjs",
    -          injectImmediately: true,
    -          js: [{ code: wrapperCode }],
    -          ...(executionId ? { executionId } : {}),
    -        });
    +      run: async () =>
    +        withNativeInputArmedTab({
    +          enabled: nativeInputEnabled,
    +          tabId: tab.id,
    +          sendMessage: (message) => chrome.runtime.sendMessage(message),
    +          run: async () => {
    +            const results = await userScripts.execute({
    +              target: { tabId: tab.id },
    +              world: "USER_SCRIPT",
    +              worldId: "summarize-browserjs",
    +              injectImmediately: true,
    +              js: [{ code: wrapperCode }],
    +              ...(executionId ? { executionId } : {}),
    +            });
     
    -        if (signal?.aborted) {
    -          return { ok: false, error: "Execution aborted" };
    -        }
    +            if (signal?.aborted) {
    +              return { ok: false, error: "Execution aborted" };
    +            }
     
    -        const result = results?.[0]?.result as BrowserJsResult | undefined;
    -        return result ?? { ok: false, error: "No result from browserjs()" };
    -      },
    +            const result = results?.[0]?.result as BrowserJsResult | undefined;
    +            return result ?? { ok: false, error: "No result from browserjs()" };
    +          },
    +        }),
         });
       } finally {
         if (abortHandler) signal?.removeEventListener("abort", abortHandler);
    
  • apps/chrome-extension/src/entrypoints/automation.content.ts+0 35 modified
    @@ -361,47 +361,12 @@ function handleNativeInputBridge() {
       });
     }
     
    -// Bridge artifact RPC calls from userScripts (page world) to the extension.
    -function handleArtifactsBridge() {
    -  window.addEventListener("message", (event) => {
    -    if (event.source !== window) return;
    -    const data = event.data as {
    -      source?: string;
    -      requestId?: string;
    -      action?: string;
    -      payload?: unknown;
    -    };
    -    if (data?.source !== "summarize-artifacts" || !data.requestId) return;
    -    chrome.runtime.sendMessage(
    -      {
    -        type: "automation:artifacts",
    -        requestId: data.requestId,
    -        action: data.action,
    -        payload: data.payload,
    -      },
    -      (response: { ok: boolean; result?: unknown; error?: string } | undefined) => {
    -        window.postMessage(
    -          {
    -            source: "summarize-artifacts",
    -            requestId: data.requestId,
    -            ok: response?.ok ?? false,
    -            result: response?.result,
    -            error: response?.error,
    -          },
    -          "*",
    -        );
    -      },
    -    );
    -  });
    -}
    -
     export default defineContentScript({
       matches: ["<all_urls>"],
       excludeMatches: META_SITE_EXCLUDE_MATCHES,
       runAt: "document_idle",
       main() {
         handleNativeInputBridge();
    -    handleArtifactsBridge();
     
         chrome.runtime.onMessage.addListener(
           (
    
  • apps/chrome-extension/src/entrypoints/background/listeners.ts+5 0 modified
    @@ -55,6 +55,11 @@ export function bindBackgroundListeners<Session extends SessionWithNavAt>(option
         );
       });
     
    +  chrome.runtime.onUserScriptMessage?.addListener(
    +    (raw, sender, sendResponse): boolean | undefined =>
    +      runtimeActionsHandler(raw, sender, sendResponse),
    +  );
    +
       chrome.storage.onChanged.addListener((changes, areaName) => {
         if (areaName !== "local") return;
         if (!changes.settings) return;
    
  • apps/chrome-extension/src/entrypoints/background/runtime-actions.ts+22 9 modified
    @@ -6,8 +6,9 @@ import {
       upsertArtifact,
     } from "../../automation/artifacts-store";
     import {
    +  getArtifactsGuardError,
       getNativeInputGuardError,
    -  updateNativeInputArmedTabs,
    +  updateArmedTabs,
     } from "../../automation/native-input-guard";
     
     export type NativeInputRequest = {
    @@ -33,7 +34,8 @@ export type ArtifactsRequest = {
     type RuntimeMessage =
       | NativeInputRequest
       | ArtifactsRequest
    -  | { type: "automation:native-input-arm" };
    +  | { type: "automation:native-input-arm"; tabId?: number; enabled?: boolean }
    +  | { type: "automation:artifacts-arm"; tabId?: number; enabled?: boolean };
     
     function safeSendResponse(sendResponse: (response?: unknown) => void, value: unknown) {
       try {
    @@ -154,7 +156,13 @@ async function dispatchNativeInput(
       }
     }
     
    -export function createRuntimeActionsHandler({ armedTabs }: { armedTabs: Set<number> }) {
    +export function createRuntimeActionsHandler({
    +  nativeInputArmedTabs,
    +  artifactsArmedTabs,
    +}: {
    +  nativeInputArmedTabs: Set<number>;
    +  artifactsArmedTabs: Set<number>;
    +}) {
       return (
         raw: unknown,
         sender: chrome.runtime.MessageSender,
    @@ -165,10 +173,11 @@ export function createRuntimeActionsHandler({ armedTabs }: { armedTabs: Set<numb
         }
     
         const type = (raw as RuntimeMessage).type;
    -    if (type === "automation:native-input-arm") {
    +    if (type === "automation:native-input-arm" || type === "automation:artifacts-arm") {
           const msg = raw as { tabId?: number; enabled?: boolean };
    -      updateNativeInputArmedTabs({
    -        armedTabs,
    +      updateArmedTabs({
    +        armedTabs:
    +          type === "automation:native-input-arm" ? nativeInputArmedTabs : artifactsArmedTabs,
             senderHasTab: Boolean(sender.tab),
             tabId: msg.tabId,
             enabled: msg.enabled,
    @@ -181,7 +190,7 @@ export function createRuntimeActionsHandler({ armedTabs }: { armedTabs: Set<numb
           void (async () => {
             const tabId = sender.tab?.id;
             const guardError = getNativeInputGuardError({
    -          armedTabs,
    +          armedTabs: nativeInputArmedTabs,
               senderTabId: tabId,
             });
             if (guardError) {
    @@ -202,8 +211,12 @@ export function createRuntimeActionsHandler({ armedTabs }: { armedTabs: Set<numb
         const msg = raw as ArtifactsRequest;
         void (async () => {
           const tabId = sender.tab?.id;
    -      if (!tabId) {
    -        safeSendResponse(sendResponse, { ok: false, error: "Missing sender tab" });
    +      const guardError = getArtifactsGuardError({
    +        armedTabs: artifactsArmedTabs,
    +        senderTabId: tabId,
    +      });
    +      if (guardError) {
    +        safeSendResponse(sendResponse, { ok: false, error: guardError });
             return;
           }
     
    
  • apps/chrome-extension/src/entrypoints/background.ts+4 1 modified
    @@ -57,6 +57,7 @@ export default defineBackground(() => {
       // Prevents arbitrary pages from triggering trusted clicks via the
       // postMessage → content-script → runtime bridge.
       const nativeInputArmedTabs = new Set<number>();
    +  const artifactsArmedTabs = new Set<number>();
     
       function resolveLogLevel(event: string) {
         const normalized = event.toLowerCase();
    @@ -75,7 +76,8 @@ export default defineBackground(() => {
         console.debug("[summarize][extractor]", { event, ...detailPayload });
       };
       const runtimeActionsHandler = createRuntimeActionsHandler({
    -    armedTabs: nativeInputArmedTabs,
    +    nativeInputArmedTabs,
    +    artifactsArmedTabs,
       });
       const hoverController = createHoverController({
         hoverControllersByTabId,
    @@ -412,6 +414,7 @@ export default defineBackground(() => {
         onTabRemoved: (tabId) => {
           hoverController.abortHoverForTab(tabId);
           nativeInputArmedTabs.delete(tabId);
    +      artifactsArmedTabs.delete(tabId);
         },
       });
     
    
  • tests/background-listeners.userscript.test.ts+65 0 added
    @@ -0,0 +1,65 @@
    +import { describe, expect, it, vi } from "vitest";
    +import { bindBackgroundListeners } from "../apps/chrome-extension/src/entrypoints/background/listeners";
    +
    +function installChromeListenerStubs() {
    +  const onMessage = { addListener: vi.fn() };
    +  const onUserScriptMessage = { addListener: vi.fn() };
    +  (globalThis as unknown as { chrome: unknown }).chrome = {
    +    runtime: {
    +      onConnect: { addListener: vi.fn() },
    +      onMessage,
    +      onUserScriptMessage,
    +    },
    +    storage: { onChanged: { addListener: vi.fn() } },
    +    webNavigation: { onHistoryStateUpdated: { addListener: vi.fn() } },
    +    tabs: {
    +      onActivated: { addListener: vi.fn() },
    +      onUpdated: { addListener: vi.fn() },
    +      onRemoved: { addListener: vi.fn() },
    +    },
    +  };
    +  return { onMessage, onUserScriptMessage };
    +}
    +
    +describe("background userScripts runtime messages", () => {
    +  it("routes userScripts messages through runtime actions", () => {
    +    const { onUserScriptMessage } = installChromeListenerStubs();
    +    const runtimeActionsHandler = vi.fn(() => true);
    +    const hoverRuntimeHandler = vi.fn(() => false);
    +
    +    bindBackgroundListeners({
    +      panelSessionStore: {
    +        registerPanelSession: vi.fn(),
    +        deletePanelSession: vi.fn(),
    +        getPanelSession: vi.fn(() => null),
    +        getPanelSessions: vi.fn(() => []),
    +        clearCachedExtractsForWindow: vi.fn(async () => undefined),
    +        clearTab: vi.fn(),
    +      },
    +      handlePanelMessage: vi.fn(),
    +      onPanelDisconnect: vi.fn(),
    +      runtimeActionsHandler,
    +      hoverRuntimeHandler,
    +      emitState: vi.fn(),
    +      summarizeActiveTab: vi.fn(),
    +      onTabRemoved: vi.fn(),
    +    });
    +
    +    const listener = onUserScriptMessage.addListener.mock.calls[0]?.[0];
    +    expect(listener).toBeTypeOf("function");
    +    const sendResponse = vi.fn();
    +    const result = listener?.(
    +      { type: "automation:artifacts", action: "listArtifacts" },
    +      { tab: { id: 123 } },
    +      sendResponse,
    +    );
    +
    +    expect(result).toBe(true);
    +    expect(runtimeActionsHandler).toHaveBeenCalledWith(
    +      { type: "automation:artifacts", action: "listArtifacts" },
    +      { tab: { id: 123 } },
    +      sendResponse,
    +    );
    +    expect(hoverRuntimeHandler).not.toHaveBeenCalled();
    +  });
    +});
    
  • tests/runtime-actions.artifacts-security.test.ts+124 0 added
    @@ -0,0 +1,124 @@
    +import { beforeEach, describe, expect, it, vi } from "vitest";
    +import { createRuntimeActionsHandler } from "../apps/chrome-extension/src/entrypoints/background/runtime-actions";
    +
    +const storage = new Map<string, unknown>();
    +const TAB_ID = 777;
    +
    +function installChromeStorage() {
    +  (globalThis as unknown as { chrome: unknown }).chrome = {
    +    storage: {
    +      session: {
    +        async get(key: string) {
    +          return { [key]: storage.get(key) };
    +        },
    +        async set(value: Record<string, unknown>) {
    +          for (const [key, val] of Object.entries(value)) storage.set(key, val);
    +        },
    +      },
    +    },
    +  };
    +}
    +
    +function createHandler() {
    +  return createRuntimeActionsHandler({
    +    nativeInputArmedTabs: new Set<number>(),
    +    artifactsArmedTabs: new Set<number>(),
    +  });
    +}
    +
    +function dispatchArtifact(
    +  handler: ReturnType<typeof createHandler>,
    +  raw: unknown,
    +  tabId: number | undefined = TAB_ID,
    +): Promise<unknown> {
    +  return new Promise((resolve) => {
    +    const sender = typeof tabId === "number" ? ({ tab: { id: tabId } } as any) : ({} as any);
    +    const ret = handler(raw, sender, resolve);
    +    expect(ret).toBe(true);
    +  });
    +}
    +
    +describe("runtime artifact bridge guard", () => {
    +  beforeEach(() => {
    +    storage.clear();
    +    installChromeStorage();
    +  });
    +
    +  it("blocks page-origin artifact reads unless the tab is armed by extension code", async () => {
    +    storage.set(`automation.artifacts.${TAB_ID}`, {
    +      "legit-secret.txt": {
    +        fileName: "legit-secret.txt",
    +        mimeType: "text/plain",
    +        contentBase64: btoa("LEGIT_AUTOMATION_ARTIFACT_SECRET"),
    +        size: 32,
    +        createdAt: "2026-01-01T00:00:00.000Z",
    +        updatedAt: "2026-01-01T00:00:00.000Z",
    +      },
    +    });
    +    const handler = createHandler();
    +
    +    const blocked = await dispatchArtifact(handler, {
    +      type: "automation:artifacts",
    +      requestId: "attacker-read-existing",
    +      action: "getArtifact",
    +      payload: { fileName: "legit-secret.txt" },
    +    });
    +
    +    expect(blocked).toEqual({ ok: false, error: "Artifacts bridge not armed for this tab" });
    +  });
    +
    +  it("ignores page-origin attempts to arm the artifact bridge", async () => {
    +    const handler = createHandler();
    +    const armResponse = vi.fn();
    +
    +    handler(
    +      { type: "automation:artifacts-arm", tabId: TAB_ID, enabled: true },
    +      { tab: { id: TAB_ID } } as any,
    +      armResponse,
    +    );
    +
    +    const blocked = await dispatchArtifact(handler, {
    +      type: "automation:artifacts",
    +      requestId: "attacker-create",
    +      action: "createOrUpdateArtifact",
    +      payload: { fileName: "attacker-note.txt", content: "PAGE_CONTROLLED" },
    +    });
    +
    +    expect(armResponse).not.toHaveBeenCalled();
    +    expect(blocked).toEqual({ ok: false, error: "Artifacts bridge not armed for this tab" });
    +  });
    +
    +  it("allows artifact operations only while extension code has armed the tab", async () => {
    +    const handler = createHandler();
    +
    +    handler({ type: "automation:artifacts-arm", tabId: TAB_ID, enabled: true }, {} as any, vi.fn());
    +    const created = await dispatchArtifact(handler, {
    +      type: "automation:artifacts",
    +      requestId: "trusted-create",
    +      action: "createOrUpdateArtifact",
    +      payload: {
    +        fileName: "trusted-note.txt",
    +        content: "TRUSTED_AUTOMATION_ARTIFACT",
    +        mimeType: "text/plain",
    +      },
    +    });
    +    expect(created).toMatchObject({ ok: true });
    +
    +    handler(
    +      { type: "automation:artifacts-arm", tabId: TAB_ID, enabled: false },
    +      {} as any,
    +      vi.fn(),
    +    );
    +    const blockedAfterDisarm = await dispatchArtifact(handler, {
    +      type: "automation:artifacts",
    +      requestId: "late-read",
    +      action: "getArtifact",
    +      payload: { fileName: "trusted-note.txt" },
    +    });
    +
    +    expect(blockedAfterDisarm).toEqual({
    +      ok: false,
    +      error: "Artifacts bridge not armed for this tab",
    +    });
    +  });
    +});
    

Vulnerability mechanics

Root cause

"Missing authorization check in the content script's window.postMessage bridge allowed any web page to send crafted artifact-operation messages to the extension background, because the bridge did not verify that the tab had been explicitly armed by extension-owned automation code."

Attack vector

An attacker-controlled web page (or any page the user visits) sends a `window.postMessage` call with `source: "summarize-artifacts"` and a crafted `action`/`payload` to the content script. The removed `handleArtifactsBridge()` listener [patch_id=424362] blindly forwarded every such message to `chrome.runtime.sendMessage` without checking whether the extension's automation had armed the tab. Because the background handler previously only checked for a valid `sender.tab.id` and did not consult an armed-tab set for artifact operations [CWE-862], the attacker could list, read, create, overwrite, or delete any automation artifact scoped to that tab. The attack requires no authentication and is triggered simply by the user visiting a malicious or compromised page.

Affected code

The vulnerable code was the `handleArtifactsBridge()` function in `apps/chrome-extension/src/entrypoints/automation.content.ts` [patch_id=424362], which registered a `window.addEventListener("message", …)` listener that forwarded any message with `source: "summarize-artifacts"` to `chrome.runtime.sendMessage` without authorization checks. The background handler in `runtime-actions.ts` also lacked an armed-tab guard for artifact operations, only checking for a valid `sender.tab.id` [patch_id=424362].

What the fix does

The patch removes the vulnerable `handleArtifactsBridge()` content-script listener entirely [patch_id=424362], so artifact RPCs are no longer accepted from the page world via `window.postMessage`. Instead, the `runBrowserJs` function now wraps artifact operations inside `withArtifactsArmedTab()` [patch_id=424362], which sends an `automation:artifacts-arm` message to the background before execution and disarms afterward. The background handler (`createRuntimeActionsHandler`) now maintains a separate `artifactsArmedTabs` set and rejects any artifact request whose sender tab is not in that set [patch_id=424362]. Additionally, `chrome.runtime.onUserScriptMessage` is registered so that user scripts running in the extension's own world can still perform authorized artifact operations [patch_id=424362].

Preconditions

  • networkAttacker must induce the victim to visit a web page they control (or a page that can inject/execute arbitrary JavaScript).
  • inputThe attacker's page must be able to call window.postMessage() with source='summarize-artifacts' and a crafted action/payload.

Reproduction

The public PoC reference at https://github.com/steipete/summarize/pull/222 does not contain standalone reproduction steps. The test file `tests/runtime-actions.artifacts-security.test.ts` [patch_id=424362] demonstrates the attack pattern: a message with `type: "automation:artifacts"` and any `action` (e.g., `getArtifact`, `createOrUpdateArtifact`) is sent from a sender whose tab is not in the `artifactsArmedTabs` set, and the handler returns `{ ok: false, error: "Artifacts bridge not armed for this tab" }`. Before the patch, no such guard existed, so the same message would have succeeded.

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

References

4

News mentions

0

No linked articles in our index yet.