Reverse Engineering

Why Browser Extension Content Scripts Cannot Hook Page Functions

Why Browser Extension Content Scripts Cannot Hook Page Functions — conceptual illustration
On this page

A browser extension content script runs in an "isolated world" -- a separate JavaScript execution context that shares the DOM with the page but has its own global object. So when a content script reassigns window.fetch or HTMLCanvasElement.prototype.toDataURL, it only changes its own copy. The page keeps calling the real, untouched function. This isolation is a deliberate security boundary, and it is the first wall every browser-instrumentation project hits.

Quick facts

What it isSeparate JS context for extension content scripts
SharedThe DOM (elements, nodes)
Not sharedThe JavaScript global object and prototypes
ConsequencePatching window.* never affects page scripts
Why it existsSecurity: stops pages and extensions tampering with each other

Two worlds, one DOM

Chromium gives every content script its own "isolated world": a fresh V8 context with a distinct window, distinct prototypes, and distinct built-ins. The DOM tree is shared -- you can read and modify elements -- but the JavaScript objects are not. This is why an extension can rewrite page text but cannot intercept the page calling navigator.userAgent from script. The two contexts see the same nodes through different lenses.

Why your hook silently does nothing

This is the classic first failure of a browser-instrumentation project. You write window.fetch = wrappedFetch in a content script, reload, and see no logs -- because the page is calling its window.fetch, not yours. Nothing errors; it just does not work. The isolation is total for built-ins and globals: there is no flag that lets a content script reach across into the page's world.

The way around it

The escape hatch is to stop hooking from the isolated world and instead make your code run as page script. Historically that meant injecting a <script> tag into the DOM; the cleaner approach is the DevTools protocol's addScriptToEvaluateOnNewDocument, which runs your code in the page's own context before any page script loads. But even then, a wrapper you install in the page world is still a JavaScript function that does not look native -- so the isolated-world problem is only the first of several.

Code example

javascript
// content_script.js -- runs in the ISOLATED world
window.fetch = function (...args) {
  console.log('intercepted fetch', args);   // never fires for page requests
  return originalFetch(...args);
};

// The page's own code calls a DIFFERENT window.fetch:
//   page world:     window.fetch  ->  real native fetch  (untouched)
//   isolated world: window.fetch  ->  your wrapper       (ignored by page)
// Same DOM, separate globals. The hook is invisible to the page.

Related terms

Concept map

How Why Can't a Browser Extension Hook Page JavaScript connects

The terms most directly tied to this one. Hover a node to see its neighbours, click to preview, drag to rearrange.

0 terms · 0 connections
You are here · Reverse Engineering
Building map…

Frequently asked questions

Can a content script ever access page variables directly?

No. The isolated world shares the DOM but not the JavaScript global object, so page variables and patched built-ins are unreachable from a content script. You must run code in the page world to interact with them.

What is the MAIN world in MV3?

Manifest V3 added a "world": "MAIN" option for registered content scripts, letting them run in the page context instead of the isolated world. It removes the isolation barrier but does not make your hooks look native -- detection by toString still applies.

Does this isolation exist in headless automation too?

The isolated-world concept is specific to extension content scripts. Automation tools use the DevTools protocol to inject into the page world directly, which is why they reach for addScriptToEvaluateOnNewDocument instead.

Last updated: 2026-06-09