Chrome Extension eval() Alternatives for Manifest V3: Complete Guide

24 min read

Chrome Extension eval() Alternatives for Manifest V3: Complete Guide

Chrome Extension eval() Alternatives for Manifest V3: Complete Guide

The transition from Manifest V2 to Manifest V3 has fundamentally changed how Chrome extension developers approach dynamic code execution. If you have ever relied on eval() or similar methods to execute dynamic code in your extension, you have likely encountered significant challenges during migration. This comprehensive guide explores the alternatives to eval() that work within Manifest V3’s security constraints, providing practical solutions for developers who need flexible code execution capabilities.

Understanding why Google restricted eval() in Manifest V3 is crucial for appreciating the alternative approaches. The Chrome extension platform evolved to prioritize security, privacy, and performance. The eval() function, while powerful, presented substantial risks including code injection vulnerabilities, difficult-to-audit behavior, and potential for malicious extensions to execute arbitrary code. These concerns led Google to restrict or disable eval() in the extension sandbox environment.

This guide covers everything from understanding the restrictions to implementing modern alternatives that maintain functionality while adhering to Manifest V3 requirements. Whether you are migrating an existing extension or building a new one, you will find practical code examples and architecture patterns that work with the latest Chrome extension platform.


Understanding Manifest V3 Restrictions on Code Execution

Manifest V3 introduced significant changes to how extensions can execute code, particularly in content scripts and background service workers. The most notable restriction involves the removal of remote code execution, meaning extensions can no longer load and execute code from external URLs. This change was designed to prevent malicious extensions from downloading and running untrusted code after installation.

The eval() function specifically faces several obstacles in Manifest V3. In content scripts, eval() runs in the context of the web page rather than the extension, creating security vulnerabilities and inconsistent behavior. The Chrome extension documentation explicitly advises against using eval() in content scripts, recommending safer alternatives like chrome.scripting.executeScript().

Background service workers in Manifest V3 also have limited access to dynamic code execution. While you can still use eval() in the background context, doing so is strongly discouraged and may trigger warnings during the Chrome Web Store review process. The recommended approach involves including all necessary code in the extension bundle and avoiding runtime code generation.

Why eval() Was Restricted

The decision to restrict eval() and similar dynamic code execution methods stems from multiple security concerns. First, eval() makes security auditing extremely difficult because the code being executed is determined at runtime rather than during installation. This opacity allows malicious extensions to hide malicious behavior from reviewers.

Second, eval() creates vulnerabilities in content scripts where the evaluated code runs in the web page’s context rather than the extension’s isolated world. This means the evaluated code has access to the page’s DOM and can potentially interact with the extension’s background context through message passing in unexpected ways.

Third, dynamic code execution complicates Chrome’s extension sandboxing model. The platform aims to provide strong isolation between extensions and web content, but eval() can potentially leak information across these boundaries. By restricting dynamic execution, Chrome maintains a more predictable and auditable security model.


Alternative 1: chrome.scripting.executeScript()

The primary recommended alternative to eval() for content scripts is chrome.scripting.executeScript(). This API provides a secure and declarative way to inject code into web pages while maintaining clear separation between extension code and web page code.

Basic Implementation

The chrome.scripting.executeScript() method allows you to inject JavaScript files or inline code into target pages. Here is a practical example demonstrating how to replace eval()-based code execution with this safer alternative:

// Manifest V3 - background service worker
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'executeDynamicLogic') {
    // Define your logic as a function
    const dynamicFunction = new Function('data', `
      return data.value * 2 + Math.random();
    `);
    
    // Execute in the context of the specified tab
    chrome.scripting.executeScript({
      target: { tabId: sender.tab.id },
      func: (inputData) => {
        // This code runs in the page context
        const result = inputData.value * 2;
        return { success: true, result };
      },
      args: [{ value: message.data }]
    }).then(results => {
      sendResponse({ success: true, result: results[0].result });
    }).catch(error => {
      sendResponse({ success: false, error: error.message });
    });
    
    return true; // Indicates async response
  }
});

