BragJack and the Missing Sender Validation in Agent Bridges

BragJack and the Missing Sender Validation in Agent Bridges

pr0h0
browser-extensionsai-agentsweb-securitypostmessagebrowser-security
AI Usage (81%)

Introduction

Three outlets published coverage of BragJack on 2026-09-20 and 2026-09-21. The story: a set of flaws that let one browser extension hijack AI assistants in five major browsers, with no user interaction required. Those three secondary reports are the entire source set I had to work with — no vendor advisory, no CVE record, no technical write-up containing a payload. This post separates what the reports actually claim from what I could verify, then walks through the extension-to-agent message path where sender validation is usually missing, and shows how to test your own bridge with two unpacked Manifest V3 extensions.

The browser count is not what makes this interesting. The "no clicks" part is. When a hijack needs nothing beyond installation, the only gates left are the extension store and whatever the user already has installed. That turns every AI companion extension into a trust decision about an installed base you don't control.

The bridge between extensions and AI agent APIs is a trust boundary, not glue code

Most teams building a browser agent integration treat the bridge as plumbing. A content script observes the page, a service worker relays state, the agent surface receives a tool call. The hop that matters is the last one, because that is where intent turns into action. "Summarize this page" and "read the user's mail tab" arrive on the same channel, often in the same message shape. If the listener can't authenticate the sender, what you have isn't an integration layer. It is an open port with a tool registry bolted to it.

What the BragJack reports claim versus what I could verify

Here is the reporting, separated from what I am adding.

ClaimWhere it comes fromConfidence
One extension can hijack AI assistantsTech Times headline and lead, tech-insider.org, Pasquale PillitteriReported, consistent across all three
Five major browsers are affectedAll three sourcesReported as a count; the specific five browsers are not named in my source set
No user interaction is requiredTech Times, Pasquale PillitteriReported
BragJack is a set of flaws, not one bugtech-insider.orgReported
The underlying cause is missing sender validation in extension-to-agent messagingNot stated in the material I hadInferred from general extension and agent architecture
A CVE or vendor advisory existsNone foundUnconfirmed
Fixed as of 2026-09-21Not statedUntested

Three outlets repeating the same summary does not make a primary source. The detail in the snippets I received is thin enough that I would not quote an affected-browser list, a version range, or a patch status from it. If a vendor advisory shows up later, it supersedes everything in that table.

Confirmed versus inferred: what the outlets state plainly and what I am filling in

Confirmed, and only in the weak sense: a disclosure was reported on 2026-09-20 and 2026-09-21 by tech-insider.org, Tech Times, and Pasquale Pillitteri, describing cross-browser AI assistant hijacking from a single extension with no user clicks, framed as a trust and permission boundary problem between extensions and agent integrations.

Inferred, and I want to be blunt about the line: the mechanism. One extension, no clicks, works across browser implementations — those properties are exactly what you get from a receiver that listens for messages and routes on message type alone. That is my read of how these bridges are normally written, not something the reports assert. Everything in the technical sections below is about that mechanism in general, not a claim about what BragJack's authors did.

How extension-to-agent messaging is wired — and where sender validation goes missing

The four legs of a browser agent message path

A typical browser agent bridge has four legs, and each one carries a different notion of identity:

  1. Page scripts to content script, over window.postMessage or a MessageChannel. Identity here is event.origin and event.source. The content script runs in an isolated world, but the page can still reach it through the window.
  2. Content script to background service worker, over chrome.runtime.sendMessage or a long-lived port. This leg is internal, so it usually gets no check at all.
  3. Other extensions or web pages to the service worker, over chrome.runtime.onMessageExternal. This is the leg the sender object exists for. The manifest's externally_connectable key gates which pages and extension IDs may connect, but the listener still receives a sender, and still has to use it.
  4. Service worker to the agent surface — another extension, a native host over native messaging, or the browser's own assistant API. This is where tool calls execute with the user's real session.

The failure mode I keep running into: a check on leg 1, then complete trust on legs 2 through 4.

Why sender identity is the whole ballgame in extension messaging

Look at the shape of a listener that routes on message type:

