Security Considerations
Relevant source files
The following files were used as context for generating this wiki page:webext-bridge is designed to simplify messaging between different contexts of a web extension. This communication, especially between less-privileged contexts (like web pages) and more-privileged contexts (like content or background scripts), requires robust security measures to prevent potential vulnerabilities. This document outlines the key security mechanisms implemented within webext-bridge to ensure safe and isolated communication channels.
The primary security focus is on the boundary between the window context (an untrusted web page) and the content-script context. The library employs explicit opt-in mechanisms, namespacing, and origin verification to mitigate risks such as unauthorized message listening or sender spoofing.
Window-to-Content-Script Communication
The most critical security boundary managed by webext-bridge is between a standard web page (window context) and the extension's content script (content-script context). Communication across this boundary uses window.postMessage, which can be insecure if not handled properly. webext-bridge implements two key features to secure this channel: mandatory namespacing and origin identity enforcement.
Namespace Isolation
To prevent conflicts with other libraries or extensions using postMessage, webext-bridge requires developers to define a unique namespace. This namespace isolates the extension's messages from any other messages being passed in the window.
- Content Script: Must call
allowWindowMessaging(namespace)to start listening for messages from the window. - Window Script: Must call
setNamespace(namespace)to configure the message channel.
Communication is only established if both contexts use the same namespace. An error is thrown if an attempt is made to send a message without first setting a namespace.
Sources: src/content-script.ts:40-43, src/window.ts:18-21
The ensureNamespaceSet function enforces this requirement.
// src/internal/post-message.ts:47-56
function ensureNamespaceSet(namespace: string) {
if (typeof namespace !== 'string' || namespace.trim().length === 0) {
throw new Error(
'webext-bridge uses window.postMessage to talk with other "window"(s) for message routing'
+ 'which is global/conflicting operation in case there are other scripts using webext-bridge. '
+ 'Call Bridge#setNamespace(nsps) to isolate your app. Example: setNamespace(\'com.facebook.react-devtools\'). '
+ 'Make sure to use same namespace across all your scripts whereever window.postMessage is likely to be used`',
)
}
}
Sources: src/internal/post-message.ts:47-56
This diagram shows the required setup for window-to-content-script communication.
Origin Identity Enforcement
To prevent a malicious script in the window from spoofing its identity (e.g., pretending to be the background script), the content script hardcodes the origin of any message it receives from the window. When the content script's onMessage handler for postMessage is triggered, it automatically assigns the origin context as "window", ignoring any origin data that might be in the message payload.
This diagram illustrates how the content script sanitizes the origin of incoming messages from the window before forwarding them.
Sources: src/content-script.ts:16-23
Here is the relevant code snippet:
// src/content-script.ts:16-23
win.onMessage((message: InternalMessage) => {
endpointRuntime.handleMessage(Object.assign({}, message, {origin: {
// a message event inside `content-script` means a script inside `window` dispatched it to be forwarded
// so we're making sure that the origin is not tampered (i.e script is not masquerading it's true identity)
context: "window",
tabId: null
}}))
})
Sources: src/content-script.ts:16-23
Background Script Routing Security
The background script acts as a central message router for all extension contexts. It implements security checks to ensure messages are routed correctly and that sender information is trustworthy.
Connection-based Origin Verification
When a context (like a content script or devtools panel) connects to the background script, it establishes a runtime.Port. The background script uses the port.sender property, which is reliably provided by the browser, to determine the tabId and frameId of the connecting context.
When a message is relayed through the background script, it explicitly overwrites the origin.tabId and origin.frameId with the values from the port's sender information. This prevents a compromised content script from sending a message and pretending it came from a different tab.
This sequence diagram shows how the background script verifies the origin tab ID.
Sources: src/background.ts:286-289
// src/background.ts:286-289
if (msg.type === 'deliver' && msg.message?.origin?.context) {
// origin tab ID is resolved from the port identifier (also prevent "MITM attacks" of extensions)
msg.message.origin.tabId = linkedTabId
msg.message.origin.frameId = linkedFrameId
endpointRuntime.handleMessage(msg.message)
}
Sources: src/background.ts:286-292
Endpoint-based Routing
The library uses a strict format for identifying endpoints, which helps ensure messages are routed only to valid destinations. The parseEndpoint function uses a regular expression to validate and deconstruct endpoint strings.
| Endpoint Context | Format Example | Notes |
|---|---|---|
background | background | Singleton context. |
popup | popup | Singleton context. |
options | options | Singleton context. |
devtools | devtools@123 | 123 is the tab ID of the inspected window. |
content-script | content-script@123.0 | 123 is the tab ID, 0 is the frame ID. |
window | window@123.0 | 123 is the tab ID, 0 is the frame ID. |
Sources: src/internal/endpoint.ts:3-14
Stream Security
The Stream class provides a persistent, namespaced channel for communication. While streams are scoped, developers are still responsible for verifying the identity of the other endpoint before transmitting sensitive data.
The documentation for stream.send() explicitly warns about this:
Warning: Before sending sensitive data, verify the endpoint using stream.info.endpoint.isInternal() The other side could be malicious webpage speaking same language as webext-bridge
Sources: src/internal/stream.ts:61-63
The stream.info property provides the Endpoint details of the other party, which can be inspected.
// src/internal/stream.ts:51-53
public get info(): StreamInfo {
return this.streamInfo
}
Sources: src/internal/stream.ts:51-53
Summary
webext-bridge incorporates several key security features to protect extension communications:
- Namespace Isolation:
postMessagecommunication between the page and content scripts is opt-in and requires a shared, developer-defined namespace. - Origin Enforcement: The library programmatically sets and verifies the origin of messages at critical boundaries (
content-scriptandbackground), preventing sender spoofing. - Structured Endpoints: A strict endpoint format ensures messages are routed to valid, well-defined contexts.
- Developer Responsibility: For continuous
Streamcommunication, the library provides the tools to verify the remote endpoint, but places the responsibility of doing so on the developer, especially when dealing with untrusted contexts.