This approach differs fundamentally from eval() because the code being injected is either a function reference or a string defined in your extension bundle. Chrome can audit this code during the review process, and users can inspect what your extension does before installing.

Passing Data Between Contexts

One common use case for eval() was executing code that referenced data available only at runtime. The chrome.scripting API handles this through the args parameter, which allows you to pass serializable data into the injected function:

chrome.scripting.executeScript({
  target: { tabId: tabId },
  func: (userConfig, apiEndpoints) => {
    // Access the passed data directly
    console.log('User config:', userConfig);
    
    // Initialize extension-provided functionality
    window.extensionAPI = {
      fetchData: (endpoint) => fetch(apiEndpoints[endpoint])
    };
    
    // Your main logic here
    return { initialized: true };
  },
  args: [
    { theme: 'dark', language: 'en' },
    { users: '/api/users', posts: '/api/posts' }
  ]
});

The args parameter accepts only JSON-serializable values, which actually improves security by preventing injection of potentially dangerous objects like DOM nodes or extension-specific APIs.


Alternative 2: Dynamic Function Constructor with Sandboxing

For scenarios requiring more flexibility than chrome.scripting.executeScript() provides, the Function constructor offers a middle ground between full eval() access and completely static code. While not as restricted as eval(), using the Function constructor with proper precautions provides a controlled way to execute dynamic logic.

Safe Dynamic Function Pattern

The Function constructor creates functions from string arguments, similar to eval() but with slightly different scoping behavior. When combined with proper input validation and sandboxing, it provides a manageable approach to dynamic code execution:

class SafeCodeExecutor {
  constructor(allowedGlobals = {}) {
    this.allowedGlobals = allowedGlobals;
    this.timeout = 5000;
  }

  execute(code, context = {}) {
    return new Promise((resolve, reject) => {
      // Validate inputs
      if (typeof code !== 'string') {
        return reject(new TypeError('Code must be a string'));
      }

      // Create timeout guard
      const timeoutId = setTimeout(() => {
        reject(new Error('Code execution timeout'));
      }, this.timeout);

      try {
        // Build safe function with restricted scope
        const safeGlobals = Object.keys(this.allowedGlobals);
        const globalValues = Object.values(this.allowedGlobals);
        
        // Create function with explicit parameter names
        const paramNames = [...safeGlobals, 'context'];
        const functionBody = `
          "use strict";
          ${code}
        `;
        
        const dynamicFn = new Function(...paramNames, functionBody);
        
        // Execute with controlled globals
        const result = dynamicFn(...globalValues, context);
        
        clearTimeout(timeoutId);
        resolve(result);
        
      } catch (error) {
        clearTimeout(timeoutId);
        reject(error);
      }
    });
  }
}

// Usage example
const executor = new SafeCodeExecutor({
  console: console,
  Math: Math,
  JSON: JSON,
  fetch: fetch
});

executor.execute(`
  const doubled = context.value * 2;
  const random = Math.random();
  return { doubled, random };
`, { value: 42 }).then(result => {
  console.log('Result:', result);
});

This pattern provides several advantages over raw eval(). The function constructor creates functions rather than executing code directly, which provides slightly better scoping. The explicit parameter list gives you control over what globals are available. The timeout mechanism prevents runaway code from consuming unlimited resources.

Integrating with Content Scripts

When you need dynamic logic in content scripts, combine the Function constructor with message passing to keep sensitive code in the background:

// Background service worker - handles dynamic execution
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'executeTemplate') {
    const executor = new SafeCodeExecutor({
      console: console,
      Math: Math,
      Date: Date,
      JSON: JSON
    });
    
    executor.execute(message.code, message.context)
      .then(result => sendResponse({ success: true, result }))
      .catch(error => sendResponse({ success: false, error: error.message }));
    
    return true; // Keep message channel open for async response
  }
});

