How to Safely Use eval() in Chrome Extensions with Sandboxed Iframes (2026)

AppBooster Team · · 4 min read
Secure sandbox environment for code execution

The eval() Problem in Chrome Extensions

Chrome extensions enforce a strict Content Security Policy that blocks eval(), new Function(), and setTimeout/setInterval with string arguments. This protects users from code injection attacks, but it also breaks templating libraries (Handlebars, Underscore templates) and other tools that rely on dynamic code generation.

The solution: sandboxed iframes. They run in an isolated origin where eval() is allowed, but they can’t access chrome.* APIs or the extension’s privileged context.


Why Not Just Allow eval()?

Allowing eval() in your extension’s main context would give dynamically generated code full access to all chrome APIs, stored data, and permissions your extension holds. If any user input or external data reaches eval(), it becomes a severe security vulnerability.

Sandboxing separates concerns: dangerous operations run in isolation, results are passed back via postMessage().


Step 1: Declare Sandboxed Pages

{
  "manifest_version": 3,
  "name": "Template Engine Demo",
  "version": "1.0",
  "sandbox": {
    "pages": ["sandbox.html"]
  },
  "action": {
    "default_popup": "popup.html"
  }
}

Step 2: Create the Sandbox

sandbox.html

<!DOCTYPE html>
<html>
<head>
  <script src="https://cdn.jsdelivr.net/npm/handlebars@latest/dist/handlebars.min.js"></script>
</head>
<body>
  <script type="text/x-handlebars-template" id="user-card">
    <div class="card">
      <h2>{{name}}</h2>
      <p>{{email}}</p>
      {{#if premium}}<span class="badge">Premium</span>{{/if}}
      <ul>
        {{#each extensions}}
        <li>{{this.name}} - {{this.users}} users</li>
        {{/each}}
      </ul>
    </div>
  </script>

  <script type="text/x-handlebars-template" id="stats-dashboard">
    <div class="stats">
      <h3>{{title}}</h3>
      <p>Total installs: {{formatNumber installs}}</p>
      <p>Rating: {{rating}}/5</p>
    </div>
  </script>

  <script src="sandbox.js"></script>
</body>
</html>

sandbox.js

// Pre-compile all templates
const templates = {};
document.querySelectorAll('script[type="text/x-handlebars-template"]').forEach((el) => {
  templates[el.id] = Handlebars.compile(el.innerHTML);
});

// Register custom helpers
Handlebars.registerHelper('formatNumber', (num) => {
  return new Handlebars.SafeString(Number(num).toLocaleString());
});

// Listen for render requests from the extension
window.addEventListener('message', (event) => {
  const { templateId, context, requestId } = event.data;

  if (!templateId || !templates[templateId]) {
    event.source.postMessage({
      requestId,
      error: `Template "${templateId}" not found`
    }, event.origin);
    return;
  }

  try {
    const html = templates[templateId](context);
    event.source.postMessage({ requestId, html }, event.origin);
  } catch (error) {
    event.source.postMessage({
      requestId,
      error: error.message
    }, event.origin);
  }
});

Step 3: Communicate from Extension Pages

popup.html

<!DOCTYPE html>
<html>
<head>
  <style>
    body { width: 350px; padding: 16px; font-family: system-ui; }
    #output { margin-top: 12px; }
    .card h2 { margin: 0 0 4px; }
    .badge { background: #22c55e; color: white; padding: 2px 8px; border-radius: 12px; font-size: 12px; }
  </style>
</head>
<body>
  <iframe id="sandbox" src="sandbox.html" style="display:none"></iframe>
  <div id="output">Loading...</div>
  <script src="popup.js"></script>
</body>
</html>

popup.js

const sandbox = document.getElementById('sandbox');
const pendingRequests = new Map();
let requestCounter = 0;

window.addEventListener('message', (event) => {
  const { requestId, html, error } = event.data;
  const resolver = pendingRequests.get(requestId);
  if (resolver) {
    pendingRequests.delete(requestId);
    if (error) {
      resolver.reject(new Error(error));
    } else {
      resolver.resolve(html);
    }
  }
});

function renderTemplate(templateId, context) {
  return new Promise((resolve, reject) => {
    const requestId = ++requestCounter;
    pendingRequests.set(requestId, { resolve, reject });

    sandbox.contentWindow.postMessage(
      { templateId, context, requestId },
      '*'
    );
  });
}

// Usage
async function init() {
  const html = await renderTemplate('user-card', {
    name: 'Jane Developer',
    email: 'jane@example.com',
    premium: true,
    extensions: [
      { name: 'Tab Manager', users: 15000 },
      { name: 'Dark Reader', users: 85000 }
    ]
  });

  document.getElementById('output').innerHTML = html;
}

sandbox.addEventListener('load', init);

What Can and Can’t Run in the Sandbox

CapabilitySandboxExtension Page
eval()YesNo
new Function()YesNo
chrome.* APIsNoYes
DOM access (own page)YesYes
Network requestsYesYes
postMessageYesYes
Extension storageNoYes

Security Considerations

  1. Sanitize HTML output — Even though the sandbox is isolated, rendered HTML gets injected into your extension page. Use textContent instead of innerHTML when possible, or sanitize with DOMPurify.
  2. Validate message origins — Check event.origin in your message handlers.
  3. Don’t pass functionspostMessage() only serializes data. Functions, DOM nodes, and class instances can’t cross the boundary.
  4. Limit template complexity — Complex templates with many helpers increase the attack surface.

Beyond Templating

The sandbox pattern works for any CSP-incompatible code:

  • Markdown parsers that use eval() internally
  • Math expression evaluators
  • Custom scripting languages or DSLs
  • Legacy libraries that rely on dynamic code generation

What’s Next

Sandboxed iframes let you use powerful dynamic code features without compromising your extension’s security. Keep privileged operations in the main context, dangerous code in the sandbox, and communicate through structured messages.

Build secure, feature-rich extensions with ExtensionBooster.

Share this article

Build better extensions with free tools

Icon generator, MV3 converter, review exporter, and more — no signup needed.

Related Articles