Chrome Extension Permissions: A Deep Dive — Developer Guide

23 min read

Chrome Extension Permissions: A Deep Dive

Permissions are the cornerstone of Chrome extension security. They determine what data your extension can access, what actions it can perform, and most importantly, how much trust users place in your extension. This deep dive covers every aspect of the permissions system in Manifest V3, from basic concepts to advanced security patterns.

Understanding Permission Categories

Chrome extensions have three distinct permission categories, each with different security implications and user experience considerations. Understanding these categories is essential for building secure, trustworthy extensions.

Required Permissions

Required permissions are declared in the manifest.json file under the permissions key. These permissions are presented to users during installation and cannot be granted after the fact without explicit user action. The key principle is that required permissions should only include those absolutely necessary for the extension’s core functionality.

{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0",
  "permissions": [
    "storage",
    "activeTab"
  ],
  "host_permissions": [
    "https://api.myservice.com/*"
  ]
}

Common required permissions include storage for persistent data, activeTab for on-demand page access, and specific API permissions needed for core features. Host permissions like website access are also declared in host_permissions in MV3.

The critical decision is determining whether a permission is truly required for core functionality. If a permission only enables enhanced features that users explicitly invoke, it should be declared as optional instead.

Optional Permissions

Optional permissions provide a more granular approach to permission management. These are declared in the optional_permissions field and must be explicitly requested at runtime using the Chrome Permissions API. This approach significantly improves user trust and installation rates.

{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0",
  "permissions": [
    "storage"
  ],
  "optional_permissions": [
    "bookmarks",
    "history",
    "notifications",
    "geolocation"
  ]
}

Optional permissions offer several strategic advantages. Users can install the extension with minimal initial permissions, building trust before granting additional access. When a permission is requested contextually (when the user actually needs the feature), the user better understands why the permission is necessary. Additionally, extensions can gracefully degrade functionality when permissions are denied rather than failing completely.

Host Permissions

Host permissions control an extension’s access to websites and web resources. They are declared separately in MV3 using the host_permissions key and represent one of the most sensitive permission categories because they determine what data the extension can read and modify on web pages.

{
  "host_permissions": [
    "https://*.google.com/*",
    "https://api.myservice.com/v1/*"
  ]
}

Match patterns provide precise control over URL access. The basic format is <scheme>://<host><path>, with wildcards providing flexibility:

Pattern Matches
*://*/* All HTTP and HTTPS URLs
https://*/* All HTTPS URLs
https://*.example.com/* All HTTPS pages on example.com and subdomains
https://example.com/* Only example.com (not subdomains)
https://example.com/api/* Only API endpoints
<all_urls> All URLs including file:// and ftp://

Host permissions can also be optional, allowing users to grant access to specific sites they choose rather than having the extension access everything by default.

The activeTab Permission

The activeTab permission is one of the most valuable security features in Manifest V3. It grants temporary access to the active tab only when the user explicitly invokes the extension, dramatically reducing the extension’s attack surface.

How activeTab Works

When activeTab is declared, the extension has zero tab access by default. Access is granted only in specific user-triggered scenarios:

  1. The user clicks the extension’s action button
  2. The user invokes a keyboard shortcut assigned to the extension
  3. The user selects a context menu item from the extension
  4. The user accepts an omnibox suggestion from the extension
{
  "permissions": ["activeTab"],
  "action": {
    "default_title": "Analyze Page"
  }
}

This is dramatically more secure than the tabs permission, which provides access to all tabs at all times.

Using activeTab in Practice

// This function only works when activeTab is granted via user gesture
async function getActiveTabContent(): Promise<string | null> {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  
  if (!tab.id || !tab.url) {
    return null;
  }
  
  // With activeTab, this script injection is only allowed when 
  // the extension was activated by user gesture
  const results = await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: () => document.body.innerText
  });
  
  return results[0]?.result || null;
}

// Also works with declarative content scripts
async function injectAnalysisTools(): Promise<void> {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  
  if (!tab.id) return;
  
  await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    files: ['analyzer.js']
  });
}

When to Use activeTab

The activeTab permission is ideal for:

The main limitation is that you cannot monitor tab updates or changes automatically. For background monitoring, you’ll need standard permissions.

The declarativeContent API

The declarativeContent API provides a powerful way to take actions based on page content without requiring broad permissions. It allows extensions to show their action (icon in toolbar) when specific conditions are met, without actively reading page content in the background.

How declarativeContent Works

Instead of having a background script that constantly monitors all pages, you define rules that trigger when specific conditions are matched:

