Chrome extension for bypassing content security policy

Engineering
Share this

Share this blog via

Blog post | Chrome extension for bypassing content security policy

Every developer at one point in their early career must have come across a CORS issue and didn't know what to do. đŸ™‚ And eventually would master, obviously with a bit of fire-fighting, how to take care of them by enabling the right headers on the server side. But often there would be instances where setting the headers on the server side are not in our hands. We at KeyValue came across one such instance where we had to access a third-party website loaded in an iframe. We eventually had to build a tool to bypass the content security policy. Let's check how we did it.

What were we trying to do?

Assume that KeyValue has a Product Recommendation Engine and Acme Inc.(a fictitious e-commerce client) wants to use this module on their website to show recommendations to their customers.

To help Acme Inc. see how recommendations will be displayed on the end users' website, we needed to load Acme Inc.'s website in an iframe, view and inspect the Products, search for a position, and inject the recommendation module to see how recommendations are displayed.

As a feature, this is the functionality we were trying to achieve, but we had technical challenges to get this done as mentioned below.

Technical Challenges

  1. How to load a website that has a different domain in an iframe?
  2. How to load a mobile preview of the website in an iframe?
  3. How to communicate between the parent frame and sub-frame?

Approaches tried

  • The first option that we have tried out is creating a proxy server. There are many limitations with this approach like getting the full DOM content and accessing image URLs and other links. And for some websites, we even got encrypted DOM content.
  • The second option that we have tried out is building a chrome extension and that solved all the challenges. Let’s dive into more details of this approach.
    Please refer chrome extensions to know what they are and how to get started.

Implementation details

Challenge 1 - How to load a website that has a different domain in an iframe?
We all know that browser will not allow loading a website of a different domain in an iframe if the website has set content-security-policy or x-frame-options to same-origin. To overcome this we need to remove content-security-policy and x-frame-options from response headers.

Chrome extension has access to all the request and response headers using chrome.webRequest API. Using this API we can intercept the response headers and filter out content-security-policy and x-frame-options thereby we can load the website in an iframe.

const modifyResponseHeaders = (tabId) => {
  // removes csp, x-frame-options from client website to load client website in tool iframe.
  chrome.webRequest.onHeadersReceived.addListener((info) => {
    const headers = info.responseHeaders.filter((header) => {
      const headerName = header.name.toLowerCase();
      return !(headerName === 'x-frame-options' || headerName === 'frame-options'
        || headerName === 'content-security-policy');
    });

    return { responseHeaders: headers };
  },
  { urls: ['*://*/*'], types: ['sub_frame'], tabId },
  ['blocking', 'responseHeaders', 'extraHeaders']);
};

Using the chrome.webRequest API we will add an onHeadersReceived listener which has three arguments,

  • callback function which gets details of the response headers and can modify the response headers. Using this we will filter out the x-frame-options and content-security-policy
  • The webRequest.RequestFilter filter allows limiting the requests for which events are triggered in various dimensions:
    i) urls: it can be any URL
    ii) types: since we are applying this to iframe alone, specify as sub_frame
    iii) tabId: we will get the tabId from extension background.js. The listener will be added on the specified tab alone.
  • opt_extraInfoSpec (optional): if this array contains
    i) 'blocking': the callback function is handled synchronously
    ii) 'responseHeaders': to get and modify the response headers in the callback function
    iii) 'extraHeaders': to modify headers in a way to violate the CORS protocol

Challenge 2 - How to load a mobile preview of the website in an iframe?
Most websites use the user-agent from the request headers to load the mobile website content. The user-agent request header is a string that lets servers and network peers identify the application, operating system, vendor, and/or version of the requesting user agent. So using the user-agent, javascript can identify if it is a mobile browser or not and thereby load the mobile content.

To load a mobile preview of the website in an iframe, we can modify the user-agent from the request headers using the chrome extension's webRequest API.

