C.W.K.
Stream
Lesson 05 of 06 · published

MV3 Security Model — Sandbox, CSP, Permissions

~12 min · mv3, security, csp, permissions, host_permissions, sandbox

Level 0Extension Curious
0 XP0/54 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete
"MV3's security model is opinionated to the point of pickiness — and that's the feature, not the bug."

The Extension Sandbox

Every Chrome extension runs in its own renderer process, isolated from the host pages it interacts with. The extension has access to the chrome.* APIs and a private origin (chrome-extension://<id>/), but it can't reach into the host page's JavaScript context directly. Communication happens through deliberate channels — messages, content scripts injected into isolated worlds, and Chrome's tab APIs.

This isolation is the entire reason extensions are safer than user-script injectors. The host page can't trivially read your extension's storage, and your extension can't accidentally pollute the host's global scope.

Content Security Policy in MV3

MV3 ships a strict default CSP for extension pages — popup, options, side panel:

script-src 'self'; object-src 'self';

Translated: scripts run only from the extension package itself. No inline <script> tags, no eval(), no new Function(), no JavaScript URIs in handlers. Lesson 4's CSP gotcha was a direct consequence of this default.

You can tighten the CSP further (rare), but loosening it is heavily restricted. 'unsafe-eval' is banned outright. 'unsafe-inline' is banned for scripts. 'wasm-unsafe-eval' is allowed if you genuinely need WebAssembly compilation.

host_permissions — What Pages You Can Touch

Separate from the API-capability permissions list, host_permissions declares which URLs the extension is allowed to read DOM from or inject scripts into. Two patterns:

  • Narrow — list specific origins: ["https://news.ycombinator.com/*", "https://*.github.com/*"]. Chrome shows the user exactly which sites are affected at install.
  • Broad["<all_urls>"]. Triggers a stark install warning: "Read and change all your data on all websites." Use only when the extension genuinely needs page access everywhere (ClipDeck will — selection capture must work on any page).

The middle path is activeTab in the API permissions list — a just-in-time grant: when the user clicks the toolbar icon, Chrome temporarily gives the extension the current tab's URL + DOM access, expiring on tab navigation. No install-time warning, no broad surface.

The Remote Code Ban

MV3 forbids fetching JavaScript over the network and executing it. Concretely:

  • No fetch("https://...") followed by eval() on the response.
  • No <script src="https://cdn.example.com/foo.js"> in extension HTML pages.
  • No new Function(remoteString).

What's allowed: data fetched over the network (JSON, text, images) treated as data. The line is execution. If a server tells your extension what to render, fine. If a server tells your extension what to run, banned.

chrome.permissions API — Asking on Demand

For permissions declared under optional_permissions or optional_host_permissions, the extension can request them at runtime via chrome.permissions.request. Chrome shows a consent prompt; the user accepts or denies. This is the polite alternative to declaring everything upfront and scaring users at install.

For ClipDeck, all permissions will stay required-and-declared — there's no useful optional-permission flow for a single-soul utility on a private tailnet. But knowing the API exists matters when designing fleet-distributable extensions that don't want to scare every Mac on install.

MV3 security is strict by default and gets stricter as you tighten configuration. Loosening is heavily gated and audited. Build inside the defaults; the few times you must step outside, document the deletion condition.
CSP error messages are uniquely cryptic. When a popup or side panel silently fails to render JS, the Errors panel often just says "Refused to execute inline script" with a line number — without telling you which file. Searching the codebase for inline <script> tags or onclick= handlers is the recovery path.

Code

MV3 CSP — default vs Pippa Chrome Embed's iframe-allowing override·json
// Default MV3 CSP for extension_pages (you don't have to specify this):
{
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'self';"
  }
}

// Pippa Chrome Embed v0.1 loosens it slightly to allow iframe:
{
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'self'; frame-src http://localhost:5173 http://100.x.x.x:5173;"
  }
}
// (100.x.x.x = your Tailscale / private VPN host; CGNAT range is 100.64.0.0/10)
Three permission scopes — narrow / broad / activeTab·json
// Narrow — Chrome shows specific sites at install:
{
  "host_permissions": [
    "https://news.ycombinator.com/*",
    "https://*.github.com/*"
  ]
}

// Broad — Chrome shows "all websites" warning:
{
  "host_permissions": ["<all_urls>"]
}

// activeTab — no install warning, just-in-time grant on toolbar click:
{
  "permissions": ["activeTab"]
}

External links

Exercise

Open clipdeck/manifest.json. It currently has no permissions field at all — Track 1's hello-world ClipDeck only reads the active tab title via chrome.tabs.query, which works without explicit permissions. Add "permissions": ["activeTab"] to the manifest and reload. Click the toolbar icon — same behavior? Now remove activeTab and instead add "host_permissions": ["<all_urls>"]. Reload, then go to chrome://extensions and click "Details" on ClipDeck. Scroll to "Site access" — notice it now says "On all sites" (the install warning you'd see if shipping). Write one sentence on why activeTab is the right choice for a popup-only extension and why host_permissions is overkill at this stage of ClipDeck.
Hint
activeTab grants temporary access only on user click — no install warning. host_permissions grants permanent access from install — triggers the warning. ClipDeck's later tracks (content script, selection capture across pages) will need host_permissions; Track 1 doesn't. The right permission scales with the actual capability used.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Pippaserious

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.