// background.js
chrome.declarativeContent.onPageChanged.addRules([
  {
    conditions: [
      new chrome.declarativeContent.PageStateMatcher({
        pageUrl: { hostSuffix: 'example.com' }
      }),
      new chrome.declarativeContent.PageStateMatcher({
        css: ['input[type="password"]']
      })
    ],
    actions: [
      new chrome.declarativeContent.ShowAction()
    ]
  }
]);

This rule shows the extension’s action only when the user visits a page on example.com that contains a password input field.

Declarative Content Conditions

The PageStateMatcher supports multiple condition types:

// Match specific hosts
new chrome.declarativeContent.PageStateMatcher({
  pageUrl: { hostSuffix: 'example.com' }
});

// Match URL patterns
new chrome.declarativeContent.PageStateMatcher({
  pageUrl: { urlMatches: '\\.pdf$' }
});

// Match CSS selectors (element must exist on page)
new chrome.declarativeContent.PageStateMatcher({
  css: ['#search-input', '.product-card']
});

// Match content (requires host permission)
new chrome.declarativeContent.PageStateMatcher({
  contentContains: 'password'
});

Combining Conditions

Multiple conditions can be combined using the and logic:

chrome.declarativeContent.onPageChanged.addRules([
  {
    conditions: [
      // Must be on example.com AND have a form
      new chrome.declarativeContent.PageStateMatcher({
        pageUrl: { hostSuffix: 'example.com' }
      }),
      new chrome.declarativeContent.PageStateMatcher({
        css: ['form[action*="login"]']
      })
    ],
    actions: [
      new chrome.declarativeContent.ShowAction(),
      new chrome.declarativeContent.SetIcon({
        path: 'icons/active-48.png'
      })
    ]
  }
]);

Requesting declarativeContent Permission

Add declarativeContent to your manifest:

{
  "permissions": [
    "declarativeContent",
    "activeTab"
  ],
  "background": {
    "service_worker": "background.js"
  }
}

Note that declarativeContent does not trigger an install warning, making it an excellent alternative to content script injection with <all_urls>.

Permission Warnings and User Trust

Permission warnings are displayed to users during installation and represent the primary mechanism for informed consent. Understanding what triggers warnings and how to minimize them is crucial for user trust and installation rates.

Common Permission Warnings

Certain permissions trigger prominent warnings because they provide broad access to user data:

Permission Warning Message
history “Read and change your browsing history on all signed-in devices”
tabs “Read your browsing history”
bookmarks “Read and change your bookmarks”
<all_urls> “Read and change all your data on all websites”
cookies “Read and change your cookies and site data”
webRequest “Intercept, block, or modify your requests”

Some permissions like storage, alarms, and contextMenus do not trigger install-time warnings.

Warning Impact on Installation

Research consistently shows that permission warnings significantly impact installation rates. Extensions with fewer and less severe warnings have substantially higher installation conversion rates.

The severity of warnings depends on the combination of permissions requested. A single sensitive permission might be acceptable, but multiple warnings create a compounding effect that drastically reduces user trust.

Minimizing Warning Impact

Strategies to minimize permission warnings:

// ❌ Bad: Too broad - triggers severe warnings
{
  "permissions": ["tabs", "history", "bookmarks"],
  "host_permissions": ["<all_urls>"]
}

// ✅ Good: Use activeTab for on-demand access
{
  "permissions": ["activeTab"]
}

// ✅ Good: Request specific host permissions only
{
  "host_permissions": ["https://specific-site.com/*"]
}

// ✅ Good: Use optional permissions for sensitive features
{
  "permissions": ["storage"],
  "optional_permissions": ["history", "bookmarks"]
}

Requesting Permissions at Runtime

The Chrome Permissions API enables extensions to request optional permissions at runtime. This is critical for implementing the principle of least privilege.

Checking Before Requesting

Always check if a permission is already granted before requesting it:

async function requestNotificationPermission(): Promise<boolean> {
  // Check if we already have the permission
  const hasPermission = await chrome.permissions.contains({
    permissions: ['notifications']
  });
  
  if (hasPermission) {
    return true;
  }
  
  // Request the permission
  const granted = await chrome.permissions.request({
    permissions: ['notifications']
  });
  
  if (granted) {
    console.log('Notification permission granted');
  } else {
    console.log('Notification permission denied');
  }
  
  return granted;
}

Requesting Multiple Permissions

Chrome shows ONE prompt for all permissions requested together:

