60

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!

Ian McIntyre Silber
  • 5,553
  • 13
  • 53
  • 76
  • 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`. – G-Nugget Mar 20 '13 at 19:25
  • 1
    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. – user428747 Apr 09 '15 at 18:14
  • 6
    @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. – Pacerier Dec 16 '16 at 09:45

3 Answers3

79

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.

wOxxOm
  • 65,848
  • 11
  • 132
  • 136
Rob W
  • 341,306
  • 83
  • 791
  • 678
  • 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? – Ian McIntyre Silber Mar 20 '13 at 23:37
  • 4
    Turns out webRequest isn't accessible on content-scripts. I'll have to move it over to a background. – Ian McIntyre Silber Mar 20 '13 at 23:55
  • 2
    @IanMcIntyreSilber Hello, I'm just wondering if you ever got this to work and if you could possibly teach me if you did. – Zachrip Mar 26 '13 at 13:40
  • @Zachrip 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: https://developer.chrome.com/extensions/background_pages.html – Rob W Mar 26 '13 at 13:45
  • @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, xenonskiddders@gmail.com – Zachrip Mar 26 '13 at 23:05
  • @IanMcIntyreSilber Hi, I noticed Intab doesn't overcome this issue, I'm just wondering if you ever got this to work and if you? – Guy Korland Apr 01 '13 at 14:52
  • @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. – Rob W Apr 01 '13 at 14:55
  • @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? – Guy Korland Apr 01 '13 at 15:05
  • @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. `*://*/*`. – Rob W Apr 01 '13 at 15:10
  • @Rob W - I did it all but "onHeadersReceived" event doesn't show in the responseHeaders that the page has a in the page . – Guy Korland Apr 01 '13 at 15:21
  • 2
    @RobW X-Frame-Options can be set as a HTML meta element see: http://javascript.info/tutorial/clickjacking – Guy Korland Apr 01 '13 at 16:50
  • 2
    @GuyKorland I see. Chrome extensions cannot modify response bodies, so you're out of luck. – Rob W Apr 01 '13 at 16:54
  • @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... – Guy Korland Apr 07 '13 at 07:54
  • @GuyKorland Depends on your definition of "relevant tabs". Often, it suffices to use `chrome.tabs` events to keep track of them. Generic example: http://stackoverflow.com/a/11115057/938089. Example of excluding all incognito tabs: https://github.com/Rob--W/pdf.js/blob/e181a3c/extensions/chrome/pdfHandler-local.js#L31-L55 – Rob W Apr 07 '13 at 09:29
  • 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. – Bob Davies Mar 22 '14 at 20:47
  • @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. – Rob W Mar 22 '14 at 21:00
  • @RobW I had another go at it, turns out I was running into this: http://meta.stackexchange.com/questions/84462/firefox-4-alerts-framing-is-not-allowed 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. – Bob Davies Mar 22 '14 at 21:15
  • 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. – TaiwanGrapefruitTea Aug 31 '15 at 16:12
  • 2
    @TaiwanGrapefruitTea 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. – Rob W Aug 31 '15 at 19:20
  • This was working fine but suddenly it's not working for me (Chrome 46) – cprcrack Nov 09 '15 at 18:39
  • 1
    @cprcrack Works fine for me in 46.0.2490.80. `data:text/html, – Rob W Nov 10 '15 at 17:16
  • @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'.` – cprcrack Nov 10 '15 at 17:25
  • 1
    @cprcrack Works fine for me, `data:text/html,`. 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). – Rob W Nov 10 '15 at 17:28
  • @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: https://chrome.google.com/webstore/detail/ignore-x-frame-headers/gleekbfjekiniecknbkamfmkohkpodhe?hl=en-US Does that extension work for you? If you are available for a Skype chat, I'm cprcrack – cprcrack Nov 10 '15 at 17:35
  • 1
    @cprcrack 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: https://code.google.com/p/chromium/issues/detail?id=453843 – Rob W Nov 10 '15 at 17:42
  • @RobW thanks a lot. Can you think about any workaround? – cprcrack Nov 10 '15 at 17:51
  • 1
    @cprcrack Nuke Whatsapp's appcache. – Rob W Nov 10 '15 at 18:05
  • 1
    To make this work on `github.com` domain you need to remove `content-security-policy` header as well. – icl7126 Feb 13 '17 at 09:42
  • 1
    Including "types: [ 'sub_frame' ]" caused problems for me. I removed that and it appears to be working. – ElJeffe Feb 26 '17 at 23:46
  • 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://stackoverflow.com/q/958997/488666 – Maxime Pacary Mar 26 '18 at 09:37
  • 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? – Amir Jun 05 '19 at 16:05
  • 1
    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. – Sasha Pachev Jan 27 '20 at 21:26
  • 1
    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." https://developer.chrome.com/docs/extensions/reference/webRequest/ – danbae Jun 05 '21 at 12:30
26

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) and clear its cache 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],
      }, {
        cacheStorage: true,
        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});
    }
    
wOxxOm
  • 65,848
  • 11
  • 132
  • 136
  • 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: "" o.o – Nate Jul 08 '22 at 18:01
  • 1
    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. – user7607751 Sep 10 '22 at 03:13
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.

GuidN
  • 48
  • 4
  • 3
    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](/help/how-to-answer). – Swayangjit Jun 01 '22 at 09:40