Chrome Extension Security Best Practices — Developer Guide

13 min read

Security Best Practices for Chrome Extensions

Introduction

1. Principle of Least Privilege {#1-principle-of-least-privilege}

2. Content Security Policy (CSP) {#2-content-security-policy-csp}

3. Secure Messaging {#3-secure-messaging}

4. Storage Security {#4-storage-security}

5. Content Script Safety {#5-content-script-safety}

6. XSS Prevention in Extension Pages {#6-xss-prevention-in-extension-pages}

7. Network Request Security {#7-network-request-security}

8. Update and Supply Chain Security {#8-update-and-supply-chain-security}

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

16. Security Review Checklist

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.

17. Common Vulnerabilities

  1. XSS: Injecting untrusted content - Use textContent, sanitize HTML
  2. Message Injection: Forged messages - Validate origins and structure
  3. Privilege Escalation: Excessive permissions - Use least privilege
  4. Insecure Storage: Sensitive data exposed - Use chrome.storage.local
  5. 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

Preparing for Review

  1. Document all permission justifications
  2. Update privacy policy with data handling
  3. Test thoroughly before submission
  4. Fix all security vulnerabilities
  5. 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

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.