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:
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:
Reload the extension, refresh Gmail… and… still broken. Rats!
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:
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?
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
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:
This code requires two new permissions in
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:
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!