In modern Manifest V3 Chrome Extension development, security is paramount. Web browsers enforce a strict boundary between different execution contexts: content scripts (which run in the context of the web page and can touch the DOM) and service workers (formerly background scripts, which run in isolation and have access to the full chrome.* APIs).
Content scripts cannot directly make cross-origin network calls (CORS restrictions), query other tab states, or read secure storage. To execute these operations safely, we must implement robust Inter-Process Communication (IPC) via message passing. In this post, we'll design a secure message channel between content scripts and service workers.
The Message Broker Architecture
To pass data safely, our extension implements a secure message broker pattern:
- Content Script (Emitter): Listens for page gestures, harvests page metadata, and packs the values into a structured message payload. It calls
chrome.runtime.sendMessageto emit the package. - Service Worker (Broker): Listens for incoming packages via
chrome.runtime.onMessage.addListener, evaluates the payload type, executes secure API requests or cookie queries, and returns a response payload. - Content Script (Receiver): Processes the returning response inside a secure callback to update the page UI.
Step 1: Emitting Messages from Content Scripts
Here is how a content script harvests DOM details and passes them as an initialized package to the background service worker:
// content_script.js
function sendCurationPayload() {
const payload = {
message: "initiate_curation",
url: window.location.href,
title: document.title,
selected_text: window.getSelection ? window.getSelection().toString() : ""
};
console.log("[content_script.js] Sending payload to broker:", payload);
// Send to background service worker
chrome.runtime.sendMessage(payload, function(response) {
if (chrome.runtime.lastError) {
console.error("Broker connection failed:", chrome.runtime.lastError.message);
return;
}
console.log("[content_script.js] Received response from broker:", response);
});
}
Step 2: Processing Messages in the Service Worker
In Manifest V3, background tasks are handled by a service worker (background.js). It registers a listener, handles asynchronous tasks securely, and responds using sendResponse (ensuring that return true; is called to keep the message channel open during async actions):
// background.js
chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
console.log("[background.js] Incoming message intercepted:", message);
if (message.message === "initiate_curation") {
// Execute an async API request to store the curation data
storeCurationRecord(message)
.then(data => {
// Send success response back to content script
sendResponse({ status: "success", data: data });
})
.catch(error => {
sendResponse({ status: "error", error: error.message });
});
return true; // Keep connection open for async sendResponse callback!
}
if (message.message === "open_new_tab") {
chrome.tabs.create({ url: message.url });
sendResponse({ status: "tab_opened" });
}
});
async function storeCurationRecord(payload) {
const response = await fetch("https://api.yourportal.com/curate/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
return response.json();
}
Key Security & IPC Best Practices
- Return True for Async Tasks: In Manifest V3,
sendResponsebecomes invalid as soon as the listener function finishes. You must addreturn true;to tell Chrome that you intend to respond asynchronously, preventing immediate channel closures. - Verify Sender Origin: In background listeners, always inspect the
senderobject to ensure that the message was enqueued from a valid content script inside your extension, preventing external injection. - Handle runtime.lastError: Always check
chrome.runtime.lastErrorinside your callback listeners. If the service worker is idle or sleeping, message channels can time out, throwing errors that will crash your scripts if unhandled.
Implementing secure, asynchronous inter-process communication ensures that your Chrome extension safely bridges page DOM manipulations with privileged backend API operations.