// Content script - requests execution
function executeTemplate(template, data) {
  return new Promise((resolve, reject) => {
    chrome.runtime.sendMessage({
      type: 'executeTemplate',
      code: template,
      context: data
    }, response => {
      if (response.success) {
        resolve(response.result);
      } else {
        reject(new Error(response.error));
      }
    });
  });
}

Alternative 3: Template-Based Code Generation

For extensions that need to generate similar code patterns repeatedly, template-based approaches provide a clean alternative to eval(). This method uses predefined code templates with placeholder substitution, ensuring all executed code follows auditable patterns.

Template Engine Implementation

class TemplateCodeGenerator {
  constructor(templates) {
    this.templates = templates;
  }

  generate(templateName, variables) {
    const template = this.templates[templateName];
    if (!template) {
      throw new Error(`Template '${templateName}' not found`);
    }

    // Validate all required variables are provided
    const placeholders = template.match(/\$\{([^}]+)\}/g) || [];
    for (const placeholder of placeholders) {
      const varName = placeholder.slice(2, -1);
      if (!(varName in variables)) {
        throw new Error(`Missing required variable: ${varName}`);
      }
    }

    // Replace placeholders with escaped values
    let code = template;
    for (const [key, value] of Object.entries(variables)) {
      const escapedValue = this.escapeValue(value);
      code = code.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), escapedValue);
    }

    return code;
  }

  escapeValue(value) {
    if (typeof value === 'string') {
      // Basic string escaping - enhance based on context
      return JSON.stringify(value);
    }
    return String(value);
  }
}

// Define your templates
const templates = {
  dataTransformer: `
    function transform(data) {
      const config = ${config};
      return data.map(item => ({
        id: item.${idField},
        name: item.${nameField},
        timestamp: new Date().getTime()
      }));
    }
    return transform(input);
  `,
  
  eventHandler: `
    document.addEventListener('${eventType}', function(e) {
      console.log('Event captured:', e.type);
      ${handlerCode}
    });
  `
};

const generator = new TemplateCodeGenerator(templates);

// Generate and execute code
const config = JSON.stringify({ mode: 'strict', version: 1 });
const code = generator.generate('dataTransformer', {
  config,
  idField: 'userId',
  nameField: 'displayName'
});

// Execute using chrome.scripting
chrome.scripting.executeScript({
  target: { tabId: tabId },
  func: new Function('input', code),
  args: [[{ userId: 1, displayName: 'John' }]]
});

Alternative 4: Web Workers for Isolated Execution

Web Workers provide excellent isolation for computationally intensive code while avoiding the restrictions that affect direct code execution. This approach is particularly valuable for extensions that perform complex calculations or data processing.

Implementing Extension Web Workers

// Background service worker - creates and manages worker
class WorkerManager {
  constructor() {
    this.workers = new Map();
  }

  createWorker(workerId, code) {
    const blob = new Blob([code], { type: 'application/javascript' });
    const workerUrl = URL.createObjectURL(blob);
    const worker = new Worker(workerUrl);
    
    this.workers.set(workerId, worker);
    
    return new Promise((resolve, reject) => {
      worker.onmessage = (e) => resolve(e.data);
      worker.onerror = (e) => reject(e);
    });
  }

  postMessage(workerId, message) {
    const worker = this.workers.get(workerId);
    if (!worker) {
      throw new Error(`Worker '${workerId}' not found`);
    }
    worker.postMessage(message);
  }

  terminate(workerId) {
    const worker = this.workers.get(workerId);
    if (worker) {
      worker.terminate();
      this.workers.delete(workerId);
    }
  }
}

// Usage
const workerManager = new WorkerManager();

