52

I want to create a browser extension which creates a sidebar. Chrome does not have a first-class sidebar, and so we must instead put an iframe in the page. However, this breaks on many pages due to content security policy. E.g. GitHub uses a CSP, which does not allow iframes from other sites to be embedded within it. E.g. if you try to put the capitalone.com website in an iframe on GitHub you get the following:

Refused to frame 'https://www.capitalone.com/' because it violates the following Content Security Policy directive: "frame-src 'self' render.githubusercontent.com www.youtube.com assets.braintreegateway.com".

Here's a simple browser extension to reproduce that:

chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
  if (changeInfo.status === 'complete') {
   chrome.tabs.executeScript(tabId, { code: 'document.body.innerHTML=\'<iframe style=\"width:600px; height:600px\" src=\"https://www.capitalone.com/\"></iframe>\' + document.body.innerHTML;' }, function() {
     console.log('Iframe injection complete');
   })
  }
}.bind(this));

Yet, according to Wikipedia, a browser extension should be able to inject an iframe despite any content security policy:

According to the CSP Processing Model,[20] CSP should not interfere with the operation of browser add-ons or extensions installed by the user. This feature of CSP effectively allows any add-on or extension to inject script into websites, regardless of the origin of that script, and thus be exempt from CSP policies.

Is there some other way that I should be injecting an iframe besides what I'm doing?

Manikandan C
  • 668
  • 1
  • 9
  • 22
Ben McCann
  • 18,548
  • 25
  • 83
  • 101

3 Answers3

86

The inability to insert an external iframe in Chrome is a bug (crbug.com/408932).

If you want to embed an external frame in an external website, then it must be loaded in a frame that is packaged with your extension.

manifest.json

{
    "name": "Embed external site",
    "version": "1",
    "manifest_version": 2,
    "content_scripts": [{
        "js": ["contentscript.js"],
        "matches": ["*://*/*"],
        "all_frames": true
    }],
    "web_accessible_resources": [
        "frame.html"
    ]
}

Do NOT use chrome.tabs.onUpdated + chrome.tabs.executeScript if you want a content script to always be inserted in a document. Your implementation is flawed and can cause the script to be run multiple times. Instead, you should declare the content script in the manifest file.