async function requestAdvancedFeatures(): Promise<boolean> {
  const result = await chrome.permissions.request({
    permissions: ['bookmarks', 'history'],
    origins: ['https://example.com/*']
  });
  
  return result;
}

Handling Permission Denials

When users deny permission requests, provide graceful degradation:

class FeatureManager {
  async enableBookmarksFeature(): Promise<boolean> {
    const hasPermission = await chrome.permissions.contains({
      permissions: ['bookmarks']
    });
    
    if (hasPermission) {
      return true;
    }
    
    const granted = await chrome.permissions.request({
      permissions: ['bookmarks']
    });
    
    if (!granted) {
      // Show user-friendly message
      this.showPermissionDeniedMessage(
        'Bookmarks',
        'Enable bookmarks to save your favorite pages'
      );
      return false;
    }
    
    return true;
  }
  
  private showPermissionDeniedMessage(
    featureName: string, 
    explanation: string
  ): void {
    // Update UI to show the feature is unavailable
    console.log(`${featureName} feature requires permission: ${explanation}`);
  }
}

Listening for Permission Changes

Users can revoke permissions at any time through chrome://extensions. Listen for these changes:

// Listen for permission removals
chrome.permissions.onRemoved.addListener((permissions) => {
  console.log('Permissions removed:', permissions.permissions);
  console.log('Origins removed:', permissions.origins);
  
  // Update UI to reflect lost functionality
  if (permissions.permissions.includes('bookmarks')) {
    this.disableBookmarksFeature();
  }
});

// Listen for permission grants
chrome.permissions.onAdded.addListener((permissions) => {
  console.log('Permissions added:', permissions.permissions);
  console.log('Origins added:', permissions.origins);
  
  // Enable new features
  if (permissions.permissions.includes('notifications')) {
    this.enableNotificationFeatures();
  }
});

Minimum Viable Permissions Strategy

The principle of minimum privilege dictates that an extension should request only the permissions it absolutely needs to function. This section covers practical strategies for implementing this principle.

Start Minimal, Expand as Needed

Design your extension to work with minimal permissions by default, then request additional permissions as users access features:

{
  "permissions": [
    "storage",
    "activeTab"
  ],
  "optional_permissions": [
    "bookmarks",
    "history",
    "notifications",
    "geolocation",
    "https://*/*"
  ]
}

Feature-Based Permission Gating

Implement feature gates that request permissions only when needed:

class FeatureGate {
  private static permissionMap: Record<string, string> = {
    'bookmarks': 'Access your bookmarks',
    'history': 'Search your browsing history',
    'notifications': 'Send you notifications',
    'geolocation': 'Provide location-based features'
  };
  
  static async checkFeature(feature: string): Promise<boolean> {
    const permission = this.getFeaturePermission(feature);
    if (!permission) return true;
    
    return await chrome.permissions.contains({
      permissions: [permission]
    });
  }
  
  static async requestFeature(feature: string): Promise<boolean> {
    const permission = this.getFeaturePermission(feature);
    if (!permission) return true;
    
    // Show user why we need this permission
    const rationale = this.permissionMap[permission];
    
    // Request the permission
    return await chrome.permissions.request({
      permissions: [permission]
    });
  }
  
  private static getFeaturePermission(feature: string): string | null {
    const map: Record<string, string> = {
      'bookmarks': 'bookmarks',
      'history': 'history',
      'notifications': 'notifications',
      'location': 'geolocation'
    };
    return map[feature] || null;
  }
}

// Usage
async function onBookmarksButtonClick(): Promise<void> {
  if (await FeatureGate.checkFeature('bookmarks')) {
    showBookmarksUI();
  } else if (await FeatureGate.requestFeature('bookmarks')) {
    showBookmarksUI();
  }
}

Progressive Enhancement Pattern

Design your extension to work with reduced functionality when permissions are denied:

class ProgressiveExtension {
  private capabilities: Set<string> = new Set();
  
  async initialize(): Promise<void> {
    // Check what permissions we have
    const allPermissions = await chrome.permissions.getAll();
    
    // Determine available capabilities
    if (allPermissions.permissions.includes('storage')) {
      this.capabilities.add('persistence');
    }
    if (allPermissions.permissions.includes('bookmarks')) {
      this.capabilities.add('bookmarks');
    }
    if (allPermissions.permissions.includes('history')) {
      this.capabilities.add('history');
    }
    if (allPermissions.origins.length > 0) {
      this.capabilities.add('webAccess');
    }
    
    // Enable features based on available capabilities
    this.configureFeatures();
  }
  
