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.
- [security] fix(extension): guard automation artifacts bridge by Hinotoi-agent · Pull Request #222 · steipete/summarize
- [security] fix(extension): guard automation artifacts bridge (#222) · steipete/summarize@3575440
- Release v0.15.2 · steipete/summarize
- Summarize < 0.15.1 Browser Extension Missing Authorization via Content Script
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
2Patches
1357544063af5[security] fix(extension): guard automation artifacts bridge (#222)
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- github.com/steipete/summarize/commit/357544063af535bd574752622f9eb94be33ee5fdnvdPatch
- github.com/steipete/summarize/pull/222nvdExploitIssue TrackingPatch
- www.vulncheck.com/advisories/summarize-browser-extension-missing-authorization-via-content-scriptnvdThird Party Advisory
- github.com/steipete/summarize/releases/tag/v0.15.2nvdRelease Notes
News mentions
0No linked articles in our index yet.