I'm the author of Intab, a Chrome extension that lets you view a link inline as opposed to a new tab. There's not much fancy stuff going on behind the scenes, it's just an iframe that loads the URL the user clicked on.
It works great except for sites that set the X-Frame-Options header to DENY or SAMEORIGIN. Some really big sites like Google and Facebook both use it which makes for a slightly janky experience.
Is there any way to get around this? Since I'm using a Chrome extension, is there any browser level stuff I can access that might help? Looking for any ideas or help!
3 Answers 3
This answer is for ManifestV2 and policy-installed MV3 extensions.
For normal ManifestV3 extensions see the other answer(s).
Chrome offers the webRequest API to intercept and modify HTTP requests. You can remove the X-Frame-Options header to allow inlining pages within an iframe.
chrome.webRequest.onHeadersReceived.addListener(
function(info) {
var headers = info.responseHeaders;
for (var i=headers.length-1; i>=0; --i) {
var header = headers[i].name.toLowerCase();
if (header == 'x-frame-options' || header == 'frame-options') {
headers.splice(i, 1); // Remove header
}
}
return {responseHeaders: headers};
}, {
urls: [
'*://*/*', // Pattern to match all http(s) pages
// '*://*.example.org/*', // Pattern to match one http(s) site
],
types: [ 'sub_frame' ]
}, [
'blocking',
'responseHeaders',
// Modern Chrome needs 'extraHeaders' to see and change this header,
// so the following code evaluates to 'extraHeaders' only in modern Chrome.
chrome.webRequest.OnHeadersReceivedOptions.EXTRA_HEADERS,
].filter(Boolean)
);
In the manifest, you need to specify the webRequest and webRequestBlocking permissions, plus the URLs patterns you're intending to intercept i.e. "*://*/*" or "*://www.example.org/*" for the example above.
33 Comments
ManifestV3 example using declarativeNetRequest
See also the warning at the end of this answer!
manifest.json for Chrome 96 and newer,
doesn't show a separate permission for "Block page content" during installation
"minimum_chrome_version": "96",
"permissions": ["declarativeNetRequestWithHostAccess"],
"host_permissions": ["*://*.example.com/"],
"background": {"service_worker": "bg.js"},
bg.js for Chrome 101 and newer using initiatorDomains and requestDomains
(don't forget to add "minimum_chrome_version": "101" in manifest.json)
const iframeHosts = [
'example.com',
];
chrome.runtime.onInstalled.addListener(() => {
const RULE = {
id: 1,
condition: {
initiatorDomains: [chrome.runtime.id],
requestDomains: iframeHosts,
resourceTypes: ['sub_frame'],
},
action: {
type: 'modifyHeaders',
responseHeaders: [
{header: 'X-Frame-Options', operation: 'remove'},
{header: 'Frame-Options', operation: 'remove'},
// Uncomment the following line to suppress `frame-ancestors` error
// {header: 'Content-Security-Policy', operation: 'remove'},
],
},
};
chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [RULE.id],
addRules: [RULE],
});
});
Old Chrome 84-100
Use the following instead, if your extension should be compatible with these old versions.
manifest.json for Chrome 84 and newer,
shows a separate permission for "Block page content" during installation
"permissions": ["declarativeNetRequest"],
"host_permissions": ["*://*.example.com/"],
"background": {"service_worker": "bg.js"},
bg.js for Chrome 84 and newer using the now deprecated domains
const iframeHosts = [
'example.com',
];
chrome.runtime.onInstalled.addListener(() => {
chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: iframeHosts.map((h, i) => i + 1),
addRules: iframeHosts.map((h, i) => ({
id: i + 1,
condition: {
domains: [chrome.runtime.id],
urlFilter: `||${h}/`,
resourceTypes: ['sub_frame'],
},
action: {
type: 'modifyHeaders',
responseHeaders: [
{header: 'X-Frame-Options', operation: 'remove'},
{header: 'Frame-Options', operation: 'remove'},
// Uncomment the following line to suppress `frame-ancestors` error
// {header: 'Content-Security-Policy', operation: 'remove'},
],
},
})),
});
});
Warning: beware of site's service worker
You may have to remove the service worker of the site(s) before adding the iframe or before opening the extension page because many modern sites use the service worker to create the page without making a network request thus ignoring our header-stripping rule.
Add
"browsingData"to"permissions"in manifest.jsonClear the SW:
function removeSW(url) { return chrome.browsingData.remove({ origins: [new URL(url).origin], }, { serviceWorkers: true, }); }// If you add an iframe element in DOM:
async function addIframe(url, parent = document.body) { await removeSW(url); const el = document.createElement('iframe'); parent.appendChild(el); el.src = url; return el; }// If you open an extension page with an
<iframe>element in its HTML:async function openPage(url) { await removeSW('https://example.com/'); return chrome.tabs.create({url}); }
In Firefox chrome.runtime.id is a different thing
Use chrome.runtime.getURL('').split('/')[2] instead.
5 Comments
initiatorDomains to be the domain of the hosting website, NOT the domain of the extension itself.allFrames:true. The content script will run inside the iframe and see its contents. It can use chrome.runtime.sendMessage (safe) or parent.postMessage (spoofable) to communicate to the popup directly. Note that the popup is a separate window so it has its own separate devtools: right-click inside the popup and select "inspect" in the menu.You can try the Frame extension that lets the user drop X-Frame-Options and Content-Security-Policy HTTP response headers, allowing pages to be iframed.
The code is available on github
It's based on ManifestV3 and working perfectly with Google & Facebook.
1 Comment
Explore related questions
See similar questions with these tags.
X-Frame-Options. In javascript, no error is thrown and no events are triggered when a page load is blocked byX-Frame-Options.