Chrome Extension Permissions: A Deep Dive — Developer Guide
23 min readChrome 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:
- The user clicks the extension’s action button
- The user invokes a keyboard shortcut assigned to the extension
- The user selects a context menu item from the extension
- 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:
- Page analyzers and inspectors
- Note-taking extensions that capture page content
- Highlighters and annotation tools
- Page-specific tools (format converters, calculators)
- Any extension that operates on-demand rather than continuously
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:
storage- Data persistencealarms- Scheduled taskscontextMenus- Right-click menu
Tab Management:
activeTab- On-demand tab accesstabs- Full tab informationwindowManagement- Window control
Content Access:
scripting- Script injectiondeclarativeContent- Content-based rulescontentSettings- Site-specific settings
Data Access:
bookmarks- Bookmark managementdownloads- Download controlhistory- Browsing historytopSites- Frequently visited sites
User Features:
notifications- System notificationsgeolocation- Location accessidentity- OAuth authenticationmanagement- Extension management
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:
- Prefer optional permissions - Request permissions only when users need specific features
- Use activeTab - For on-demand page access without install warnings
- Use declarativeContent - For content-based activation without active page reading
- Request specific hosts - Never use
<all_urls>unless absolutely necessary - Check before use - Always verify permissions before API calls
- Handle revocation - Gracefully degrade when users remove permissions
- 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.
Related Articles
- Permissions Model - Comprehensive guide to the complete permissions model
- Security Best Practices - Security patterns for extension development
- Declarative Content API - Deep dive into declarative content rules
- Scripting API - Programmatic script injection techniques
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.