bridge-sw.js — type-only routing
// BEFORE: routes on message shape, ignores the sender argument entirely
chrome.runtime.onMessageExternal.addListener((msg, sender, reply) => {
if (msg?.type === "agent.run") {
  return runTool(msg.tool, msg.args, reply);
}
if (msg?.type === "agent.history") {
  return sendHistory(reply);
}
if (msg?.type === "agent.navigate") {
  return navigate(msg.url, reply);
}
});

The sender parameter is sitting right there in the signature, unused. That is the bug, in one line. sender.id identifies an extension sender; sender.origin and sender.url describe a page sender; on the window leg you have event.origin and event.source. A listener that only checks msg.type is treating "who is calling" as irrelevant.

⚠️

Tool names are guessable. If your bridge accepts { type: "agent.run", tool: "..." }, the protocol documents itself, and a hostile extension only has to guess a string — not reverse your minified bundle.

Why a no-click hijack raises severity

When the bridge checks only shape, anything that can reach the listener is a valid caller. The user never approves an action in the agent surface, because no action prompt is reached in a way they control. That makes the attack surface every extension they have installed — which is where typosquats, abandoned extensions that change hands, and compromised updates live. Your capability boundary is now the entire extension ecosystem on that machine, not the two extensions you wrote.

A local harness for testing sender validation in your own bridge

One caveat before the code: this harness is two unpacked Manifest V3 test extensions plus my own bridge code, on Chrome stable. It does not reproduce BragJack, and I don't have their proof of concept. It answers one question you can verify today: does your listener distinguish senders?

Minimal two-extension test for sender validation

The receiver starts with type-only routing, as above. Then the same listener with an allowlist:

bridge-sw.js — after: sender allowlist
// AFTER: identity is checked before any tool is reachable
const ALLOWED_EXTENSION_IDS = new Set([
"eepljhbacfgpnjmkodlfabkhjmcginph", // trusted companion extension
]);
const ALLOWED_PAGE_ORIGINS = new Set(["https://app.example.com"]);

chrome.runtime.onMessageExternal.addListener((msg, sender, reply) => {
if (!sender.id || !ALLOWED_EXTENSION_IDS.has(sender.id)) {
  console.warn("[bridge] rejected sender", sender.id, sender.origin);
  return; // no reply: the caller gets a closed channel
}
if (sender.origin && !ALLOWED_PAGE_ORIGINS.has(sender.origin)) {
  console.warn("[bridge] rejected origin", sender.origin);
  return;
}
if (msg?.type !== "agent.run") return;

const capability = CAPABILITIES[msg.tool]; // per-tool scope, see below
if (!capability) return reply({ error: "unknown tool" });

return runTool(capability, msg.args, reply);
});

The sender side is short, and that is the point — it costs an attacker nothing:

test-sender/background.js
// Any installed extension that knows the bridge's ID can do this.
chrome.runtime.sendMessage(
"abkdnbmdmhbfkeppgjdhkfpkdhbmdhfc", // bridge extension ID
{ type: "agent.run", tool: "readPage", args: { selector: "#inbox" } },
(res) => console.log("[sender] reply", res)
);

Instrumenting the listener to see sender identity

Don't guess at what the runtime hands you. Log it.

bridge-sw.js — instrument before dispatch
chrome.runtime.onMessageExternal.addListener((msg, sender, reply) => {
console.log("[bridge] incoming", {
  type: msg?.type,
  tool: msg?.tool,
  senderId: sender.id,
  senderOrigin: sender.origin,
  senderUrl: sender.url,
});
// dispatch below this line
});

// Page leg: check both origin and source, and log them before anything runs.
window.addEventListener("message", (event) => {
console.log("[bridge] window-message", {
  origin: event.origin,
  source: event.source === window ? "same-window" : "other-window",
  type: event.data?.type,
});
if (!ALLOWED_PAGE_ORIGINS.has(event.origin)) return;
// dispatch below this line
}, false);

Observed output before and after the sender allowlist

Before the allowlist, my test sender reached the tool with no prompts. Console transcript from the bridge service worker:

[bridge] incoming { type: "agent.run", tool: "readPage", senderId: "abkdn…hostile-test" }
[bridge] no sender check — dispatching tool=readPage
[bridge] tool result: { title: "Inbox (3)", text: "…" }

