Working around Content Security Policy issues in Chrome Extensions

4 minute read

Previously, we discussed a use case for a Chrome Extension to inject a script via script tag into the web page. The script would run in the context of that web page so that the extension can access resources and share javascript objects with the web page. However, some web pages have a Content Security Policy that prevents AJAX calls to domains that don’t belong to a whitelist. This article explains our method to get around that.

Execution Contexts

There are essentially three executions contexts, where each one is an almost entirely isolated environment.

  • The execution environment of the web page, these includes any scripts that are orginally loaded by website, or anything contained by script tags added to the DOM of the document. Any script run in this context is subject to the original content security policies by the web page. Furthermore, you can’t directly access any of the Chrome Extension resources. (see how to load scripts into this environment)

  • The execution environment of content scripts. They are scripts started by chrome.tabs.executeScript(). Content script can manipulate the DOM of the host web page. In this execution context, you can’t access any javascript objects or functions on the web page. But it still have access to the chrome extension resources such as chrome.runtime or chrome.tabs.

  • Execution environment of the Chrome extension itself.

Background of the APIRequest.io Chrome Extension

The APIRequest.io Ajax Capture Chrome Extension, was created to capture both request and responses for single page applications (to enable easier collaboration and debugging of these apps). Before this extension existed, there was no extension as far as we know that could capture responses due to the limitations of the Chrome WebRequest API. The solution we found involves using a script tag to inject a script into the web page’s context as discussed in this post

However, compatibility with Content Security Policy was not added for the initial release due to time constraints. Thus, in the web page’s execution context, we can’t make AJAX calls (needed to store the captured data in a persisted and sharable link) if the original web page’s Content Security Policy doesn’t allow us to communicate with domains that don’t belong to the original whitelist.

Solution

In order to be compatible with arbitrary Content Security Policies, the solution is to pass the data to another Execution Context where it is not subject to the Content Security Policy, execute the AJAX call, and process the result.

Message Passing Between the Web Page Context and Content Script.

This involves using window.postMessage()

Sending the message


  const domain = window.location.protocol + '//' + window.location.hostname + ':' + window.location.port;
  // console.log(domain);
  window.postMessage({ type: 'API_AJAX_CALL', payload: payload}, domain);

The domain variable is the web page that the message will be sent to. Since the web page and the content script actually are executing on the same page itself, by using the same domain of the host web page, the message will be passed to the content script and vice versa.

While it is possible to do window.postMessage(data, '*'), but the ‘*’ implies any page can read the message, which can be dangerous. You don’t want malicious web pages (in other tabs) to see the messages if the message is sensitive.

1. Receiving message and make the AJAX call

In the content script context, we aren’t subject to the Content Security Policy, we can receive the message and make the API call.

window.addEventListener("message", function(event) {
  // We only accept messages from ourselves
  if (event.source != window)
    return;

  // console.log("Content script received event: " + JSON.stringify(event.data));

  if (event.data.type && (event.data.type == "API_AJAX_CALL")) {
    //make my ajax call here with the payload.
    const request = superagent.post(myAPIEndPointUrl)

    request.send(event.data.payload)
      .end(function (err, res) {
          returnAjaxResult(err, res.body)     
      });
  }
}, false);

Since the window sending the message is the same window, we should check to make sure it is the same before we accept the message. This ensures we know where the message comes from.

2. Return result of the Ajax to original context

The windows.postMessage does not have a callback method. Therefore, to pass AJAX result back to original webpage, we have to use windows.postMessage again.

function returnAjaxResult(err, resbody) {
  const domain = window.location.protocol + '//' + window.location.hostname + ':' + window.location.port;
  // console.log(domain);
  window.postMessage({ type: 'API_AJAX_RESULT', payload: {error: err, responsebody: resbody}}, domain);
}

In this manner, the Content Script acts like a proxy for the AJAX calls to domains not in the Content Security Policy.

Message passing between ContentScript and Extension

Since both have access to Chrome Extension related objects, you can just use those resources

From content script to extension:

chrome.runtime.sendMessage({payload: playload}, function(response) {
  // callback

});

Passing message from extension to content script is more interesting because it depends on which tab you executed the content script. (chrome.tabs.executeScript requires a tabId also, so you can just remember that.)

  chrome.tabs.sendMessage(tabId, {playload: playload}, function(response) {
    // callback

  });

The message passing also have a call back, which makes a lot of easier to handle.

Closing Thoughts

Our focus isn’t to build chrome extensions, but as a side project tool that we use ourselves, it is definitely a fun project. For this Content Security Policy issue, I had punt it for a bit in lieu of time constraints, but then a user messaged me that he was able to get it work using message passing. We are glad others have found our side projects useful as well since we use APIRequest.io Capture Chrome Extension and our very popular Moesif CORS Extension often for our own single page apps.

Do you spend a lot of time debugging customer issues?
Moesif makes debugging easier for RESTful APIs and integrated apps


Learn More

Leave a Comment