// Define worker code as string
const workerCode = `
  self.onmessage = function(e) {
    const { action, data } = e.data;
    
    switch(action) {
      case 'calculate':
        const result = complexCalculation(data.values);
        self.postMessage({ success: true, result });
        break;
        
      case 'processBatch':
        const processed = data.items.map(item => processItem(item));
        self.postMessage({ success: true, processed });
        break;
        
      default:
        self.postMessage({ error: 'Unknown action' });
    }
  };

  function complexCalculation(values) {
    return values.reduce((sum, val) => sum + Math.sqrt(val), 0);
  }
  
  function processItem(item) {
    return { ...item, processed: true, timestamp: Date.now() };
  }
`;

// Initialize worker
await workerManager.createWorker('processor', workerCode);

// Send work to worker
workerManager.postMessage('processor', {
  action: 'calculate',
  data: { values: [1, 4, 9, 16, 25] }
});

Web Workers offer significant advantages including true parallel execution, complete isolation from the page context, and no access to the DOM. This isolation provides natural protection against many security issues that affect eval()-based approaches.


Alternative 5: Precompiled Function Registry

For extensions that need to support multiple code paths based on user configuration, a precompiled function registry provides flexibility without runtime code generation. This pattern maintains a collection of predefined functions that can be enabled or combined based on configuration.

Registry Implementation

class FunctionRegistry {
  constructor() {
    this.functions = new Map();
    this.defaults = {};
  }

  register(name, fn, defaultConfig = {}) {
    this.functions.set(name, { fn, config: defaultConfig });
    this.defaults[name] = defaultConfig;
  }

  execute(name, input, customConfig = {}) {
    const registered = this.functions.get(name);
    if (!registered) {
      throw new Error(`Function '${name}' not registered`);
    }

    const config = { ...registered.config, ...customConfig };
    return registered.fn(input, config);
  }

  listAvailable() {
    return Array.from(this.functions.keys());
  }
}

// Create registry with predefined functions
const registry = new FunctionRegistry();

// Register transformation functions
registry.register('filterByDate', (items, config) => {
  const threshold = config.daysAgo 
    ? Date.now() - (config.daysAgo * 24 * 60 * 60 * 1000) 
    : 0;
  
  return items.filter(item => new Date(item.date).getTime() > threshold);
}, { daysAgo: 30 });

registry.register('sortByField', (items, config) => {
  const { field, ascending = true } = config;
  return [...items].sort((a, b) => {
    const comparison = a[field] > b[field] ? 1 : -1;
    return ascending ? comparison : -comparison;
  });
}, { field: 'name', ascending: true });

registry.register('aggregateByCategory', (items, config) => {
  return items.reduce((acc, item) => {
    const category = item[config.categoryField || 'category'];
    acc[category] = (acc[category] || 0) + 1;
    return acc;
  }, {});
}, { categoryField: 'category' });

// Use the registry in your extension
function processData(data, operations) {
  let result = data;
  
  for (const operation of operations) {
    result = registry.execute(operation.name, result, operation.config);
  }
  
  return result;
}

// Example usage
const processed = processData(
  [{ name: 'Item 1', category: 'A', date: '2025-01-01' }],
  [
    { name: 'filterByDate', config: { daysAgo: 60 } },
    { name: 'sortByField', config: { field: 'name', ascending: true } }
  ]
);

This pattern provides excellent auditability because all possible code paths are defined in your extension bundle. Users and reviewers can inspect exactly what functions are available and what they do. Configuration controls which functions are used and with what parameters, providing flexibility without runtime code generation risks.


Best Practices for Dynamic Code in Manifest V3

Regardless of which alternative you choose, following security best practices ensures your extension remains secure and passes Chrome Web Store review.

Security Checklist

Always validate and sanitize any input that influences code execution. Even when using safer alternatives, unsanitized input can lead to unexpected behavior or security vulnerabilities. Use strict type checking and bounds validation before incorporating data into generated code or passing it to executed functions.

