Chrome Extension Security Best Practices — Developer Guide
13 min readSecurity Best Practices for Chrome Extensions
Introduction
- Extensions have elevated privileges — security matters more than in regular web apps
- Common attack vectors: XSS in extension pages, message spoofing, permission over-reach
1. Principle of Least Privilege {#1-principle-of-least-privilege}
- Only request permissions you actively use
- Use
optional_permissionsfor features users may not enable - Use
@theluckystrike/webext-permissionsrequestPermission()to request at runtime instead of install time - Use
activeTabinstead of<all_urls>whenever possible - Example:
const result = await requestPermission('tabs'); if (result.granted) { /* proceed */ }
2. Content Security Policy (CSP) {#2-content-security-policy-csp}
- MV3 default CSP:
script-src 'self'; object-src 'self' - Never use
unsafe-evalorunsafe-inline - No remote code loading — bundle everything locally
- Cross-ref:
docs/mv3/content-security-policy.md
3. Secure Messaging {#3-secure-messaging}
- Validate message origins in
onMessagehandlers - Never trust data from content scripts blindly — web pages can manipulate the DOM
- Use
@theluckystrike/webext-messagingtyped messages to enforce request/response contracts:type Messages = { saveBookmark: { request: { url: string; title: string }; response: { id: string } }; }; const messenger = createMessenger<Messages>(); // Type system prevents sending malformed messages - Handle
MessagingErrorfor failed communications - Never pass
eval()-able strings through messages
4. Storage Security {#4-storage-security}
chrome.storage.localis only accessible to your extension — prefer it for sensitive datachrome.storage.syncsyncs across devices — don’t store secrets there- Use
@theluckystrike/webext-storageschema validation to prevent storing unexpected data types:const schema = defineSchema({ apiKey: 'string', isEnabled: 'boolean' }); const storage = createStorage(schema, 'local'); // storage.set('apiKey', 123) — TypeScript error! Must be string - Never store plaintext passwords or tokens — use
chrome.identityfor OAuth
5. Content Script Safety {#5-content-script-safety}
- Sanitize all data from web pages before using it
- Use
textContentinstead ofinnerHTMLwhen reading page data - Never inject user-controlled strings with
innerHTMLordocument.write - Use
DOMPurifyif you must insert HTML from untrusted sources
6. XSS Prevention in Extension Pages {#6-xss-prevention-in-extension-pages}
- Extension popups, options, and background pages are targets for XSS
- Never use
innerHTMLwith dynamic content — use DOM APIs or a framework - Don’t use
eval(),new Function(),setTimeout(string)— all blocked by MV3 CSP anyway - Sanitize any data displayed from
chrome.storageor messages
7. Network Request Security {#7-network-request-security}
- Always use HTTPS for external requests
- Validate and sanitize API responses before processing
- Use
fetch()with proper error handling - Set appropriate
Content-Typeheaders
8. Update and Supply Chain Security {#8-update-and-supply-chain-security}
- Pin dependency versions in package.json
- Audit dependencies with
npm audit - Use a bundler to avoid shipping
node_modules - Chrome Web Store auto-updates — ensure every version is thoroughly tested
Security Checklist
Introduction
Chrome extensions operate with elevated privileges compared to regular web applications. They can access sensitive APIs, modify web pages, and store user data. This makes security a paramount concern. Common attack vectors include cross-site scripting (XSS), message spoofing, permission over-reach, and supply chain vulnerabilities.
This guide covers essential security practices aligned with Google’s documentation at developer.chrome.com/docs/extensions/develop/migrate/improve-security.
1. Principle of Least Privilege
Request only the minimum permissions necessary. Use optional_permissions and request at runtime when needed. Prefer activeTab over <all_urls>.
{
"optional_permissions": ["tabs", "bookmarks"],
"permissions": ["activeTab", "storage"]
}
async function requestPermission() {
const result = await chrome.permissions.request({ permissions: ['bookmarks'] });
if (result.granted) { /* proceed */ }
}
2. Content Security Policy (CSP)
Manifest V3 enforces strict CSP: script-src 'self'; object-src 'self'. Never use unsafe-eval or unsafe-inline.
{
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; style-src 'self' 'unsafe-inline'"
}
}
// BAD - XSS vulnerability
element.innerHTML = userInput;
// GOOD - Safe DOM manipulation
element.textContent = userInput;
3. Avoiding Remotely Hosted Code
Manifest V3 prohibits remote code execution. Bundle all JavaScript locally. Never fetch and execute external code.
// BAD
fetch('https://cdn.example.com/script.js').then(code => eval(code));
// GOOD
import { helperFunction } from './utils/helper.js';
4. Input Sanitization in Content Scripts
Treat all web page data as untrusted. Use DOMPurify for HTML sanitization.
import DOMPurify from 'dompurify';
function renderUserContent(htmlContent) {
const sanitized = DOMPurify.sanitize(htmlContent, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
});
document.getElementById('content').innerHTML = sanitized;
}
function validateMessage(message) {
return message && typeof message === 'object' && typeof message.action === 'string';
}
5. XSS Prevention in Extension Pages
Use textContent instead of innerHTML. Avoid document.write(), eval(), and new Function().
function safeRender(userName) {
const div = document.createElement('div');
div.textContent = userName; // Automatically escapes HTML
document.body.appendChild(div);
}
6. Secure Storage
Use chrome.storage.local for sensitive data. Never store secrets in chrome.storage.sync. Use chrome.identity for OAuth.
const secureStorage = {
async setSecure(key, value) {
await chrome.storage.local.set({ [key]: value });
},
async getSecure(key) {
return (await chrome.storage.local.get(key))[key];
}
};
7. OAuth Token Security
Use chrome.identity for OAuth flows. Implement token refresh. Clear tokens on logout.
async function authenticate() {
const redirectUri = chrome.identity.getRedirectURL();
const authUrl = `https://oauth.provider.com/authorize?client_id=${CLIENT_ID}&redirect_uri=${redirectUri}&response_type=token`;
const responseUrl = await chrome.identity.launchWebAuthFlow({
url: authUrl,
interactive: true
});
const token = new URL(responseUrl).hash.split('&')[0].split('=')[1];
await secureStorage.setSecure('oauth_token', token);
return token;
}
8. Native Messaging Security
Validate all native messages. Use strict schema validation. Limit host access.
class NativeMessenger {
async sendMessage(message) {
const allowedTypes = ['ping', 'getData', 'saveData'];
if (!message?.type || !allowedTypes.includes(message.type)) {
throw new Error('Invalid message type');
}
return chrome.runtime.sendNativeMessage(APP_ID, message);
}
}
9. Web Accessible Resources
Restrict access using matches. Avoid exposing sensitive files. Use unique filenames.
{
"web_accessible_resources": [
{ "resources": ["images/*.png"], "matches": ["https://trusted-site.com/*"] }
]
}
10. Cross-Origin Request Security
Validate URL origins. Use HTTPS. Validate all API responses.
async function secureFetch(url) {
const allowedOrigins = ['https://api.example.com'];
const urlObj = new URL(url);
if (!allowedOrigins.includes(urlObj.origin)) {
throw new Error('Origin not allowed');
}
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
11. Message Validation
Validate message origins and structure in onMessage handlers.
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (sender.id !== chrome.runtime.id) return false;
if (!validateMessage(message)) {
sendResponse({ error: 'Invalid message' });
return false;
}
handleMessage(message)
.then(result => sendResponse({ success: true, data: result }))
.catch(error => sendResponse({ error: error.message }));
return true;
});
12. DOM-Based Attack Protection
Avoid dangerous patterns: innerHTML, document.write, eval, setTimeout(string).
// Dangerous patterns to avoid:
element.innerHTML = userInput; // XSS!
document.write(htmlContent); // Blocked in MV3
eval(userData); // Blocked by CSP
// Safe alternatives:
element.textContent = userInput;
const span = document.createElement('span');
span.textContent = userData;
element.appendChild(span);
13. Safe innerHTML Alternatives
Use DOMPurify for HTML when necessary. Create elements programmatically.
import DOMPurify from 'dompurify';
function safeRenderHtml(html) {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'ul', 'li'],
ALLOWED_ATTR: ['class']
});
document.getElementById('container').innerHTML = clean;
}
function createSafeElements(userData) {
const list = document.createElement('ul');
userData.forEach(item => {
const li = document.createElement('li');
li.textContent = item.name;
list.appendChild(li);
});
return list;
}
14. CSP Header Configuration
Configure strict CSP in manifest. Use report-uri for monitoring.
{
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; img-src 'self' data: https:; connect-src https://api.example.com"
}
}
15. Evaluating Third-Party Dependencies
Audit dependencies regularly. Pin versions. Use minimal dependencies.
npm audit
npm outdated
- Pin exact versions in package.json
- Use npm audit fix cautiously
- Review dependencies for abandonment
16. Security Review Checklist
- Only essential permissions requested
- Optional permissions used where possible
- CSP configured without unsafe-eval/unsafe-inline
- All JavaScript bundled locally
- No eval() or dynamic code execution
- All messages validated and typed
- No innerHTML with dynamic content
- Sensitive data in chrome.storage.local only
- OAuth tokens handled properly
- HTTPS for all network requests
- Dependencies audited
Related Articles
Related Articles
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.
- Dependencies audited (npm audit)
- Web accessible resources minimized
17. Common Vulnerabilities
- XSS: Injecting untrusted content - Use textContent, sanitize HTML
- Message Injection: Forged messages - Validate origins and structure
- Privilege Escalation: Excessive permissions - Use least privilege
- Insecure Storage: Sensitive data exposed - Use chrome.storage.local
- Dependency Vulnerabilities: Compromised libraries - Regular audits
18. Google Security Review Process
Google looks for: permission justification, user data handling, security practices, potential abuse prevention.
Common Rejection Reasons
- Excessive permissions without justification
- Remote code execution capability
- Inadequate input validation
- Storing sensitive data insecurely
- CSP violations (unsafe-inline, unsafe-eval)
- Vulnerable dependencies
Preparing for Review
- Document all permission justifications
- Update privacy policy with data handling
- Test thoroughly before submission
- Fix all security vulnerabilities
- Provide video demonstration
19. Code Examples Summary
// manifest.json
{
"manifest_version": 3,
"permissions": ["activeTab", "storage"],
"optional_permissions": ["bookmarks"],
"host_permissions": ["https://api.example.com/*"],
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
}
}
// Secure message handling
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (sender.id !== chrome.runtime.id || !validateMessage(message)) {
return false;
}
handleMessage(message).then(sendResponse).catch(e => sendResponse({ error: e.message }));
return true;
});
// Safe content script
function displayData(data) {
document.getElementById('data').textContent = data;
}
20. References
- Improve Security - Chrome Extensions
- Manifest V3 Migration Guide
- Content Security Policy
- Chrome Web Store Program Policies
Conclusion
Security requires defense-in-depth: principle of least privilege, strict CSP, input validation, secure storage, and regular audits. Treat all external data as potentially malicious. Keep dependencies updated. Build secure extensions that protect users.