Getting around X-Frame-Options DENY in a Chrome extension?
Asked Answered
M

3

61

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!

Malena answered 20/3, 2013 at 19:20 Comment(3)
It might be different for extensions, but I know that in javascript there is currently no way of knowing if the load was blocked by X-Frame-Options. In javascript, no error is thrown and no events are triggered when a page load is blocked by X-Frame-Options.Schnurr
I don't think so its going to be possible. There is a reason why X-Frame-Option is added which is so that the Url cannot be framed in an Iframe which is not in a domain (in case of Same Origin). If somehow u are able to bypass this its a security breach/bug in X-Frame whihc will be fixed in the later version. Also more and more websites are using this option to add that security to their website without doing a lot of stuff:. It would be exciting to see if it can be beaten though. Thats my 2 cents.Lamkin
@user428747, Chrome extensions should be allowed to do it. They aren't javascript, they are part of the "trusted bundle" which means that they should be considered part of the browser itself.Broil
S
81

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.

Shortstop answered 20/3, 2013 at 21:12 Comment(33)
Wow, this is really helpful. I think I'm close to getting it to work but I'm running into an error: Uncaught TypeError: Cannot read property 'onBeforeSendHeaders' of undefined. Fiddled with a bunch of stuff but can't seem to resolve it. Any ideas?Malena
Turns out webRequest isn't accessible on content-scripts. I'll have to move it over to a background.Malena
@IanMcIntyreSilber Hello, I'm just wondering if you ever got this to work and if you could possibly teach me if you did.Cellophane
@Cellophane Exactly as stated in the answer. Copy-paste the answer to a file called background.js, and add the "background": {"scripts":["background.js"]} section to the manifest file. For more information, see the documentation for background pages: developer.chrome.com/extensions/background_pages.htmlShortstop
@RobW I've already done so, but it doesn't seem to work. If we can get in contact I can show you what I have. Either Skype(preferred)-zachripper, or email, [email protected]Cellophane
@IanMcIntyreSilber Hi, I noticed Intab doesn't overcome this issue, I'm just wondering if you ever got this to work and if you?Goshen
@GuyKorland Do you have any other extensions which modify headers? This could cause the header to be re-added. Instead of removing the header, try putting garbage in it.Shortstop
@Rob W - I tried to write an extension according to solution suggested and it didn't seem to work, do you have a working solution?Goshen
@GuyKorland Yes. I have already received some mails from others with similar questions. The most common mistake is to forget the last line of my answer. To get a working example, you need at least three permissions: webRequest, webRequestBlocking, and the origin permission for the site you want to free, e.g. *://*/*.Shortstop
@Rob W - I did it all but "onHeadersReceived" event doesn't show in the responseHeaders that the page has a <meta http-equiv="X-Frame-Options" content="deny"> in the page <head>.Goshen
@RobW X-Frame-Options can be set as a HTML meta element see: javascript.info/tutorial/clickjackingGoshen
@GuyKorland I see. Chrome extensions cannot modify response bodies, so you're out of luck.Shortstop
@RobW on the same issue do you know how to filter events from non relevant tabs? The problem is that using tabID won't help since chrome.tabs is an async method...Goshen
@GuyKorland Depends on your definition of "relevant tabs". Often, it suffices to use chrome.tabs events to keep track of them. Generic example: https://mcmap.net/q/324201/-chrome-extension-attach-properties-to-each-tab. Example of excluding all incognito tabs: github.com/Rob--W/pdf.js/blob/e181a3c/extensions/chrome/…Shortstop
I couldn't get this to work because Chrome seems to have been modified since this post was made. The code technically does the right stuff and it works, but will not work against an iframe in a background page anymore due to a restriction against the extension loading iframes. There may still be a workaround by combining this in the background page, along with dynamically embedding the iframe in a content page, then using messaging to manage it, but not tried that yet since managing enabling/disabling this by site (so I don't interfere with normal framebreaking) would be pretty complex.Mauve
@BobDavies The method still works fine for me in Chromium 33.0.1750.152. When I try to load stackoverflow.com in an iframe in my background page with the code from my answer, Chrome does not block the iframe. Without the code, the iframe is blocked (because of X-Frames-Option: SAMEORIGIN). Please show a reasonably scoped example (source code) where you're experiencing the issue, and mention your Chrome version.Shortstop
@RobW I had another go at it, turns out I was running into this: meta.stackexchange.com/questions/84462/… I assumed it was being generated by Chrome, but it was not (it's in SOs code somewhere), presumably the result of an isnottop test.Mauve
Thanks, this is a very useful post! This method worked for me on Chrome 44. Note: when I look at the Response Header in the Chrome debugger, I seem to see the "before modified values" listed as: x-frame-options:SAMEORIGIN but I don't get the error message any more and the iframe displays content as desired.Hettiehetty
@Hettiehetty Header modifications by extensions don't show up in the devtools. If you want to see whether the header modification was successful, take a look at chrome://net-internals/#events.Shortstop
This was working fine but suddenly it's not working for me (Chrome 46)Anemone
@Anemone Works fine for me in 46.0.2490.80. data:text/html,<iframe src="https://stackoverflow.com"> is normally a blank screen, but I can successfully get the frame to load after creating an extension with the above snippet.Shortstop
@RobW not working for me with https://web.whatsapp.com/, could you try it? Gives me error Refused to display 'https://web.whatsapp.com/' in a frame because it set 'X-Frame-Options' to 'DENY'.Anemone
@Anemone Works fine for me, data:text/html,<iframe src="https://web.whatsapp.com/"></iframe>. Are you sure that your extension does not contain an error? E.g. do you have the right permissions (you'll need "permissions": ["https://web.whatsapp.com/*", "webRequest", "webRequestBlocking"] in manifest.json).Shortstop
@RobW yes, I have those permissions, and it was working fine some time ago without any change. I'm on Windows 10, are you? I created a test page at http://twentyspy.com/wa.html and I don't seem the only one suffering this issue, this extension is also unable to allow framing, at least in my environment: chrome.google.com/webstore/detail/ignore-x-frame-headers/… Does that extension work for you? If you are available for a Skype chat, I'm cprcrackAnemone
@Anemone Reproduced (even using my simplified example, by reloading the page). Whatsapp is using App cache, which results in re-use of the cache (without extension modifications) without network requests. Looks like a known bug: code.google.com/p/chromium/issues/detail?id=453843Shortstop
@RobW thanks a lot. Can you think about any workaround?Anemone
@Anemone Nuke Whatsapp's appcache.Shortstop
To make this work on github.com domain you need to remove content-security-policy header as well.Bax
Including "types: [ 'sub_frame' ]" caused problems for me. I removed that and it appears to be working.Dugaid
Once the X-Frame-Options header removed (thanks to this solution), It seems that Google has recently added a "frame buster" right before redirecting to OAuth callback URL, which is rather complex to get around + leads to cat/mouse playing https://mcmap.net/q/80596/-frame-buster-buster-buster-code-needed/488666Caban
It used to work for me, but recently stopped working. could it be that Chrome changed their API or security policy so this is not possible anymore?Indefectible
Got it to work to display the results of a custom Google search in an iframe after removing content-security-policy header in addition to the frame ones.Elianaelianora
Update 2021: "Starting from Chrome 89, the X-Frame-Options response header cannot be effectively modified or removed without specifying 'extraHeaders' in opt_extraInfoSpec. [array which is parameter 3 to the chrome.webRequest.*.addListener() calls] Note: Specifying 'extraHeaders' in opt_extraInfoSpec may have a negative impact on performance, hence it should only be used when really necessary." developer.chrome.com/docs/extensions/reference/webRequestLohengrin
P
30

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.

  1. Add "browsingData" to "permissions" in manifest.json

  2. Clear 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});
    }
    
Polak answered 14/9, 2021 at 12:28 Comment(4)
We did everything except for the manifest host_permissions, and it failed. Thanks for the very complete solution! Also, for the lazy people who don't care about security, this is a valid URL pattern: "<all_urls>" o.oMisbelief
Side note: For those of you whom are trying to use an extension to inject iframe from a website inside another website (NOT inside the extension itself), please note, you'll need to change the rule's initiatorDomains to be the domain of the hosting website, NOT the domain of the extension itself.Company
I'm looking to implement this in the popup of an extension. How do I access the iframe content once I've implemented this manifest and bg.js please? I can't see where the resultant iframe goes! Thanks.Klutz
Register a content script using chrome.scripting.registerContentScripts with 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.Polak
F
2

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.

Fideicommissum answered 1/6, 2022 at 8:40 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Gauntlett

© 2022 - 2024 — McMap. All rights reserved.