Browser Extension Content Scripts and Page Isolation
Understand the boundary between extension content scripts and page JavaScript, how isolated worlds affect APIs and DOM, and how to test the boundary safely.
BotBrowser Team
Want the structured docs for Platform?
This article lives in the editorial library. For step-by-step setup, reference material, and ongoing updates, jump into the docs section.
BotBrowser can repeat an authorized extension fixture and observe visible DOM and message outcomes, but it cannot grant host permissions or make page data trusted. An extension content script can inspect and modify a page's DOM while running in a JavaScript environment separated from the page's own variables. That separation is an isolated world, not a second DOM and not a universal security boundary. The page and the extension can see many of the same elements, but they do not automatically share JavaScript objects, prototypes, or private extension state. A reliable design keeps privileged work in the extension and treats page communication as an explicit, validated interface.
TL;DR
- An isolated world separates JavaScript bindings while the page and content script can still observe many of the same DOM mutations.
- Host permissions, match patterns, frame rules, and manifest policy still decide where a script can run.
- Keep privileged APIs and durable state in the extension context or service worker; validate every page-originated message.
- Main-world bridges are observable and replaceable by page code, so use the narrowest bridge possible and never send secrets.
- BotBrowser can repeat a synthetic authorized fixture and compare visible results; it cannot grant permissions or prove trust in page data.
Contents
- What isolation means
- Crossing the boundary deliberately
- DOM sharing and event behavior
- Permissions, frames, and lifecycle
- A message contract that survives change
- Testing the real boundary
- BotBrowser capability and limitation
- Conclusion
What isolation means
The browser creates a content-script world with its own global object and JavaScript bindings. A variable assigned by the page is not automatically visible to the content script, and a variable assigned by the content script is not automatically visible to page scripts. Built-ins and prototypes can also differ, which prevents ordinary page code from replacing an extension's references merely by changing window.Array or another page-owned property.
The DOM is the important shared surface. Both worlds can often read and mutate the same nodes, attributes, and event targets. A DOM mutation is therefore observable to the page even when the JavaScript objects used to perform it are isolated. Do not treat isolated worlds as a way to hide visible changes or as permission to read data that the page itself cannot expose.
An extension still needs the right host permission and content-script match pattern. Isolation does not grant access to every URL, frame, or privileged browser API. APIs such as storage, tabs, and network controls belong in the extension context or service worker, where the manifest and permission model apply.
DOM sharing and event behavior
The shared DOM makes the boundary easy to misunderstand. A content script can add a class, insert a button, or change text that page code immediately observes. A page can also replace a node, remove an attribute, or dispatch an event that the content script receives. The JavaScript references used by each side remain separate, but the browser's document tree is a common observable surface. Design visible changes as part of the page contract, not as hidden extension state.
Event listeners deserve the same care. Register listeners on the smallest stable node, namespace extension-owned attributes, and remove listeners when a route or frame is no longer active. A page may stop propagation, replace a target, or dispatch a synthetic event. Therefore an event is a notification, not proof that a user performed an action. For a sensitive operation, require an extension-side state check and a fresh user gesture in the extension UI.
Prototype separation helps with ordinary page monkey-patching, but it does not make every value safe. Objects read from the DOM or from a page message still need shape checks. Convert values into a small internal representation before passing them to privileged code. Do not retain page objects across asynchronous boundaries when a string, number, or bounded record is enough.
Mutation observers can make a content script responsive, but an observer is not an authorization signal. Limit the observed subtree, disconnect it during teardown, and debounce bursts so a page cannot force unbounded work by changing a large subtree repeatedly. If a feature needs a stable semantic state, prefer a documented attribute or extension-owned marker and verify it before acting.
Permissions, frames, and lifecycle
The manifest's host permissions and content-script matches are the first boundary. A script that matches the top-level page may not automatically run in every child frame, and a frame with a different origin can have a different policy and document lifecycle. Record the frame URL and injection timing in a fixture. Do not broaden a match pattern merely because a test missed a frame; decide whether that frame is actually in scope.
Restricted pages, browser UI pages, extension pages, and pages with a restrictive policy can reject injection or produce a different execution path. Treat a missing content script as an explicit unsupported state. Keep the page's core task usable and expose a clear extension fallback rather than retrying injection indefinitely.
Navigation and reload destroy the old document and its listeners. A service worker can also be suspended and restarted without preserving in-memory variables. Store only the minimum durable state, re-check the current tab and frame before using it, and make startup idempotent. A worker restart is normal lifecycle behavior, not evidence that isolation failed.
Frames add a second source of timing variation. A child document may load after the parent, navigate independently, or be removed while a message is in flight. Use a tab and frame identifier supplied by the browser API, reject stale identifiers, and make an absent frame a recoverable result. Never use a frame's URL alone as proof of a person's identity or account.
A message contract that survives change
Define a small message vocabulary such as read-selection, set-badge, and request-refresh. Every message should carry a version and a bounded data object. The receiver should ignore unknown versions, reject unexpected fields, and return a state such as ok, unsupported, or denied. This makes a browser update or a staged extension rollout easier to diagnose than a generic thrown exception.
Page bridges need an origin rule and a reply rule. Check event.source against the expected window, compare event.origin with the declared page origin, and require a request identifier that cannot collide with another feature. Do not accept a wildcard origin for a privileged reply. If the page is allowed to send a request, let the extension decide which fields are safe and construct the privileged operation itself.
The service worker should treat the content script as an untrusted caller even when both were installed by the same extension. Verify the tab, frame, sender identity, and operation-specific state. A content script can be replaced or influenced by page-visible conditions, and an extension update can leave old tabs running briefly. Revalidation at the privileged boundary is cheaper than explaining an operation that used stale context.
Replies should be short and explicit. Return a status and a bounded result, not a page snapshot or a credential. If the worker cannot answer because it is restarting, show a retry path and preserve the page's input. Do not queue arbitrary page messages in memory; cap the queue and discard entries when their tab or frame disappears.
Main-world bridges and API ownership
Some integrations need a page-facing hook because the page owns an API that an isolated content script cannot observe directly. Treat this as a compatibility adapter. Keep the injected code deterministic, avoid closures over secrets, and expose one narrowly named function or event. The adapter should only translate data; the service worker remains responsible for permissions, storage, network decisions, and policy.
Page code can redefine globals, intercept events, and inspect properties in the main world. It can also load another script after the adapter. Therefore a main-world bridge cannot provide confidentiality from the page. If confidentiality is required, move the operation to extension UI or a browser API that the page cannot call. A bridge is for interoperability, not for hiding privileged state.
When a page API is optional, feature-detect it and retain a first-party fallback. A missing method, a rejected call, a navigation, and a worker restart are separate states. Preserve typed content, keep controls keyboard-accessible, and explain the reduced behavior without exposing implementation details. Do not turn a failed bridge into a claim about the browser or the user.
Privacy and evidence boundaries
Extension fixtures should use synthetic pages, names, and records. Record the browser release, manifest revision, host permission category, frame type, injection timing, visible result, and fallback. Do not collect cookies, account names, page captures, full message bodies, or arbitrary URLs when a category is enough. A message type such as selection-changed is usually more useful than the selected text itself.
Separate browser observations from product decisions. Seeing a DOM mutation proves that the tested content script could change that fixture. It does not prove that a user consented, that a server stored the change, or that another browser release behaves identically. A service-worker response proves the worker returned a status; it does not prove a remote transaction.
Use retention that matches the test purpose. A compatibility ledger may retain a fixture revision and visible assertion until the next browser release. A production diagnostic may retain only the outcome category and extension version. Keep the mapping from a synthetic fixture to a real customer workflow out of the browser log unless the product has a separate, documented purpose and access control.
Crossing the boundary deliberately
Use extension messaging for extension-owned state. Validate message type, sender, origin, and message data shape before acting, and return the smallest result needed by the feature. If page JavaScript must participate, use a narrow DOM or window.postMessage bridge with an unpredictable request identifier and strict origin checks. Never treat a message from the page as an authenticated extension command.
Avoid injecting a string into the page's main world when a content-script API is sufficient. Main-world code shares the page's JavaScript environment and can be observed or replaced by page scripts. If a main-world bridge is unavoidable, keep it tiny, avoid secrets, and make the privileged side re-check every request.
Testing the real boundary
Build a fixture with a page script, a content script, and an extension service worker. Set same-named variables and replace a page prototype in the page script; then record that the content script observes its own values while both scripts see intentional DOM changes. Test a second frame, a restricted URL, a reload, and a service-worker restart separately. A passing fixture demonstrates the tested browser and manifest configuration only; it does not prove isolation for every release.
BotBrowser can repeat an authorized extension fixture and record visible DOM results, message outcomes, and fallback behavior. It cannot grant host permissions, change a browser's extension policy, or turn page data into trusted extension input. Keep fixtures synthetic and record browser version, manifest permissions, frame URL, injection timing, and observed result without account data or page secrets.
BotBrowser capability and limitation
BotBrowser supports controlled browser-context runs that compare the visible result of the same extension fixture across declared browser releases and profiles, but it cannot grant host permissions or make page data trusted. That is useful for checking injection timing, DOM fallback behavior, message rejection, and worker restart handling. The evidence remains a browser observation tied to the declared fixture.
BotBrowser can repeat this authorized extension fixture, but it cannot grant host permissions or make page data trusted.
BotBrowser does not grant host permissions, override extension policy, inspect another origin's private state, or authenticate a page message. It cannot prove that a page's data is accurate, that a remote operation completed, or that every browser release will preserve the same lifecycle timing. Keep those decisions with the extension manifest, application authorization, and service evidence.
Review checklist for an extension release
Before shipping a content-script change, write down the exact match patterns and the frames that are intentionally included. Confirm that the requested host scope is no broader than the feature requires, and verify that restricted pages have a useful fallback. A permission review should distinguish a browser-enforced permission from an application choice: the extension may ask to run on a host, but it cannot decide that a page's content is safe or that a user intended a privileged action.
Check the document lifecycle with a short matrix: initial injection, same-document navigation, full navigation, reload, back-forward cache restoration when supported, child-frame creation, child-frame removal, and service-worker suspension. For each row record whether the listener exists, whether stale state is discarded, and what visible control remains if the worker is unavailable. The goal is a bounded recovery path, not a promise that every browser schedules events at the same moment.
Review the DOM contract with the page owner. List extension-owned attributes and events, expected selectors, and the behavior when a site redesign removes them. Prefer semantic controls and stable data attributes over brittle CSS paths. When a selector fails, show the extension's disabled or fallback state instead of scanning the whole document for a similar-looking element. This limits work and prevents accidental interaction with unrelated page content.
Review message evolution as an API. Assign a version, document required and optional fields, and define how an older worker handles a newer content script. During a staged rollout, accept only the versions that are safe and return an explicit unsupported state for the rest. Remove compatibility branches after the rollout is complete so an obsolete assumption does not become a permanent permission surface.
Review failure and cancellation. A page can close a frame while a request is pending, a worker can restart between request and response, and the user can navigate before a result arrives. Abort pending work where the browser API permits it, ignore responses whose tab or frame identifier is stale, and keep the page's original input. Do not retry a privileged action automatically just because a worker returned an unknown result.
Finally, make the evidence readable to support teams. Record the extension version, browser release, fixture revision, context type, and visible state. Use categories such as injected, unsupported, denied, stale, and completed; avoid logging page text, cookies, account identifiers, or full message data. A compact record lets a team reproduce a boundary issue without turning the extension into a data collection channel.
For related boundaries, see Browser API integrity across realms and Browser automation test fixtures and isolation.
Conclusion
Treat a content script as an isolated JavaScript world with a shared, observable DOM. Keep privileged operations in the extension context, make every bridge explicit, validate messages, and test frames and lifecycle transitions. The safe next step is to document the smallest host permission and message schema that your feature needs.
Sources
Related Articles
Take BotBrowser from research to production
The guides cover the model first, then move into cross-platform validation, isolated contexts, and scale-ready browser deployment.