Firefox WebExtension, isolated HTML overlay
J

2

6

I'm looking for a way to display an isolated overlay on some websites using WebExtensions.
An iframe would seem like the way to go for this as it provides a whole separate scope for css, js and the DOM. And another neat thing is that the target website won't be able to read or change the content.

In Chrome extensions that seems to be possible without any problems, but with WebExtensions in Firefox, even though they share the same syntax, I get security warnings/errors and it doesn't work.

I've tried two different things:

  • Creating an iframe without an src attribute and inject that into the body of website. This method failed because I get CSP errors/warnings when I do iframe.contentWindow.document.open().
    Relevant content-script code:

    let html = `
        <!DOCTYPE html>
        <html>
          <head></head>
          <body>
            <h1>TEST</h1>
          </body>
        </html>
    `
    let iframe = document.createElement('iframe')
    document.body.appendChild(iframe)
    iframe.contentWindow.document.open()
    iframe.contentWindow.document.write(html)
    iframe.contentWindow.document.close()
    
  • The other thing I tried (which would make way more sense as it would disallow the website from accessing the content), was to put my iframe code into a file (overlay.html) in my WebExtension and make the iframe load it by setting it's src to browser.extension.getURL('overlay.html').
    Relevant content-script code:

    let iframe = document.createElement('iframe')
    iframe.src = browser.extension.getURL('overlay.html')
    document.body.appendChild(iframe)
    

    In the manifest I defined the overlay.html as web_accessible_resources for this:

    "web_accessible_resources": [ 
        "overlay.html"
    ],
    

    The thing about that is that the iframe simply doesn't load the overlay.html file. But it is definitely available, otherwise location.href = browser.extension.getURL('overlay.html') wouldn't work. It would have been extremely convenient if that would have worked, as I could have stored the whole overlay (html, css, js) as separate files in my extension. As if it would be a standalone website. And using the content-script I could have accessed it to add new data or whatever.

Edit:

At the moment I'm using the srcdoc attribute of my iframe to set the source code it should contain (thanks to wOxxOm for that). So at least I have something that works now. But what I really dislike about this approach is that I can't interact with the iframe content from my content script. Interestingly though, I can interact with the parent page from within the iframe's js code, but again not with the content script. It's also really messy, inconvenient and hard to maintain to put all your html, css, js code into one string instead of multiple files like a normal website.

Jennyjeno answered 19/1, 2017 at 20:39 Comment(6)
Maybe the sites you've tried define a very restrictive CSP? I'd try iframe.srcdoc. As for style isolation you can use Shadow DOM instead of iframe.Oratory
Thank you this was incredibly helpful!Jennyjeno
1) About the CSP issue: I don't think it's the site. It gives the same error for me on file:///C:/. I think it's related to the extension's own CSP. I'm still unfamiliar with the syntax, so can't offer a solution. Be cautious poking holes without fully understanding what else you might be inadvertently allowing. 2) I created then immediately appended iframe. Direct assignment does NOT work equally for all attributes, although iframe.src did work for me. Try specifying ALL attributes with iframe.setAttribute( attribute, value ); Specifically, I think class is problematic.Cerargyrite
(continued) However, relying upon a class attribute set on the iframe may lead to the risk of class collision problems that the Shadow DOM technique attempts to minimize. This technique is new to me so I can't comment further without research.Cerargyrite
Actually, iframes should probably be avoided entirely by WebExtensions. Even if the site is trusted now, all an attacker has to do is inject code into the page and they can hide or overlay the iframe or change the src attribute. bugzilla.mozilla.org/show_bug.cgi?id=1287590 Not sure if DOM shadow adequately protects against such things.Cerargyrite
Without knowing your exact security problems, I'd wager that this is the CSP complaining that your iframe is trying to access a resource over something other than https.Docilla
D
3

The Problem

Firstly, we know the problem is a Security Issue, what exactly is the issue? Well when trying to load an extension resource into an iframe by setting the iframe src the browser complains about Security and prevents the iframe from connecting over a different protocol, in this instance 'moz-extension://'.

The Solution

Load the html etc from the extension context and inject as a string.

The Nitty Gritty

To get around this, we can set the src attribute of the iframe to data:text/html;charset=utf8,${markup}.

This directly tells the iframe that the content is html, it uses utf8 encoding and it is followed by the raw markup. We're completely bypassing the need for the iframe to load any resources over the network.

The execution context of a Firefox content script is seperate from the page it has been loaded for. This means that you can make an xhr request without violating CSP.

If you make an xhr request to your markup, you can then get the content of the response as a string, and directly inject it into the iframe src attribute.

Thus the content script:

    function loaded (evt) {
    if (this.readyState === 4 && this.status === 200) {
        var html = this.responseText;
        console.log(html);
        var iframe = document.createElement('iframe');

        iframe.src = 'data:text/html;charset=utf-8,' + html;
        document.body.appendChild(iframe);

        console.log('iframe.contentWindow =', iframe.contentWindow);
    } else {
        console.log('problem loading');
    }
}

var xhr = new XMLHttpRequest();
xhr.overrideMimeType("text/html");
xhr.open("GET", browser.extension.getURL('test.html'), false);
xhr.addEventListener("readystatechange", loaded);


xhr.send(null);

With a simple HTML file

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Test</title>
    <meta charset="utf8" />
</head>
<body>
   <h1>Hello World</h1>
</body>
</html>

So now you have successfully injected a html template into the target iframe.

If you need any images, scripts, css files etc, you'll need to write a bootloader based on the method outlined above, injecting new script tags and so forth directly into the iframe document.

Docilla answered 25/1, 2017 at 11:39 Comment(0)
S
0

btw, sometimes people use IFRAME url in different protocol, i.e.
https:// (while current page is http:// ).

So, IFRAME url should be relative, like src="//example.com"

Salzhauer answered 28/1, 2017 at 19:43 Comment(1)
From the context of a firefox extension accessing files from the extensions file system and not the website context, it won't work. firefox extension files reside on the moz-extension protocol.Docilla

© 2022 - 2024 — McMap. All rights reserved.