(remove "all_frames": true if you don't want to insert the frame in every subframe.)

contentscript.js

// Avoid recursive frame insertion...
var extensionOrigin = 'chrome-extension://' + chrome.runtime.id;
if (!location.ancestorOrigins.contains(extensionOrigin)) {
    var iframe = document.createElement('iframe');
    // Must be declared at web_accessible_resources in manifest.json
    iframe.src = chrome.runtime.getURL('frame.html');

    // Some styles for a fancy sidebar
    iframe.style.cssText = 'position:fixed;top:0;left:0;display:block;' +
                           'width:300px;height:100%;z-index:1000;';
    document.body.appendChild(iframe);
}

frame.html

<style>
html, body, iframe, h2 {
    margin: 0;
    border: 0;
    padding: 0;
    display: block;
    width: 100vw;
    height: 100vh;
    background: white;
    color: black;
}
h2 {
    height: 50px;
    font-size: 20px;
}
iframe {
    height: calc(100vh - 50px);
}
</style>
<h2>Displaying https://robwu.nl in a frame</h2>
<iframe src="https://robwu.nl/"></iframe>

It is important to observe that I loaded an https site in the frame. If you attempt to load an HTTP site in the frame, then the mixed content policy will block the frame from being loaded if one of the parent frames is an https page.

Replace https://robwu.nl/ with http://example.com/ and the frame will remain blank on https pages such as https://github.com. Simultaneously, the following message will be printed to the console.

[blocked] The page at 'https://github.com/' was loaded over HTTPS, but ran insecure content from 'http://example.com/': this content should also be loaded over HTTPS

Community
  • 1
  • 1
Rob W
  • 341,306
  • 83
  • 791
  • 678
  • Hi Rob, thanks for the excellent answer. I want to inject a script which is loaded from my server so that I can handle updates without having to push a new extension. Unfortunately that means I can't bundle it as a content script. Can I do something like have a simple content script that just accepts the dynamically loaded script from my server and runs it? – Ben McCann Jul 21 '14 at 19:24
  • @BenMcCann You could fetch the script with `XMLHttpRequest`, store the script using [`chrome.storage`](https://developer.chrome.com/extensions/storage) and either use `eval` in the content script or [`chrome.tabs.executeScript`](https://developer.chrome.com/extensions/tabs#method-executeScript) from the background page to execute the content script. I have previously posted a concrete implementation of this concept at https://stackoverflow.com/questions/10285886/chrome-extension-adding-external-javascript-to-current-pages-html/10371025#10371025. – Rob W Jul 21 '14 at 19:26
  • Is there a way to set the inner iframe src element (src="https://robwu.nl/">) dynamically? – cnmuc Dec 08 '15 at 18:53
  • @cnmuc Yes. Put `` at the end of the document and then assign to `.src` in the script file (script.js). E.g. `document.querySelector('iframe').src = 'https://example.com';` – Rob W Dec 08 '15 at 20:19
  • Is there any way to run javascript within "frame.html"? The src for my iframe is generated programmatically and I'm having trouble setting it. – raphaeltm Jan 26 '17 at 06:40
  • @Raphaeltm Read the comment before your comment. – Rob W Jan 26 '17 at 09:52
  • Hi @RobW. I was incredibly unclear, sorry about that. The top level script would generate the url that needs to be shown, and I was wondering how best to pass that to "frame.html". Looking back at this now, I'm guessing I could just add a query string when I load frame.html and read that from the javascript in "frame.html"? – raphaeltm Feb 18 '17 at 20:35
  • @raphaeltm Yes, but only if it is acceptable that any other web page can set the URL. Otherwise you need extension messaging, to the background page and back. – Rob W Feb 19 '17 at 09:01
  • Anyone know if this workaround still works? I've been working on this for hours and for the life of me, I cannot get Chrome to stop throwing the error "Refused to frame 'https://example.com/' because it violates the following Content Security Policy directive: "child-src 'self'"." That is after loading up a local html file in the main iframe and having an iframe in that file that points to the external URL, per the instructions above. – Phil Figgins Nov 14 '17 at 01:11
  • Posted details of my similar issue at https://stackoverflow.com/questions/47395350/injecting-remote-iframe-with-chrome-extension. Hoping one of you (@RobW?) may have some insights... – Phil Figgins Nov 20 '17 at 20:38
  • postMessage from Iframe - when i try postmessage from inserted iframe-get error "Failed to execute 'postMessage' on 'DOMWindow'" i suspect most likely case is origin is chrome-extension://extension id - and because no https like url is present this is throwing this error - can you provide any help – Sandeep Chikhale Feb 28 '18 at 15:49
  • @RobW how do I hide that original Frame and show it after one of my AJAX is completed inside `script.js` which is actually inside the `frame.html` – Umair Ayub Mar 16 '18 at 10:25
  • 1
    The Chrome bug is marked as fixed with a target Chrome release v75 (which means will be release in the near future as we are at v73). – dolmen Apr 05 '19 at 13:15
  • This "bug" is indeed fixed in the latest Chrome in manifest v2 and v3. It's still strange for me why content scripts ignore all CSPs (from the target website and from manifest.json). I expected content scripts to respect CSP of the page they were injected into. – traxium Nov 05 '21 at 14:48
  • The bug is fixed but if you insert – traxium Feb 05 '23 at 08:59
7

Rob W 's answer is correct. You can follow this https://transitory.technology/browser-extensions-and-csp-headers/. I 've successfully make it work in my Chrome extension https://github.com/onmyway133/github-chat

Note that I use Chrome 59, so I can use most of ES6 features

Declare in manifest

"web_accessible_resources": [
  "iframe.html",
  "scripts/iframe.js"
]

Create an iframe in window.onload event

let url    = decodeURIComponent(window.location.search.replace('?url=', ''))
let iframe = document.createElement('iframe')
iframe.src = url

iframe.id = 'github-chat-box-iframe-inner'
iframe.style.width = '100%'
iframe.style.height = '350px'
iframe.style.border = '0px'

window.onload = () => {
  document.body.appendChild(iframe)
}
onmyway133
  • 45,645
  • 31
  • 257
  • 263
3

Your example should work in Chrome, but it currently does not because of a bug: https://code.google.com/p/chromium/issues/detail?id=408932. Rob W's answer contains a good work-around for the issue.

Macil
  • 3,575
  • 1
  • 20
  • 18
  • Note that issue is being worked on, and the issue is about frame-src CSP. – tom_mai78101 Mar 12 '19 at 15:01
  • 2
    The issue status says Fixed (https://bugs.chromium.org/p/chromium/issues/detail?id=408932#c35) - but I've just tested out on Chrome 84 - and it's still there :( – avalanche1 Jul 22 '20 at 08:11
  • For security reasons, some web servers are configured to prevent embedding into an iFrame (X-FRAME-OPTIONS). I tested with Chrome 84 with a test page (https://hndeck.sagunshrestha.com/) and is able to load the page from the injected iFrame. – Billy Aug 31 '20 at 19:30