  private configureFeatures(): void {
    // Base features always available
    this.enableBaseFeatures();
    
    // Enhanced features based on permissions
    if (this.capabilities.has('bookmarks')) {
      this.enableBookmarksFeature();
    }
    if (this.capabilities.has('history')) {
      this.enableHistoryFeature();
    }
    if (this.capabilities.has('webAccess')) {
      this.enableWebFeatures();
    }
  }
  
  private enableBaseFeatures(): void {
    // Core functionality that works without special permissions
    console.log('Base features enabled');
  }
  
  private enableBookmarksFeature(): void {
    console.log('Bookmarks feature enabled');
  }
  
  private enableHistoryFeature(): void {
    console.log('History feature enabled');
  }
  
  private enableWebFeatures(): void {
    console.log('Web access features enabled');
  }
}

Permission Groups and Organization

Understanding how permissions relate to each other helps in designing better permission strategies for your extension.

Permission Impact Table

Category Permission Install Warning Runtime Request Sensitive
Storage storage No No Low
Storage unlimitedStorage No No Low
Tabs activeTab No No Low
Tabs tabs Yes No High
Tabs tabCapture Yes No High
Content scripting Conditional No Medium
Content declarativeContent No No Low
Data bookmarks Yes Yes Medium
Data history Yes Yes High
Data cookies Yes Yes High
Network webRequest Yes No High
Network declarativeNetRequest No No Low
Host <all_urls> Yes Yes High
Host host_specific Yes Yes Medium

Logical Permission Groups

Permissions can be grouped by their functional area:

Core Operations:

Tab Management:

Content Access:

Data Access:

User Features:

Manifest V2 vs V3 Permissions

MV3 changed how permissions are organized:

MV2 MV3
permissions: ["*://*/*"] host_permissions: ["*://*/*"]
Implicit host permissions Explicit host_permissions
Background pages Service workers
webRequest blocking declarativeNetRequest
{
  // MV2
  "permissions": ["tabs", "http://*/*", "https://*/*"]
}

// MV3
{
  "permissions": ["tabs"],
  "host_permissions": ["http://*/*", "https://*/*"]
}

Security Best Practices

Always Verify Permissions

Never assume permissions exist—verify before use:

async function secureFetch(url: string): Promise<string> {
  const urlObj = new URL(url);
  
  // Verify host permission
  const hasPermission = await chrome.permissions.contains({
    origins: [`${urlObj.protocol}//${urlObj.host}/*`]
  });
  
  if (!hasPermission) {
    throw new Error(`No permission to fetch from ${url}`);
  }
  
  return fetch(url).then(r => r.text());
}

Programmatic Injection Over Manifest Scripts

Prefer programmatic script injection over manifest-declared content scripts:

// ❌ Bad: Declares content script for all URLs in manifest
{
  "content_scripts": [{
    "matches": ["<all_urls>"],
    "js": ["content.js"]
  }]
}

// ✅ Good: Programmatic injection with specific permissions
async function injectWhenNeeded(tabId: number, url: string): Promise<void> {
  // Verify we have permission for this URL
  const hasPermission = await chrome.permissions.contains({
    origins: [new URL(url).origin + '/*']
  });
  
  if (!hasPermission) {
    return;
  }
  
  await chrome.scripting.executeScript({
    target: { tabId },
    files: ['content.js']
  });
}

Handle Permission Revocation

Always handle the case where users revoke permissions:

chrome.permissions.onRemoved.addListener(async (removed) => {
  // Disable features that lost permissions
  for (const perm of removed.permissions) {
    switch (perm) {
      case 'notifications':
        disableNotifications();
        break;
      case 'bookmarks':
        disableBookmarksFeature();
        break;
      case 'history':
        disableHistoryFeature();
        break;
    }
  }
  
  // Update UI
  updateCapabilityUI();
});

Summary

Mastering Chrome extension permissions is essential for building secure, trustworthy extensions. Key principles to remember:

  1. Prefer optional permissions - Request permissions only when users need specific features
  2. Use activeTab - For on-demand page access without install warnings
  3. Use declarativeContent - For content-based activation without active page reading
  4. Request specific hosts - Never use <all_urls> unless absolutely necessary
  5. Check before use - Always verify permissions before API calls
  6. Handle revocation - Gracefully degrade when users remove permissions
  7. Explain to users - Provide clear rationale for permission requests

By following these patterns, you can build extensions that respect user privacy, maintain security, and provide excellent user experience.


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

No previous article
No next article