XSS Prevention in Chrome Extensions
A security deep-dive into XSS attack vectors specific to Chrome extensions, with practical prevention techniques for popups, content scripts, and background workers.
Table of Contents
XSS Prevention in Chrome Extensions
Your extension runs with elevated privileges. An XSS vulnerability does not just steal cookies — it can access every tab, read browsing history, and modify web pages.
Cross-site scripting in Chrome extensions is more dangerous than XSS in regular web applications. A web app's XSS is confined to that app's origin and permissions. An extension's XSS inherits the extension's permissions — which often include access to all URLs, storage of sensitive data, tab manipulation, and the ability to inject scripts into any page the user visits.
This guide covers every XSS vector specific to Chrome extensions, with concrete vulnerable-versus-safe code examples, sanitization strategies, and a prevention checklist you can use to audit your own extension.
Extension-Specific XSS Attack Vectors#
Regular web XSS focuses on reflected, stored, and DOM-based vectors. Extensions add several unique attack surfaces that web developers are not accustomed to thinking about.
innerHTML in Popup/Options UI
The most common vector. Developers render dynamic content using innerHTML, inserting unsanitized user input or data from external sources into the extension's trusted UI.
Content Script DOM Injection
Content scripts modify host pages by inserting HTML. If the inserted HTML includes data from the host page (like selected text or URL parameters), it can execute attacker-controlled scripts.
Message Passing Without Validation
Extensions pass messages between content scripts, background workers, and popups. If the receiving end trusts message data without sanitization, a compromised content script can inject payloads.
Web Accessible Resources Exploitation
Resources declared as web-accessible can be loaded by any website. If these resources accept parameters or postMessage data, they become XSS entry points.
Storage-Based XSS
Data stored via chrome.storage may contain malicious payloads. When this data is later rendered in the popup or options page without sanitization, the payload executes.
eval() and Dynamic Code Execution
While Manifest V3's CSP blocks eval() by default, some developers find workarounds or use offscreen documents where the CSP is less restrictive. Any form of dynamic code execution is an XSS risk.
Vulnerable vs. Safe Code#
The difference between vulnerable and safe extension code often comes down to a single function call. Let us walk through the most common patterns.
Sanitization Strategies#
There are three tiers of protection against XSS in extensions, and you should use all three.
Tier 1: Avoid innerHTML entirely. Use textContent for text, createElement for structure, and classList / setAttribute for styling. This eliminates the most common XSS vector with zero overhead.
Tier 2: When you need HTML rendering, use DOMPurify. It is the gold standard for HTML sanitization. Configure it with the most restrictive allowlist that serves your use case.
import DOMPurify from 'dompurify';
// Strictest config: only allow basic formatting
const STRICT_CONFIG = {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br', 'ul', 'ol', 'li'],
ALLOWED_ATTR: [],
ALLOW_DATA_ATTR: false,
};
// Moderate config: allow links but sanitize href
const MODERATE_CONFIG = {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br', 'a', 'ul', 'ol', 'li', 'code'],
ALLOWED_ATTR: ['href', 'title'],
ALLOW_DATA_ATTR: false,
ADD_HOOKS: true,
};
// Hook to ensure only safe URLs in href attributes
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
if (node.hasAttribute('href')) {
const href = node.getAttribute('href') || '';
if (!href.startsWith('https://') && !href.startsWith('http://')) {
node.removeAttribute('href');
}
}
});Tier 3: Content Security Policy. Manifest V3 enforces a strict CSP by default, but you can tighten it further. The default CSP for MV3 extensions is:
{
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
}
}This blocks inline scripts and eval(). Do not loosen it. If your code needs unsafe-eval or unsafe-inline, that is a design problem you should fix rather than a CSP restriction you should weaken.
- Use textContent instead of innerHTML for all text rendering
- Sanitize with DOMPurify before any innerHTML assignment — configure the strictest allowlist possible
- Validate every field of every message received via chrome.runtime.onMessage
- Verify sender.id matches your extension ID before processing messages
- Use parameterized queries if your extension uses IndexedDB or WebSQL
- Audit web-accessible resources — minimize what is exposed and validate all inputs they receive
- Keep Manifest V3's default CSP — never add unsafe-eval or unsafe-inline
- Use Trusted Types if targeting Chrome 83+ (which is almost all users)
- Use innerHTML with any data from storage, messages, user input, or external APIs
- Trust data from content scripts — they run in attacker-controlled pages
- Use document.write() anywhere in your extension
- Construct HTML strings with template literals and dynamic data
- Expose web-accessible resources that accept query parameters without validation
- Use eval(), new Function(), or setTimeout with string arguments
- Assume storage data is safe — it may have been written by a compromised component
- Skip sanitization because 'only I use this extension'
Sanitization Library Comparison#
If you need HTML rendering (and therefore sanitization), these are the established options.
| Feature | Library | Size (minified) | Speed | Maintenance | Extension Suitability |
|---|---|---|---|---|---|
| DOMPurify | DOMPurify | ~19KB | Fast (DOM-based) | Actively maintained, battle-tested | Excellent — industry standard |
| sanitize-html | sanitize-html | ~45KB | Moderate (string-based) | Actively maintained | Good — more configurable but heavier |
| isomorphic-dompurify | isomorphic-dompurify | ~22KB | Fast (wraps DOMPurify) | Active | Good — works in service workers too |
| xss (js-xss) | xss (js-xss) | ~30KB | Fast (regex + string) | Maintained | Acceptable — less thorough than DOMPurify |
| Trusted Types (native) | Trusted Types | 0KB (browser API) | Fastest (compile-time) | Chrome built-in | Best — zero dependency, enforced by browser |
Trusted Types deserve special attention. They are a browser-native API that prevents DOM XSS at the platform level. When you enable Trusted Types, the browser rejects any attempt to assign a raw string to innerHTML, document.write, or other dangerous sinks. You must create a "policy" that explicitly sanitizes values before assignment.
// Enable Trusted Types via CSP header or meta tag in your HTML
// <meta http-equiv="Content-Security-Policy" content="trusted-types myPolicy">
if (window.trustedTypes) {
const policy = trustedTypes.createPolicy('myPolicy', {
createHTML: (input: string) => {
return DOMPurify.sanitize(input, {
ALLOWED_TAGS: ['b', 'i', 'p', 'br'],
ALLOWED_ATTR: [],
});
},
});
// Now innerHTML requires a TrustedHTML value
const container = document.getElementById('content')!;
container.innerHTML = policy.createHTML(userInput); // Sanitized automatically
// container.innerHTML = userInput; // This throws a TypeError!
}Common Vulnerability Patterns in Real Extensions#
These are patterns found in real extensions during security audits. Each one is exploitable.
Pattern 1: URL parameter injection in options page.
Many extensions pass data to their options page via URL fragments: options.html#setting=theme. If the options page reads location.hash and injects it into the DOM, an attacker can craft a link that executes arbitrary JavaScript when the user opens it.
Pattern 2: postMessage from web-accessible resources.
If your extension has a web-accessible page that listens for window.postMessage, any website can send messages to it. Without origin validation, this is a direct XSS vector.
// DANGEROUS: No origin check on postMessage
window.addEventListener('message', (event) => {
document.getElementById('display').innerHTML = event.data.content;
});
// SAFE: Validate origin and sanitize
window.addEventListener('message', (event) => {
if (event.origin !== 'https://trusted-domain.com') return;
const sanitized = DOMPurify.sanitize(event.data.content);
document.getElementById('display').innerHTML = sanitized;
});Pattern 3: Rendering fetched API data. Extensions that fetch data from external APIs and render it in the popup often trust the API response implicitly. If the API is compromised or returns user-generated content, unsanitized rendering creates XSS.
Test Your Security Knowledge#
1. Which is the safest way to display user-generated text in an extension popup?
2. A content script receives data from chrome.runtime.onMessage. Should you trust this data?
3. What does Manifest V3's default CSP prevent?
4. When using DOMPurify, what is the most secure ALLOWED_TAGS configuration?
XSS Prevention Audit Checklist#
Use this checklist to audit your extension for XSS vulnerabilities. Go through every file in your extension and check each item.
Checklist
- Search for every innerHTML assignment — replace with textContent or add DOMPurify
- Search for document.write — remove it entirely (there is no safe use in extensions)
- Search for insertAdjacentHTML — validate that only static content is inserted
- Audit every chrome.runtime.onMessage listener — validate message structure and sender
- Audit every window.postMessage listener — verify event.origin
- Check all web-accessible resources for input handling vulnerabilities
- Verify CSP is MV3 default or stricter — no unsafe-eval or unsafe-inline
- Check that content scripts never use innerHTML with host page data
- Verify that data from chrome.storage is sanitized before rendering
- Check that fetch/XHR response data is sanitized before DOM insertion
- Audit URL parameter handling in options page and any other extension pages
- Verify no use of eval(), new Function(), or string-based setTimeout/setInterval
- Check for Trusted Types compatibility if targeting modern Chrome versions
- Test with a CSP violation reporter to catch any violations in production
Extension security is not glamorous work. Nobody notices when your XSS prevention is working correctly. But one vulnerability in an extension with elevated permissions can compromise every website your users visit. The cost of prevention — using textContent, adding DOMPurify, validating messages — is tiny compared to the cost of a breach. Invest the time upfront, audit regularly, and treat every piece of dynamic data as hostile until proven otherwise.
Extension XSS is more dangerous than web app XSS because extensions operate with elevated browser permissions. The three-layer defense is: (1) avoid innerHTML entirely by using textContent and DOM APIs, (2) when HTML rendering is necessary, sanitize with DOMPurify using the strictest possible allowlist, and (3) rely on Manifest V3's strict CSP and consider adding Trusted Types. Audit every message listener, every storage read that touches the DOM, and every web-accessible resource. Treat all data — including data from your own extension's storage — as untrusted input.
Continue reading
Related articles
Chrome Extension Security Audit Checklist
A comprehensive security audit framework for Chrome extensions covering CSP, permissions, input validation, storage, network security, and third-party dependencies.
Manifest V3 Permissions: Best Practices
Best practices for requesting and managing permissions in Manifest V3 Chrome extensions. Covers required vs optional permissions, host permissions, and review implications.
Lessons From Building 100+ Chrome Extensions
Hard-won lessons about architecture, performance, monetization, reviews, and burnout from a developer who shipped over 100 Chrome extensions across seven years.