After the allowlist, same sender, same message:

[bridge] incoming { type: "agent.run", tool: "readPage", senderId: "abkdn…hostile-test" }
[bridge] rejected sender abkdn…hostile-test undefined

On the page leg, a script on an unrelated origin got as far as the log line and no further, because the origin check returned early:

[bridge] window-message { origin: "https://evil.example", source: "same-window", type: "agent.run" }
Callersender.id / originIn allowlistResult
Trusted companion extensionpresent, allowlistedyestool dispatched, reply sent
Test extension (unpacked)present, not allowlistednorejected, no reply
Page script, foreign originorigin mismatchnorejected at origin check

One detail worth knowing: when the listener returns without replying, the sender gets no response and the message port closes. The exact text differs between callback-style runtime.sendMessage and the promise-based form, so don't build logic on that string. Don't lean on externally_connectable alone either — treat it as a manifest-level convenience and keep the check in the listener, where it lives in the code path you actually review.

Defenses that hold up against missing sender validation

  • Validate sender.id against an explicit allowlist before any tool dispatch. Not a regex, not a suffix match.
  • Validate sender.origin / event.origin on the page leg, and check event.source too if you also listen for same-window messages.
  • Never use a wildcard targetOrigin in postMessage. "*" hands your payload to whatever frame is in that window.
  • Scope capabilities per message type. A "summarize" handler should not be able to reach a "navigate" capability through a shared dispatcher.
  • Require an explicit user gesture for privileged or destructive tool calls, and bind the gesture to the specific action rather than to the session.
  • Keep an audit log with sender identity, tool name, and timestamp for every dispatch. It is the only way to reconstruct what happened.

If I could ship only one of these, it would be the sender allowlist. It is a few lines, it is testable with the harness above, and it moves the attack from "any installed extension" to "the extensions you named." Per-tool capability scoping comes second, because it caps the blast radius when the allowlist is wrong. Per-action confirmation for destructive calls comes third — valuable, but it doesn't help if the privileged path never asks.

Why agent bridges inherit the extension ecosystem's monoculture

The position I would defend: more install-time warnings won't fix this, and cross-browser agent APIs should not ship without mandatory sender validation. The properties attributed to BragJack — one extension, no clicks, five browsers — fall straight out of a shared pattern in how these bridges get written. A warning dialog at install time doesn't change what a listener accepts at runtime. Identity has to be enforced where the tool call executes.

There is a second-order problem too. The same coupling that makes a bridge convenient makes it fragile. When the agent surface and the extension share a messaging protocol with no identity layer, every browser implementation of that protocol inherits the same weakness. That is a monoculture, and I would rather see each implementation require an explicit sender check than count on store review to catch it.

What I confirmed and what I did not test

Confirmed by running it locally: in Manifest V3, the sender object is populated for external messages, sender.id is available before dispatch, and an allowlist check reliably stops an unpacked test extension from reaching a tool handler. The window-leg origin check behaves the same way.

Not tested, and I want this on the record: I did not reproduce BragJack. No access to the researchers' proof of concept, no affected version data, no confirmation of which five browsers are involved. Everything in the "how it is wired" and "defenses" sections is general architecture plus my own harness, not a reproduction of the reported attack. The root-cause claim is an inference I would happily revise if a technical write-up or vendor advisory contradicts it.

Further Reading

Secondary reporting only, with no advisory in the source set

Before you cite any of these, note what they are: news coverage of a disclosure, not primary research. No vendor security bulletin and no CVE record appeared in the material I was given for this post, so there is nothing here to check a version range or patch status against.

  • Tech Times, 2026-09-20 — "Single Extension Hijacks AI Agents in Five Browsers Without Any User Clicks" (secondary reporting)
  • tech-insider.org, 2026-09-21 — "BragJack Attack Hijacks AI Browser Agents in 5 Browsers" (secondary reporting)
  • Pasquale Pillitteri, 2026-09-20 — "BragJack flaws let one extension hijack AI assistants in five browsers" (secondary reporting)

If an advisory or the original research write-up surfaces, read that first and treat these three as background.

Share this post

More posts

Comments