Secure, test, and publish

Protect extension pages

Keep executable code packaged locally, avoid dangerous HTML sinks, and use a restrictive content security policy.

Manifest V3 forbids remotely hosted code. Every line of JavaScript that runs in your extension must ship inside the package. The reason is simple: what the store reviewed is what users run, and it can’t change after publication.

Let’s see what that rule covers, and how to keep the extension pages safe.

What counts as remote code

More than a <script src="https://cdn..."> tag. All of these break the rule:

  • JavaScript downloaded at runtime and executed
  • WebAssembly fetched from a server
  • strings turned into code with eval() or new Function()
  • a library that fetches its own logic after installation

Remote data is fine. You can fetch() JSON from an API. The package decides what to do with that data, and that decision is reviewable code.

If a library needs eval to work, don’t loosen the content security policy to accommodate it. Pick a different library. The CSP is doing its job.

Extension pages are privileged

The popup can call chrome.storage, chrome.scripting, chrome.tabs. If someone gets HTML injected into it, they inherit those powers. The same bug on an ordinary web page is bad. In an extension page it’s a lot worse.

So the rules from the popup lessons are security rules, not style: render text with .textContent or .value, build elements with createElement(), never use innerHTML or insertAdjacentHTML with anything that touched user input.

If you truly need rich text one day, bundle a reviewed sanitizer like DOMPurify in the package, and test its configuration with hostile input before trusting it.

Web-accessible resources

By default, web pages can’t load files from your extension. The web_accessible_resources manifest key opens specific files to specific pages. It exposes assets only, not APIs, but it also lets those pages detect that your extension is installed.

Page Notes needs none of it. The popup and the scripts are never requested by a website. Leave the key out.

A grep-based audit

Before every release I search the whole package for the dangerous patterns:

grep -rnE "innerHTML|insertAdjacentHTML|eval\(|new Function|document\.write|https?://" --include='*.js' --include='*.html' .

Every hit needs a justification or a fix. A URL in a comment is fine. A URL in a <script src> is not.

Then run the hostile-text test one more time, in both places. Save <img src=x onerror=alert(1)> as a note, open the popup, and show the panel on a page. You want the literal string in both, no alert, and a Network panel with no request for x. Finish by scrolling the Network panel for anything the package didn’t ship. There should be nothing.

Lesson completed