December 17, 2014

Gmail just broke every Chrome extension. Here's how we fixed ours

Gmail Just Broke Every Chrome Extension. Here's How We Fixed Ours | Mixmax

The bug: “Refused to load the script... because it violates the following Content Security Policy”

Yesterday Gmail introduced a Content Security Policy that broke Mixmax and other Chrome extensions. We jumped on it quickly and pushed a fix within an hour. Here’s how we diagnosed and solved the problem.

First, here’s a short introduction to how Mixmax works. Mixmax is a Chrome extension that upgrades Gmail’s compose window. The extension is very lightweight: it simply injects a <script> tag into the DOM when the browser navigates to https://mail.google.com. This is so we can push new code without needing to update the extension in the Chrome store (which has been an opaque and inconsistent process, taking anywhere from hours to days). The downside of injecting our script directly into the DOM is that we’re treated as a normal script tag, executed in the global namespace and also subject to a Content Security Policy. So when Gmail enforced their CSP policy yesterday, our script was blocked with this error:

Chrome web inspector image of Content Security Policy error

Yikes!

The Chrome extension API anticipates you might want to load third party scripts, of course, so extensions can specify a content_security_policy field in their manifest.json file. Perfect! So we added this to our manifest:

"content_security_policy": "script-src 'self' https://d1j5o6e2vipffp.cloudfront.net; object-src 'self'; frame-src 'self' https://app.mixmax.com"

Reload the extension, refresh Gmail… and… still broken. Rats!

Chrome web inspector image of Content Security Policy error

Problem #1: The content_security_policy field doesn’t work on cross-origin scripts.

Our Chrome extension injects our script using the following (simplified) code:

var script = document.createElement('script');
script.src = 'https://d1j5o6e2vipffp.cloudfront.net/src/build.js';
script.crossOrigin = 'anonymous';
document.head.appendChild(script);

The reason we add the crossOrigin attribute is because we attach a window.onerror handler to the page to report our uncaught exceptions. Without crossOrigin, the browser will only report ‘Script Error’ instead of giving us a stack trace. After searching around, we found Chrome bug #392338. It turns out the presence of crossOrigin on a script tag caused our extension’s content_security_policy field to be ignored.

So we commented out that attribute to see if it works:

//script.crossOrigin = 'anonymous';

Reload the extension, refresh Gmail… and… still broken. What is it now?

Chrome web inspector image of Content Security Policy error

This is strange because we included frame-src in our content_security_policy field in manifest.json. That’s not supposed to happen.

Problem #2: The content_security_policy field’s frame-src directive doesn’t work on remote iframes

Due to another Chrome bug involving cross-origin resources, the frame-src CSP directive is ignored if the frame is loaded from a remote url (as opposed to an HTML file within the extension). At this point it was looking like the content_security_policy manifest field was too unreliable.

We searched around a bit and came across a great post which describes how to intercept the HTTP headers on the wire to rewrite the content security policy. Here is our adapted code:

var hosts = 'https://d1j5o6e2vipffp.cloudfront.net';
var iframeHosts = 'https://app.mixmax.com';

chrome.webRequest.onHeadersReceived.addListener(function(details) {
  for (var i = 0; i &lt; details.responseHeaders.length; i++) {
    var isCSPHeader = /content-security-policy/i.test(details.responseHeaders[i].name);
    if (isCSPHeader) {
      var csp = details.responseHeaders[i].value;
      csp = csp.replace('script-src', 'script-src ' + hosts);
      csp = csp.replace('style-src', 'style-src ' + hosts);
      csp = csp.replace('frame-src', 'frame-src ' + iframeHosts);
      details.responseHeaders[i].value = csp;
    }
  }

  return {
    responseHeaders: details.responseHeaders
  };
}, {
  urls: ['https://mail.google.com/*'],
  types: ['main_frame']
}, ['blocking', 'responseHeaders']);

This code requires two new permissions in manifest.json: webRequest and webRequestBlocking. By default, adding required permissions to the manifest file and pushing an upgrade to the store will automatically disable the extension and prompt users to allow the new permissions. But we’re in luck: webRequest and webRequestBlocking aren’t on the list that require user approval. We tested the new permissions a few times with a copy of the extension in the store and it worked beautifully.

We hope the Chrome team fixes those two bugs to make the extension manifest content_security_policy field reliable. We’re eager to remove our code that rewrites HTTP headers–it's not an ideal solution because it could conflict with other extensions trying to do the same thing.

But at the end of the day, we're back, and that's what matters to our customers.

Like working on seemingly impossible problems? Join us to upgrade email to the 21st century!

You deserve a spike in replies, meetings booked, and deals won.

Try Mixmax free