Chrome extension for bypassing content security policy
The dreaded CORS issue is every developer's nightmare. But with this Chrome extension, we overcame a CORS issue that we faced at Keyvalue. Read on to find out how we built it.
The dreaded CORS issue is every developer's nightmare. But with this Chrome extension, we overcame a CORS issue that we faced at Keyvalue. Read on to find out how we built it.
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.
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.
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,
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,
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,
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
// 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
};
// Sending message from background.js to frame_content.js
chrome.tabs.sendMessage(tabId, message_in_JSON_format);
// 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.
chrome extension: https://developer.chrome.com/docs/extensions/mv3/getstarted/
Get our newsletters free every month. Get our newsletters free every month.