| name | extension-security |
| description | Comprehensive security guide for browser extensions covering CSP, permissions, secure messaging, sandboxing, and threat mitigation |
| tags | ["browser-extension","security","csp","permissions","sandboxing"] |
Extension Security
Comprehensive security guide for browser extensions covering Content Security Policy, permissions model, secure messaging, sandboxing, storage security, and threat mitigation patterns.
Security Model Overview
Browser extensions operate with elevated privileges. Security failures can expose users to data theft, credential compromise, and malicious code execution.
┌─────────────────────────────────────────────────────────────┐
│ Extension Context │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Popup │ │ Options │ │ Service Worker │ │
│ │ (sandbox) │ │ (sandbox) │ │ (privileged) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
│ │ │ │ │
│ └────────────────┼────────────────────┘ │
│ │ │
│ ┌───────────▼───────────┐ │
│ │ Message Channel │ │
│ └───────────┬───────────┘ │
│ │ │
│ ┌───────────▼───────────┐ │
│ │ Content Scripts │ │
│ │ (isolated world) │ │
│ └───────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Content Security Policy (CSP)
Manifest V3 Default CSP
{
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'",
"sandbox": "sandbox allow-scripts allow-forms allow-popups allow-modals"
}
}
CSP Directives Reference
| Directive | Purpose | Recommended Value |
|---|
script-src | Script sources | 'self' only |
object-src | Plugin sources | 'self' or 'none' |
style-src | Stylesheet sources | 'self' |
img-src | Image sources | 'self' data: https: |
connect-src | XHR/fetch targets | Specific origins |
frame-src | iframe sources | 'self' or 'none' |
worker-src | Worker sources | 'self' |
Strict CSP Configuration
{
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self' data:; connect-src 'self' https://api.example.com; frame-src 'none'"
}
}
CSP Anti-Patterns
| Pattern | Risk | Alternative |
|---|
'unsafe-eval' | Code injection | Static code, no eval() |
'unsafe-inline' | XSS | External scripts only |
* wildcard | Unrestricted | Specific domains |
data: for scripts | Code injection | Bundle scripts |
blob: for scripts | Code injection | Static imports |
Dynamic Script Execution
Never do this:
eval(userInput);
const fn = new Function('return ' + userInput);
element.innerHTML = userContent;
Do this instead:
import { processData } from './processor';
element.textContent = userContent;
const data = structuredClone(userInput);
Permissions Model
Permission Types
| Type | Declaration | User Prompt | Best For |
|---|
| Required | permissions | Install time | Core functionality |
| Optional | optional_permissions | Runtime | Feature gates |
| Host | host_permissions | Install/runtime | Site access |
| Optional host | optional_host_permissions | Runtime | User-selected sites |
Minimal Permissions Design
{
"permissions": [
"storage"
],
"optional_permissions": [
"tabs",
"bookmarks"
],
"host_permissions": [],
"optional_host_permissions": [
"https://*.example.com/*"
]
}
Requesting Optional Permissions
async function enableAdvancedFeature(): Promise<boolean> {
const granted = await browser.permissions.request({
permissions: ['tabs'],
origins: ['https://api.example.com/*']
});
if (granted) {
await initializeAdvancedFeature();
}
return granted;
}
async function requiresPermission(): Promise<void> {
const hasPermission = await browser.permissions.contains({
permissions: ['tabs']
});
if (!hasPermission) {
throw new Error('Feature requires tabs permission');
}
}
Permission Escalation Prevention
type RequiredPermission = 'storage' | 'alarms';
type OptionalPermission = 'tabs' | 'bookmarks';
async function checkPermissions(
required: RequiredPermission[]
): Promise<boolean> {
return browser.permissions.contains({ permissions: required });
}
Secure Message Passing
Message Types
interface Messages {
GET_DATA: { key: string };
SET_DATA: { key: string; value: unknown };
FETCH_URL: { url: string; options?: RequestInit };
}
type MessageType = keyof Messages;
interface Message<T extends MessageType> {
type: T;
payload: Messages[T];
nonce: string;
}
Validated Message Handler
browser.runtime.onMessage.addListener(
(message: unknown, sender, sendResponse) => {
if (!isValidSender(sender)) {
console.warn('Invalid sender:', sender);
return false;
}
if (!isValidMessage(message)) {
console.warn('Invalid message:', message);
return false;
}
handleMessage(message, sender).then(sendResponse);
return true;
}
);
function isValidSender(sender: browser.Runtime.MessageSender): boolean {
if (sender.id !== browser.runtime.id) return false;
if (sender.url) {
try {
const url = (sender.);
(!.(url.)) ;
} {
;
}
}
;
}
(): msg is <> {
(!msg || msg !== ) ;
m = msg <, >;
( m. !== ) ;
(!.(m. )) ;
( m. !== || m.. !== ) ;
;
}
Content Script to Background
async function sendToBackground<T extends MessageType>(
type: T,
payload: Messages[T]
): Promise<unknown> {
const message: Message<T> = {
type,
payload,
nonce: crypto.randomUUID().replace(/-/g, '')
};
try {
return await browser.runtime.sendMessage(message);
} catch (error) {
console.error('Message failed:', error);
throw error;
}
}
const data = await sendToBackground('GET_DATA', { key: 'settings' });
External Message Security
{
"externally_connectable": {
"matches": [
"https://app.example.com/*",
"https://dashboard.example.com/*"
]
}
}
browser.runtime.onMessageExternal.addListener(
(message, sender, sendResponse) => {
if (!sender.url || !ALLOWED_EXTERNAL_ORIGINS.includes(new URL(sender.url).origin)) {
return false;
}
if (!EXTERNAL_ALLOWED_ACTIONS.includes(message.action)) {
return false;
}
handleExternalMessage(message, sender).then(sendResponse);
return true;
}
);
Port-Based Communication
const ports = new Map<string, browser.Runtime.Port>();
browser.runtime.onConnect.addListener((port) => {
if (!/^content-\d+$/.test(port.name)) {
port.disconnect();
return;
}
ports.set(port.name, port);
port.onMessage.addListener((message) => {
if (isValidPortMessage(message)) {
handlePortMessage(port, message);
}
});
port.onDisconnect.addListener(() => {
ports.delete(port.name);
});
});
Sandboxing and Isolation
Content Script Isolation
Content scripts run in an isolated world but share DOM:
Protecting Against Page Manipulation
function safeGetAttribute(element: Element, attr: string): string | null {
return Element.prototype.getAttribute.call(element, attr);
}
function safeSetAttribute(element: Element, attr: string, value: string): void {
Element.prototype.setAttribute.call(element, attr, value);
}
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (!isOurElement(mutation.target)) continue;
handleMutation(mutation);
}
});
Sandbox for Untrusted Content
{
"sandbox": {
"pages": ["sandbox.html"]
}
}
const sandbox = document.getElementById('sandbox') as HTMLIFrameElement;
sandbox.contentWindow?.postMessage({
type: 'PROCESS',
data: untrustedData
}, '*');
window.addEventListener('message', (event) => {
if (event.source !== sandbox.contentWindow) return;
if (event.data.type === 'RESULT') {
handleResult(event.data.result);
}
});
Web Accessible Resources Security
{
"web_accessible_resources": [
{
"resources": ["injected.js"],
"matches": ["https://specific-site.com/*"],
"use_dynamic_url": true
}
]
}
Security considerations:
- Only expose necessary resources
- Use
use_dynamic_url: true for fingerprinting protection
- Limit
matches to specific sites
- Avoid exposing sensitive scripts
Storage Security
Storage Types and Security
| Type | Encryption | Sync | Quota | Best For |
|---|
storage.local | None | No | 10MB | Large data, local-only |
storage.sync | Transit only | Yes | 100KB | Settings, cross-device |
storage.session | Memory only | No | 10MB | Temporary, sensitive |
Encrypting Sensitive Data
class SecureStorage {
private key: CryptoKey | null = null;
async init(): Promise<void> {
const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(browser.runtime.id),
'PBKDF2',
false,
['deriveKey']
);
this.key = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: encoder.encode('extension-salt'),
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
async setSecure(: , : ): <> {
(!.) ();
encoder = ();
iv = crypto.( ());
data = encoder.(.(value));
encrypted = crypto..(
{ : , iv },
.,
data
);
browser...({
[key]: {
: .(iv),
: .( (encrypted))
}
});
}
getSecure<T>(: ): <T | > {
(!.) ();
result = browser...(key);
(!result[key]) ;
{ iv, data } = result[key];
decrypted = crypto..(
{ : , : (iv) },
.,
(data)
);
decoder = ();
.(decoder.(decrypted));
}
}
Session Storage for Sensitive Data
async function storeTemporaryCredentials(creds: Credentials): Promise<void> {
await browser.storage.session.set({
credentials: creds,
timestamp: Date.now()
});
}
async function getTemporaryCredentials(): Promise<Credentials | null> {
const result = await browser.storage.session.get(['credentials', 'timestamp']);
if (!result.credentials) return null;
if (Date.now() - result.timestamp > 3600000) {
await browser.storage.session.remove(['credentials', 'timestamp']);
return ;
}
result.;
}
Input Validation
URL Validation
const ALLOWED_PROTOCOLS = ['https:', 'http:'];
const ALLOWED_HOSTS = ['api.example.com', 'cdn.example.com'];
function validateUrl(input: string): URL | null {
try {
const url = new URL(input);
if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {
console.warn('Invalid protocol:', url.protocol);
return null;
}
if (!ALLOWED_HOSTS.includes(url.hostname)) {
console.warn('Invalid host:', url.hostname);
return null;
}
url.username = '';
url.password = '';
return url;
} catch {
console.warn('Invalid URL:', input);
return ;
}
}
JSON Schema Validation
import Ajv from 'ajv';
const ajv = new Ajv({ allErrors: true });
const settingsSchema = {
type: 'object',
properties: {
theme: { type: 'string', enum: ['light', 'dark'] },
fontSize: { type: 'number', minimum: 8, maximum: 32 },
notifications: { type: 'boolean' }
},
required: ['theme'],
additionalProperties: false
};
const validateSettings = ajv.compile(settingsSchema);
function parseSettings(input: unknown): Settings | null {
if (validateSettings(input)) {
return input as Settings;
}
console.warn('Invalid settings:', validateSettings.errors);
return null;
}
HTML Sanitization
function sanitizeHtml(input: string): string {
const div = document.createElement('div');
div.textContent = input;
return div.innerHTML;
}
import DOMPurify from 'dompurify';
const ALLOWED_TAGS = ['b', 'i', 'em', 'strong', 'a', 'p', 'br'];
const ALLOWED_ATTR = ['href'];
function sanitizeRichText(input: string): string {
return DOMPurify.sanitize(input, {
ALLOWED_TAGS,
ALLOWED_ATTR,
ALLOW_DATA_ATTR: false
});
}
Network Security
Fetch with Validation
interface FetchOptions {
url: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
body?: unknown;
timeout?: number;
}
async function secureFetch<T>(options: FetchOptions): Promise<T> {
const validatedUrl = validateUrl(options.url);
if (!validatedUrl) {
throw new Error('Invalid URL');
}
const controller = new AbortController();
const timeoutId = setTimeout(
() => controller.abort(),
options.timeout ?? 30000
);
try {
const response = await fetch(validatedUrl.toString(), {
method: options.method ?? 'GET',
headers: {
'Content-Type': 'application/json',
'credentials': 'same-origin'
},
body: options. ? .(options.) : ,
: controller.
});
(!response.) {
();
}
contentType = response..();
(!contentType?.()) {
();
}
response.();
} {
(timeoutId);
}
}
CORS and Credentials
async function fetchWithCors(url: string): Promise<Response> {
return fetch(url, {
mode: 'cors',
credentials: 'omit',
referrerPolicy: 'no-referrer'
});
}
Threat Mitigation
Cross-Site Scripting (XSS)
| Attack Vector | Mitigation |
|---|
| innerHTML | Use textContent or sanitize |
| eval() | Disallow via CSP |
| URL parameters | Validate and sanitize |
| postMessage | Verify origin |
| DOM clobbering | Use unique IDs |
Code Injection
| Attack Vector | Mitigation |
|---|
| eval() | Static code only |
| new Function() | Pre-compiled functions |
| setTimeout(string) | Use function reference |
| script injection | CSP script-src 'self' |
Data Exfiltration
| Attack Vector | Mitigation |
|---|
| Unvalidated fetch | URL allowlist |
| Image beacons | CSP img-src |
| DNS prefetch | CSP prefetch-src |
| Form action | CSP form-action |
Clickjacking
if (window !== window.top) {
document.body.innerHTML = '';
throw new Error('Framing not allowed');
}
Security Audit Checklist
Manifest Review
Code Review
Network Security
Storage Security
Related Resources
- extension-anti-patterns skill: Common security mistakes to avoid
- validate-extension command: Automated security validation
- store-submission-reviewer agent: Pre-submission security checks