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.
Table of Contents
Extension Security Audit Checklist
Find vulnerabilities before attackers do.
Browser extensions operate in one of the most privileged environments in computing. They can read every page a user visits, modify network requests, access cookies, and interact with sensitive APIs. A single vulnerability in a popular extension can compromise millions of users overnight.
In 2025 alone, over thirty Chrome extensions with combined install bases exceeding 2.5 million users were found to contain malicious code or critical vulnerabilities. Most of these were not sophisticated attacks — they exploited basic security oversights that a structured audit would have caught.
This checklist is your systematic framework for auditing Chrome extension security. Whether you are reviewing your own code or evaluating a third-party extension, work through each phase methodically. Skip nothing.
Permission Audit
Review every declared permission. Verify each is necessary and uses the minimum scope required.
Content Security Policy
Audit CSP headers for unsafe-eval, unsafe-inline, and overly broad source directives.
Input Validation
Trace every data entry point. Verify sanitization of messages, storage reads, and DOM injections.
Storage Security
Review what is stored, how it is encrypted, and who can access it across extension contexts.
Network Security
Audit all external requests, API keys, certificate validation, and data transmission.
Third-Party Dependencies
Scan dependencies for known vulnerabilities, review update policies, and verify supply chain integrity.
Phase 1: Permission Audit#
Permissions are the attack surface. Every permission your extension requests is a capability that can be exploited if your code is compromised. The principle of least privilege is not a suggestion — it is your primary defense.
Checklist
- Review manifest.json permissions array — remove any permission not actively used in code
- Replace <all_urls> host permission with specific domain patterns
- Use activeTab instead of persistent host permissions where possible
- Verify optional_permissions are used for features that not all users need
- Check that host_permissions match only domains the extension actually communicates with
- Confirm no unnecessary API permissions (tabs, history, bookmarks) are declared
- Review content_scripts matches — restrict to the narrowest URL patterns possible
- Verify externally_connectable is restricted to specific extensions/domains, not wildcards
The most common permission over-reach we see in audits:
// BAD — requests access to every website
{
"permissions": ["tabs", "storage", "cookies"],
"host_permissions": ["<all_urls>"]
}
// GOOD — requests only what's needed
{
"permissions": ["storage"],
"optional_permissions": ["tabs"],
"host_permissions": ["https://api.yourservice.com/*"]
}The tabs permission deserves special scrutiny. Without it, chrome.tabs.query() still returns tab IDs and basic info. With it, you also get url, title, and favIconUrl — which means full browsing history visibility. Only request tabs if you genuinely need URL access.
Phase 2: Content Security Policy#
Manifest V3 enforces stricter CSP defaults than V2, but "stricter" does not mean "secure." Your CSP configuration determines whether an attacker who finds an injection point can execute arbitrary code.
Checklist
- Verify no unsafe-eval in extension_pages CSP
- Verify no unsafe-inline in extension_pages CSP
- Check that script-src does not include wildcard domains (*.example.com)
- Ensure no CDN URLs in script-src — bundle all scripts locally
- Review sandbox CSP separately if using sandboxed pages
- Confirm that content_scripts do not inject inline scripts into web pages
- Verify CSP is not relaxed in the sandbox directive beyond what is necessary
// manifest.json — Secure CSP configuration
{
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'none'; base-uri 'self';"
}
}The critical rule: never load remote scripts. Every JavaScript file your extension executes must be bundled in the extension package. This is enforced by MV3 for extension pages, but content scripts can still inject <script> tags into web pages that load remote code — and attackers know this.
- Bundle all JavaScript locally within the extension package
- Use script-src 'self' as the base policy for extension pages
- Implement object-src 'none' to prevent plugin-based attacks
- Use sandboxed pages for any eval-like operations (template engines, etc.)
- Validate CSP headers using the CSP Evaluator tool from Google
- Add CDN URLs to script-src — supply chain attack vector
- Use unsafe-eval even 'temporarily' during development
- Use unsafe-inline for convenience — it defeats CSP entirely
- Relax CSP in production to match development settings
- Trust that MV3 defaults are sufficient — always set explicit policies
Phase 3: Input Validation and Injection Prevention#
Every data flow into your extension is a potential injection vector. Messages from content scripts, data from storage, URL parameters, and clipboard contents all need sanitization before they touch the DOM or are used in API calls.
These numbers come from aggregate audit data across extension security research. The XSS figure is particularly alarming — nearly every extension that renders dynamic content has at least one path where unsanitized data reaches innerHTML.
Message Validation#
// VULNERABLE — no sender validation, no input sanitization
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
document.getElementById('output').innerHTML = message.data;
});
// SECURE — validates sender, sanitizes input, uses textContent
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// Verify the message comes from our own extension
if (sender.id !== chrome.runtime.id) {
console.warn('Message from unknown sender:', sender.id);
return;
}
// Validate message structure
if (typeof message?.data !== 'string' || message.data.length > 10000) {
return;
}
// Use textContent instead of innerHTML to prevent XSS
const output = document.getElementById('output');
if (output) {
output.textContent = message.data;
}
});Checklist
- All runtime.onMessage handlers validate sender.id matches chrome.runtime.id
- No use of innerHTML with unsanitized data — use textContent or DOMPurify
- All runtime.onMessageExternal handlers verify sender against allowlist
- URL construction uses the URL API, not string concatenation
- JSON.parse calls are wrapped in try-catch with schema validation
- Content scripts do not eval() or Function() any data from web pages
- Popup and options page validate all URL parameters before use
- Clipboard data is sanitized before rendering or processing
DOM Injection Safety#
When your extension needs to inject HTML into web pages via content scripts, the risk surface expands dramatically. The web page's JavaScript can observe and manipulate anything you inject.
// VULNERABLE — injecting HTML string into hostile page
function showNotification(text: string) {
document.body.innerHTML += `<div class="ext-notify">${text}</div>`;
}
// SECURE — using Shadow DOM for isolation
function showNotification(text: string) {
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'closed' });
const container = document.createElement('div');
container.textContent = text; // textContent, never innerHTML
const style = document.createElement('style');
style.textContent = `.notify { position: fixed; top: 16px; right: 16px;
background: #1a1a2e; color: #fff; padding: 12px 20px;
border-radius: 8px; z-index: 2147483647; font-family: system-ui; }`;
container.className = 'notify';
shadow.appendChild(style);
shadow.appendChild(container);
document.body.appendChild(host);
setTimeout(() => host.remove(), 5000);
}The mode: 'closed' shadow DOM prevents the host page's JavaScript from accessing your injected elements via element.shadowRoot. This is critical for extensions that display sensitive information on web pages.
Phase 4: Storage Security#
Checklist
- No plaintext passwords, tokens, or API keys in chrome.storage.local or sync
- Sensitive data is encrypted with Web Crypto API before storage
- Encryption keys are not stored alongside encrypted data
- Temporary secrets use chrome.storage.session instead of persistent storage
- Storage reads in content scripts are validated and minimal
- Old or unused data is cleaned up — no stale tokens accumulating
- Storage quota usage is monitored to prevent silent data loss from quota exceeded errors
Phase 5: Network Security#
Every HTTP request your extension makes is a potential data leak or interception point. This phase reviews how your extension communicates with external services.
Checklist
- All external requests use HTTPS — no HTTP endpoints
- API keys are stored securely, not hardcoded in source files
- fetch() calls include appropriate timeout handling
- Server responses are validated against expected schemas before use
- No sensitive data is included in URL query parameters (use POST body instead)
- CORS preflight responses from your API server restrict origins to your extension ID
- Certificate pinning is implemented for critical API endpoints if using native messaging
- Rate limiting is implemented client-side to prevent accidental DoS of your own API
- Error responses do not leak sensitive data to console in production builds
// Secure API communication pattern
const API_BASE = 'https://api.yourextension.com/v1';
interface ApiResponse<T> {
data: T;
error: string | null;
}
async function secureRequest<T>(
endpoint: string,
options: RequestInit = {}
): Promise<ApiResponse<T>> {
const token = await getEncryptedToken(); // Retrieved and decrypted from storage
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000); // 10s timeout
try {
const response = await fetch(`${API_BASE}${endpoint}`, {
...options,
signal: controller.signal,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
...options.headers,
},
});
clearTimeout(timeout);
if (!response.ok) {
// Don't log response body — it might contain sensitive data
return { data: null as T, error: `Request failed: ${response.status}` };
}
const data = await response.json();
// Validate response shape before trusting it
if (!isValidResponseShape<T>(data)) {
return { data: null as T, error: 'Invalid response format' };
}
return { data, error: null };
} catch (err) {
clearTimeout(timeout);
return { data: null as T, error: 'Network request failed' };
}
}Phase 6: Third-Party Dependencies#
Supply chain attacks are the fastest-growing threat vector for browser extensions. A single compromised npm package can inject malicious code into your extension at build time.
Checklist
- Run npm audit (or equivalent) and resolve all high/critical vulnerabilities
- Pin exact dependency versions in package.json — no ^ or ~ ranges for production deps
- Use a lockfile (package-lock.json or yarn.lock) and commit it to version control
- Review the dependency tree — check transitive dependencies, not just direct ones
- Verify that no dependency makes unexpected network requests at build or runtime
- Check npm package download trends — sudden ownership changes are a red flag
- Use Snyk, Socket.dev, or similar tools for continuous dependency monitoring
- Audit any web-accessible resources — ensure they cannot be loaded by arbitrary websites
- Remove unused dependencies — every package is attack surface
Vulnerability Severity Distribution#
Based on aggregate data from extension security audits, here is where most vulnerabilities fall in terms of severity:
The bulk of findings are medium severity — things like overly broad permissions, missing sender validation on messages, and storage of sensitive data without encryption. These are not dramatic exploits, but they are the building blocks an attacker chains together to achieve a critical impact.
Common Attack Patterns to Test For#
Automated Security Scanning#
Manual audits are essential, but automated tooling catches low-hanging fruit faster. Integrate these into your CI pipeline:
# Run npm audit for dependency vulnerabilities
npm audit --production --audit-level=high
# Use retire.js to check for known vulnerable libraries
npx retire --js --path ./dist/
# Scan for hardcoded secrets
npx secretlint "src/**/*"
# Static analysis for common extension vulnerabilities
# (hypothetical tool - use CRXcavator or similar in practice)
npx extension-audit ./dist/manifest.json- Run security scans on every pull request, not just before releases
- Audit permissions after every feature addition — scope creep is real
- Use chrome.storage.session for temporary secrets
- Implement CSP reporting to detect policy violations in production
- Enable two-factor authentication on all extension store accounts
- Review web_accessible_resources on every release — minimize exposed files
- Use closed Shadow DOM when injecting UI into web pages
- Test with extensions like Chrome DevTools Security panel
- Ship console.log statements that output sensitive data
- Trust content script messages without sender validation
- Store API keys in source code — use environment variables at build time
- Allow web_accessible_resources with broad match patterns
- Ignore npm audit warnings because 'it is just a dev dependency'
- Use innerHTML to render any data that originated outside your extension
- Skip security review for 'small' updates — attackers exploit small changes
- Assume MV3 makes your extension automatically secure
This checklist gives you a systematic starting point, but security is ongoing. Schedule quarterly audits. Subscribe to Chrome extension security advisories. Monitor your dependencies for new CVEs. The extensions that stay secure are the ones that treat security as a continuous practice, not a one-time review. Every new feature, every dependency update, every API change is an opportunity for a vulnerability to enter your codebase.
For more on building secure extensions, see our guides on Manifest V3 Permissions Best Practices and the Complete Guide to Manifest V3. Understanding the permission model deeply is the foundation of extension security.
Continue reading
Related articles
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.
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.