const renderPreviewMode = (tabId, previewMode) => {
  // displaying the desktop/mobile view
  const webRequestCallback = (details) => {
    const headers = details.requestHeaders;
    if (previewMode === 'mobile') {
      for (let i = 0; i < headers.length; i += 1) {
        if (headers[i].name.toLowerCase() === 'user-agent') {
          let { value } = headers[i];
          const startIndex = value.indexOf(' (');
          const endIndex = value.indexOf(')');
          if (startIndex !== -1 && endIndex !== -1) {
            value = value.replace(value.substring(startIndex, endIndex + 1), '');
            // appending the (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) to user agent to display the mobile view
            value = `${value.substring(0, startIndex)} (iPhone; CPU iPhone OS 10_3_1 like Mac OS X)${value.substring(startIndex, value.length - 1)}`;
          }
          const mobileIndex = value.indexOf('Safari');
          if (mobileIndex !== -1) {
            value = `${value.substring(0, mobileIndex)}Mobile/14E304 ${value.substring(mobileIndex, value.length - 1)}`;
          }
          headers[i].value = value;
          break;
        }
      }
    }
    return { requestHeaders: headers };
  };
  chrome.webRequest.onBeforeSendHeaders.addListener(webRequestCallback,
    { urls: ['<all_urls>'],
      types: ['sub_frame'],
      tabId },
    ['blocking', 'requestHeaders']);
};

Using the chrome webRequest API we will add an onBeforeSendHeaders listener which has three arguments,

  • callback function which gets details of the request headers by which the user-agent in request headers is modified based on the previewMode which we get from the tool.
  • The webRequest.RequestFilter filter allows limiting the requests for which events are triggered in various dimensions:
    i) urls: it can be any url
    ii) types: since we are applying this to iframe alone, specify as sub_frame
    iii) tabId: we will get the tabId from extension background.js. The listener will be added on the specified tab alone.
  • opt_extraInfoSpec (optional): if this array contains
    i) 'blocking': the callback function is handled synchronously
    ii) 'requestHeaders': to get and modify the request headers in the callback function

Challenge 3 - How to communicate between the parent frame and sub-frame?
A communication channel between the parent frame(the frame in which the extension tool is running) and sub-frame(client website iframe) is required to inspect the client website, inject the elements to the client website, send back messages to the parent frame, etc.
The browser will not allow to access the content of the sub-frame (using iframe.contentWindow.contentDocument) because iframe is hosting a different domain.
So in order to communicate, we use the chrome extension to inject a web_accessible_resources (frame-content.js) to the iframe which will create a communication channel from the parent frame to frame-content.js which is residing inside the sub-frame.

i) Injecting a script (frame-content.js) to iframe using chrome extension in the background script

const injectFrameContent = ({ tabId, url = [] }) => {
  chrome.webNavigation.getAllFrames({ tabId }, (frames) => {
    const frame = frames.find((el) => el.url === url);
    if (frame) {
      chrome.tabs.executeScript(tabId, {
        frameId: frame.frameId,
        file: 'frame-content.js'
      });
      chrome.tabs.insertCSS(tabId, {
        frameId: frame.frameId,
        file: 'frame-content.css'
      });
    }
  });
};

Using the getAllFrames method of chrome.webnavigation API, we will get information about all the frames in the specified tab. getAllFrames has 2 arguments,

  • details: Information about the tab to retrieve all frames from.
  • callback function: which gives a handle of all the frames inside the tab by which we find out which frame is the client website iframe. Once we get the client website frameId, we will inject frame-content.js using chrome.tabs.executeScript. Also we will inject the corresponding CSS file using chrome.tabs.insertCSS method

Now that we have injected the script into the iframe, we need to establish a communication channel between the various extension components (Extensions include background scripts, content scripts, web accessible resources, etc).

ii) An end-to-end communication between the tool and frame-content.js happens with multiple hops as shown in the figure below

  • The tool sends a message to the content_script (content.js) using window.postMessage, while it listens for a message using a message listener.
  • The content_script (content.js) sends a message to the background script (background.js) using chrome.runtime.sendMessage method, while it listens for messages from background script using chrome.runtime.onMessage listener.
// sending message from content.js to background.js
chrome.runtime.sendMessage(message_in_JSON_format);
// content.js listening for messages from background.js
chrome.runtime.onMessage.addListener((message, senderResponse) => {
  // message in JSON format
  // senderResponse will have the tabId from which the message is send
};
  • The background script (background.js) sends message to the web accessible resource (frame_content.js) using chrome.tabs.sendMessage method, while it listens for messages from frame_content.js using chrome.runtime.onMessage listener
// Sending message from background.js to frame_content.js
chrome.tabs.sendMessage(tabId, message_in_JSON_format);
  • frame_content.js sends a message to background.js using chrome.runtime.sendMessage method.
// sending message from frame_content.js to background.js
chrome.runtime.sendMessage(message_in_JSON_format);

Our experience in building feature-rich platforms assured success while we developed the tool to bypass the content security policy. You too can make use of our expertise because building cool ideas is what we do.

References:

chrome extension: https://developer.chrome.com/docs/extensions/mv3/getstarted/