Hardening Internal Browser Extensions: Least-Privilege Manifest v3 Without Losing Features



Hook: The Extension Update That Gave Support Root Access
Your support team uses a Chrome extension to surface customer data inside Salesforce. A rushed update adds "host_permissions": ["<all_urls>"] and a new content script that injects a side panel. The extension now runs on every tab, not just your domain. A compromised third-party site leverages the extension to read OAuth tokens from local storage and call your internal APIs. Because the extension is signed with your corporate cert, endpoint security ignores it. Incident response discovers your extension is effectively malware waiting to happen.
Developers often treat internal extensions as trusted tooling. This article reframes them as untrusted apps that deserve a full threat model. We break down permissions, messaging, storage, and update channels. Examples target Manifest v3, but concepts apply to Firefox and Edge.
The Problem Deep Dive
Extension pitfalls:
- Broad host permissions. Developers request
*://*/*out of convenience. - Background scripts with privileged tokens. Long-lived API keys sit in extension storage.
- Content scripts running on external origins. Attackers inject scripts that interact with your extension via DOM events.
- Unverified update channels. Self-hosted updates lack signing or integrity checks.
- Lack of telemetry. No visibility into where the extension runs or what it does.
Typical manifest anti-pattern:
{
"manifest_version": 3,
"name": "Support Helper",
"permissions": ["storage", "tabs", "scripting"],
"host_permissions": ["<all_urls>"],
"background": { "service_worker": "background.js" }
}
Background script fetches https://internal-api/auth with a static token stored in chrome.storage.sync.
Technical Solutions
Quick Patch: Restrict Host Permissions and Use Declarative Net Request
Limit host permissions to specific domains:
"host_permissions": ["https://support.example.com/*", "https://api.example.com/*"]
Use declarativeNetRequest rules to block extension requests to unknown origins.
Durable Fix: OAuth Device Flow + Short-Lived Tokens
- Use the OAuth device flow so extension never handles passwords.
- Store refresh tokens in extension
chrome.storage.session(cleared on browser restart). - Request short-lived access tokens with limited scopes.
- Rotate tokens via backend service; revoke on logout.
Background service worker:
chrome.runtime.onInstalled.addListener(() => {
chrome.identity.launchWebAuthFlow({
url: authUrl,
interactive: true,
}, handleAuthResponse);
});
Content Script Isolation
- Inject scripts only on whitelisted origins.
- Use
chrome.scripting.executeScriptwithworld: "ISOLATED". - Communicate via
chrome.runtime.sendMessage; never trust DOM events from the page without validation.
Messaging Security
Implement message validation:
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (sender.origin !== "https://support.example.com") return false;
if (msg.type === "FETCH") {
fetch(apiBase + msg.path, { headers: { Authorization: `Bearer ${token}` } })
.then((r) => r.json())
.then(sendResponse);
return true;
}
});
Reject messages from other extensions or unknown origins.
Storage Hygiene
- Use
chrome.storage.sessionor in-memory caches for sensitive data. - Encrypt data with keys derived from enterprise policies if persistence required.
- Clear storage on logout or when host permissions revoked.
Update Strategy
- Publish via Chrome Web Store private channel with review.
- Sign updates and monitor release pipelines for tampering.
- Implement rollback via version pinning in enterprise policies.
Telemetry
- Emit usage metrics (domains accessed) to backend.
- Alert when extension runs on unexpected origins.
- Monitor for permission changes across versions.
Alprina Policies
Scan manifest files for host_permissions wildcards. Flag background scripts referencing chrome.storage.sync for tokens. Ensure content_security_policy restricts remote code.
Testing & Verification
- Unit test messaging handlers with Jest or Mocha.
- End-to-end tests using Puppeteer: load controlled pages and ensure extension refuses to run on unauthorized origins.
- Pen test: simulate malicious page sending
window.postMessageevents; verify extension ignores them. - Review update pipeline: ensure build artifacts match source (reproducible builds).
Common Questions & Edge Cases
Do we need CSP in extensions? Yes. Set "content_security_policy": { "extension_pages": "default-src 'self'" } to block remote scripts.
How to support multiple internal domains? Enumerate them explicitly; avoid wildcards. Use dynamic host permissions request if needed (chrome.permissions.request).
Can we bypass Chrome Web Store review? Enterprise policy distribution is possible but increases risk. Maintain strict signing and review processes.
What about Firefox? Port the same principles. Firefox supports Manifest v2 but offers similar permission APIs.
How to handle service worker shutdown? Persist minimal state and handle reconnection gracefully. Do not rely on long-lived background contexts.
Conclusion
Browser extensions are powerful but dangerous. Tame them with scoped permissions, short-lived tokens, strict messaging, and monitored update channels. Treat every extension release like shipping a privileged application to customer machines-because that is exactly what you are doing.