Extension Security
1. Using Strict Content Security Policies.
Section titled “1. Using Strict Content Security Policies.”Writing stricter Content Security Policies (CSPs) for Google Chrome Extensions helps enhance security by preventing Cross-Site Scripting (XSS) and other code injection attacks. Stricter CSP’s limits the sources from which the content can be loaded.
-
Chrome Extensions run in privileged contexts. A compromised extension can:
-
Access sensitive browser APIs.
-
Steal user data.
-
Execute arbitrary code.
-
For MV3, the default CSP is:
"content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self'" }This is stricter than MV2 (which often allowed unsafe-eval) but can still be tightened further.
-
Ways to tigthen CSPs:
-
Disallow Inline Scripts
-
Restrict img-src, media-src, and others
"content_security_policy": {"extension_pages": "default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self'; object-src 'none';"}Explanation:
default-src 'none': blocks all by defaultcript-src 'self': only allow internal JSconnect-src 'self': allow fetch/XHR to selfimg-src 'self': only internal imagesstyle-src 'self': disallow inline styles and external stylesheetsobject-src 'none': completely block plugins -
2. Minimizing Permissions
Section titled “2. Minimizing Permissions”Minimizing permissions in a Google Chrome extension is crucial for security because of the privileged access that extensions can have to a user’s browser and data.
- Reducing Attack Surface:
- Every permission that is granted is a potential entry point for an attacker.
- Fewer permissions means less ways for bad actors to exploit the extension.
2.Preventing Privelege Escalation: In a compromised extension, excessive permissions can let the attacker: * Access sensitive data (emails, cookies, bookmarks) * Control browser actions * Manipulate web requests * Restricting permissions helps contain damage.
- Easier Maintenance and Auditing
With fewer permissions:
-
Code reviews are simpler
-
Security audits are faster
-
It’s easier to reason about what the extension is allowed to do
Best Practices: | Why it helps:Use host_permissions selectively | Don’t request <all_urls> unless necessary. Use specific domains.Use optional permissions (optional_permissions) | Only request permissions when needed, and ask at runtime.Avoid persistent background scripts if possible | Use service workers (Manifest V3) to reduce runtime exposure.Use declarative APIs (declarativeNetRequest) | Safer than webRequest, which can expose full request/response data.Audit permissions regularly | Refactor or remove unnecessary permissions as features change.
-
3. Regular Updates and Monitoring
Section titled “3. Regular Updates and Monitoring”For depveloperes it is very important to patch any newly found vulnerabilities promptly.
-
Monitor security advisories (e.g., Chromium Blog, CVE databases) for:
- JavaScript vulnerabilities
- Extension API changes
- Third-party library issues
Fix identified issues immediately!
2.Updating Manifest and Dependencies:
-
Ensure you’re using the latest stable Manifest version (Manifest V3 is currently required).
-
Reghularly udpate and audit third-party libraries
You can use tools like “npm audit” or “Snyk”
3.Use Semantic Versioning:
- What is Semantic Versioning?
Semantic Versioning (SemVer) is a structured way to version software using a three-part number:
MAJOR.MINOR.PATCHFor Example:
1.4.2Each segment conveys meaning about the update’s impact:
Segment When to Increment Description MAJOR Breaking changes Incompatible API or behavior changes MINOR New features Backward-compatible feature additions PATCH Bug fixes / small changes Backward-compatible fixes, performance improvements, or security patches -
The use of SemVer in Extensions:
- Helps users know what kind of changes to expect.
- Makes it easier for Google reviewers to track meaningful updates.
-
Helps Security Patch management:
- Clear PATCH version increases make it easier to distinguish security updates from new features.
-
Aids in Automation:
- Automated deployment tools and CI/CD pipelines can detect when to trigger updates based on version increments.
How to Apply Semantic Versioning in Your Extension?:
-
Update manifest.json version
"version": "2.3.1"This is required by Chrome and is the official version used for auto-updates.
-
Match It with CHANGELOG.md
## [2.3.1] - 2025-05-30### Fixed- Patched a bug that caused login tokens to persist incorrectly.- Fixed performance issue in background script.## [2.3.0] - 2025-05-25### Added- New feature: Ability to export settings as JSON.## [2.0.0] - 2025-05-01### Changed- Switched from Manifest V2 to V3. This is a breaking change.- Dropped support for Chrome < 90.
Make sure the logs are structure consistently.
-
Version Bumping Strategy
Situation New Version Why You fix a bug in a stable release 1.2.3 → 1.2.4PATCH update You add a feature (e.g., dark mode) 1.2.3 → 1.3.0MINOR update You migrate to Manifest V3 1.2.3 → 2.0.0MAJOR update You fix a security vulnerability 1.3.2 → 1.3.3PATCH update (critical) You refactor internal code (no changes) No bump needed Optional, if behavior unchanged -
Security Best Practices Using SemVer:
Action Benefit Use PATCH for all security fixes Easier to isolate and track in audits Avoid skipping MAJOR bumps Prevent breaking user experience Communicate changes clearly Reduces user confusion/trust issues Document deprecated features Helps manage risky transitions -
Automation Tools for SemVer5:
Tool Use Case standard-version Auto-bump version + changelog semantic-release Full CI-based release mgmt npm version patch Quick version bump
4. Monitoring – Code & Setup
Section titled “4. Monitoring – Code & Setup”-
Capture and Report Errors:
background.js
self.onerror = function (message, source, lineno, colno, error) {fetch("https://your-logging-endpoint.com/log", {method: "POST",headers: {"Content-Type": "application/json"},body: JSON.stringify({message,source,lineno,colno,stack: error ? error.stack : null})});};This logs any uncaught errors in your background service worker.
-
Log User Behavior (non-sensitive):
Example: Track Feature Usage:
function logFeatureUsage(featureName) {fetch("https://your-logging-endpoint.com/feature-usage", {method: "POST",body: JSON.stringify({ feature: featureName, time: Date.now() })});}// UsagelogFeatureUsage("clicked_sync_button");Ensure you’re not collecting PII (Personally Identifiable Information) without explicit user consent.
-
Monitor for Malicious Behavior (e.g., Extension Hijack):
Detect unexpected API usage or content injection:
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {if (changeInfo.status === 'complete') {chrome.scripting.executeScript({target: { tabId: tabId },func: () => {if (document.querySelector('iframe[src*="malicious-site.com"]')) {alert('Suspicious iframe detected.');}}});}}); -
Security Checklist (Code-Focused):
Task Code Reference Use Manifest V3 manifest.jsonRemove inline scripts Move to external .jsEnforce CSP "content_security_policy"Log errors securely fetcherror handlerSanitize inputs sanitizeInput()Test core logic Jest/Mocha unit tests CI/CD pipeline GitHub Actions YAML Patch dependencies npm audit fix
-
5. Validate and Sanitize Input
Section titled “5. Validate and Sanitize Input”- Why Input Sanitization & Validation Matter?
Chrome extensions often:
- Accept user input (e.g., popup forms, options pages)
- Inject content scripts into webpages
- Make API calls and handle responses
Without sanitization/validation:
- A user could inject '; const safeInput = sanitizeInput(userInput); // → <script>alert("xss")</script>">
Use textContent or text() instead of innerHTML:
// Unsafe (XSS risk): element.innerHTML = userInput;
// Safe: element.textContent = userInput;Whitelist allowed tags/attributes (advanced):
If you need rich input (e.g., , ), use a sanitizer:
import DOMPurify from 'dompurify';
const safeHTML = DOMPurify.sanitize(userInput); element.innerHTML = safeHTML;Sidenote: “DOMPurify” is a robust open-source sanitizer that neutralizes XSS payloads.
Input Validation (Ensuring Correct Format):
Validation checks data correctness — before using it in logic or sending to a server.
When to Validate?
- Before saving input to chrome.storage
- Before sending input via chrome.runtime.sendMessage
- Before calling any API
-
Example: Validate an Email Address:
function isValidEmail(email) {const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;return regex.test(email);}const email = "user@example.com";if (!isValidEmail(email)) {alert("Invalid email");} -
Example: Validate Numeric Input (range):
function isValidAge(age) {return Number.isInteger(age) && age >= 18 && age <= 120;} -
Use JSON Schema for Complex Validation:
For structured data (e.g., user settings), use a schema validator:
Schema (userSettingsSchema.json):
{"type": "object","properties": {"theme": { "type": "string", "enum": ["dark", "light"] },"emailNotifications": { "type": "boolean" }},"required": ["theme"]}Validator (using ajv):
bash:
npm install ajvjs:
import Ajv from 'ajv';import schema from './userSettingsSchema.json';const ajv = new Ajv();const validate = ajv.compile(schema);const userSettings = {theme: "dark",emailNotifications: true};if (!validate(userSettings)) {console.error(validate.errors);} -
Security Tips & Pitfalls:
Pitfall Fix / Best Practice Using innerHTMLwith user inputAlways use textContentor a sanitizerAccepting unvalidated JSON Use schema-based validation (e.g., AJV) Saving raw input to storage Sanitize + validate before saving Trusting input from content scriptsValidate all messages at the receiver Not encoding URLs Use encodeURIComponent()for query params -
Messaging Validation Example:
Chrome extensions often use messaging between components (popup, background, etc.). Validate all data:
background.js chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {if (message.type === 'SAVE_DATA' && typeof message.payload === 'string') {const clean = sanitizeInput(message.payload);chrome.storage.local.set({ data: clean });} else {sendResponse({ error: 'Invalid input' });}}); -
Recommended Libraries:
Purpose Library Notes Sanitization DOMPurifyDOM XSS protection Schema Validation AjvJSON Schema 7+ Type-safe validation Zod/YupJS-first, great for TS Regex checking Built-in Use for basic email, phone, etc. -
Example: Securely Handling a Form:
html:
<form id="feedback-form"><textarea id="feedback"></textarea><button type="submit">Send</button></form>js:
document.getElementById('feedback-form').addEventListener('submit', (e) => {e.preventDefault();const raw = document.getElementById('feedback').value;const sanitized = DOMPurify.sanitize(raw);if (sanitized.length < 10) {alert("Feedback too short");return;}chrome.storage.local.set({ feedback: sanitized });});
6. Secure Data Storage:
Section titled “6. Secure Data Storage:”-
Storage APIs in Chrome Extensions:
Main Options:
API Description Secure for sensitive data? chrome.storage.localLocal storage (persistent, per user) ✅ Yes (with caution) chrome.storage.syncSynced across devices via Google account ⚠️ Yes (not for secrets) localStorageFrom web pages or content scripts (DOM-based) ❌ No (not secure) IndexedDB Complex structured storage ✅ Yes (less common) Cookies Not recommended in extensions ❌ No Native messaging Send/receive from a local app — highly secure but complex ✅ Secure for advanced use -
Storing Data Securely:
-
chrome.storage.local – Default and Most Secure API:
// Save datachrome.storage.local.set({ theme: 'dark', token: 'abc123' });// Get datachrome.storage.local.get(['theme', 'token'], (result) => {console.log(result.theme);});Notes:
- Stored data is sandboxed to the extension
- Not accessible from web pages or content scripts directly
- Still visible in devtools (chrome-extension://…)
- Should not store secrets in plain text
-
-
Encrypting Sensitive Data:
Chrome storage doesn’t encrypt values natively. To store tokens, user data, or secrets, encrypt before storing.
Example: AES Encryption with crypto.subtle
js:
async function encryptData(key, data) {const enc = new TextEncoder().encode(data);const iv = crypto.getRandomValues(new Uint8Array(12));const encrypted = await crypto.subtle.encrypt({name: 'AES-GCM',iv: iv},key,enc);return {iv: Array.from(iv),data: Array.from(new Uint8Array(encrypted))};}async function generateKey() {return await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 },true,['encrypt', 'decrypt']);}Use crypto.subtle.importKey if you want to persist a key between sessions (you’ll need to save it securely as well).
Save Encrypted Value:
js:
const key = await generateKey(); // or load a saved keyconst encrypted = await encryptData(key, 'my_secret_token');// Save encrypted tokenchrome.storage.local.set({ token: JSON.stringify(encrypted) });You must handle key management carefully — consider deriving it from a password or hardcoding an initialization key with constraints (less ideal).
-
Validate Before Storage
Before saving any user data:
-
Validate and sanitize it
-
Avoid storing unnecessary information
Example:
js:
function isValidTheme(theme) {return ['light', 'dark'].includes(theme);}const theme = 'dark';if (isValidTheme(theme)) {chrome.storage.local.set({ theme });}
-
-
Things To Avoid:
Mistake Why It’s Bad Storing raw access tokens Leaked via devtools Using localStorageAccessible from content scripts Not sanitizing before saving Risk of DOM injection Using chrome.storage.syncfor tokensCould sync to other machines, insecure Hardcoding secrets into JS files Exposed to anyone with the CRX or devtools -
Best Practices Summary:
Practice Description Use chrome.storage.localBest for secure, local-only data Encrypt sensitive fields Use AES-GCM and crypto.subtleAPIValidate input before saving Prevent storing garbage, XSS risk Avoid localStorageentirelyNot safe in extensions Use key derivation for encryption Optional: derive keys from passphrases if needed Don’t expose secrets in popup UI Never populate secrets in DOM unnecessarily
Page provenance
Community-maintained guidance. Use the edit link below to propose a sourced correction.
Was this page useful?
Help us prioritize the next improvement.