Prefer declarative approaches over imperative code generation. Chrome’s APIs like chrome.scripting and declarativeContent provide safe ways to accomplish common tasks without runtime code execution. These approaches are easier to audit, more performant, and less likely to trigger security warnings.

Keep sensitive operations in the background service worker whenever possible. The background context has fewer restrictions than content scripts and provides better isolation from potentially malicious web pages. Use message passing to coordinate between content scripts and background workers.

Document your code execution patterns clearly for reviewers. Chrome Web Store reviewers need to understand why your extension uses dynamic code execution and how it remains secure. Include comments and documentation explaining the purpose of any eval()-like patterns in your code.

Performance Considerations

Dynamic code execution carries performance overhead that can impact extension responsiveness. The Function constructor and template-based approaches involve parsing and compilation steps that take time. For frequently-called code paths, consider precompiling functions and caching results.

Web Workers provide the best performance for CPU-intensive tasks because they execute in separate threads. If your extension performs heavy computations, moving this work to workers improves overall responsiveness and prevents blocking the main extension flow.

When using chrome.scripting.executeScript(), minimize the frequency of injections. Each injection has overhead for context switching between the extension and page contexts. Batch related operations and consider persistent content scripts for scenarios requiring ongoing code execution.


Migration Strategies for Existing Extensions

Migrating from Manifest V2 eval()-based code to Manifest V3 alternatives requires careful planning. Start by auditing your existing code to identify all locations where dynamic code execution occurs.

Audit and Categorize

Group your dynamic code uses into categories based on their purpose. Code that generates user-specific configurations might map well to the template-based approach. Complex calculations could benefit from Web Workers. UI manipulation code should use chrome.scripting.executeScript().

For each category, evaluate which alternative provides the best balance of security, performance, and development complexity. Some uses might not require dynamic execution at all and can be refactored to use static code with configuration.

Incremental Migration

Migrate incrementally rather than attempting a complete rewrite. Start with the simplest use cases that map cleanly to alternatives, then address more complex scenarios. This approach reduces risk and allows you to validate each migration step.

Maintain backwards compatibility during migration if your extension supports both Manifest V2 and V3. Use feature detection to determine which execution method to use:

function executeCode(code, context) {
  // Check which API is available
  if (typeof chrome !== 'undefined' && chrome.scripting) {
    // Manifest V3 approach
    return chrome.scripting.executeScript({
      target: { tabId: context.tabId },
      func: new Function('ctx', code),
      args: [context]
    });
  } else if (typeof chrome !== 'undefined' && chrome.tabs) {
    // Manifest V2 approach (for legacy support)
    return chrome.tabs.executeScript(context.tabId, {
      code: `(${code})(${JSON.stringify(context)})`
    });
  } else {
    throw new Error('No suitable execution API available');
  }
}

Conclusion

Manifest V3’s restrictions on eval() and dynamic code execution, while challenging, have driven the development of better patterns for extension development. The alternatives explored in this guide each address specific use cases while maintaining security and auditability.

The chrome.scripting.executeScript() API should be your first choice for content script injection, providing the most straightforward migration path and best security properties. For more complex scenarios requiring dynamic logic, the Function constructor with proper safeguards, template-based code generation, Web Workers, and precompiled function registries offer flexible solutions.

Remember that the goal of these restrictions is not to limit extension functionality but to create a more secure extension ecosystem. By adopting these alternatives, you contribute to a safer environment for users while building extensions that can pass Chrome Web Store review with confidence.

The techniques in this guide provide a foundation for building robust, secure, and performant Chrome extensions that work within Manifest V3’s framework. As the platform continues to evolve, staying current with best practices ensures your extensions remain functional and compliant.


Additional Resources

For more information on Chrome extension development and Manifest V3, consult the official Chrome Extension Documentation and the Manifest V3 migration guide. The Chrome Extensions community forums provide valuable insights from developers working through similar migration challenges.

No previous article
No next article