I'm working on a knowledge management platform for organizations.
Check it out

Wrap a website with a Webextension

I've tried to wrap any website into my own interface for the last couple of days, and that revealed itself to be a very painful experience. Actually I haven't managed to get it working as well as I wanted. So I want to describe all things that I tried in case in the future I want to attack the problem again

Here is an example of what I'm trying to achieve: example

Basically, I want to be able to show a search bar and a sidebar (wrapping content) on any website (page).

How to do that

There are only three ways that I'm aware of:

  1. Inject the page into an iframe on a domain you control

  2. Use a Webextension, and inject some css and js, to add padding to the body of the page and insert your wrapping content as fixed element

  3. Use a Webextension, and replace the html of the page with your wrapping content plus an iframe to the current page

Inject the page into an iframe

So that's the easiest solution of all and you don't need an extension. But :

  1. Some websites prevent to be opened from an iframe via the X-FRAME-OPTIONS header

  2. Some websites use SameSite=Strict cookies which are not sent when the main frame domain is not the same as the one of the page (cross origin)

  3. Some websites try to communicate with the parent frame and for example reload itself in the parent frame

The 1. can be bypassed by using an extension, basically you just need to subscribe to the onbeforerequest event and filter out the x-frame-options header. We do not really impact the security of the page doing so

The 2. can be bypassed by using an extension and rewriting the cookies and removing the SameSite attributes, but I have not tried it, I'm sure other issues will arise, and we will impact the page from working properly in some cases. We also introduce a new CSRF security threat

I haven't experienced 3. for now, but in the past I have worked with websites that did it, and it was hell. Actually if you use an extension you could use the sandbox attributes and other allow parameters excluding the 'allow-top-navigation' parameters. I'll document it here if I happen to have the issue

Overall that was the first solution tested, which was easy but not enough to support all our use cases

Inject wrapping content into the page

What you need to do is to inject some CSS to add paddings to the body of the page: body{padding-top:55px;padding-left:200px} which will shift the page a bit down and to the right. And then inject a content-script (run_at document_end ) which will render your wrapping content. Here is the issue you will have to deal with:

  1. The CSP of the page may prevent you to inject your own CSS or own js

  2. The page may render itself comparing to the window size

The 1. is easy to bypass, you just need to subscribe to onbeforerequest, and remove the content-security-policy header. If you want to respect the security of the website, instead of removing the CSP, you can modify it, to allow also your script and css, if you want to use some inline script, you will have to use a nonce.

The 2. is a much bigger issue. And actually I don't think there is any solution to fix it. If you try to add padding on Google docs for example, it just doesn't work. So that technique only works for basic websites that don't use JS to place elements.

I've given up that way as I'm working mostly with app that use a ton of JS

Replace the page with the wrapping content + an iframe to self

Just to explain in an example what that is. Imagine you want to wrap https://domain.com/path. Then you will let the page load. When the page finishes loading, you replace the whole page with

document.body.parentElement.innerHTML=`
    <html>
        <head></head>
        <body>
            <div>
                <wrapping-content></wrapping-content>
                <iframe src="https://domain.com/path"></iframe>
            </div>
        </body>
    </html>`

That's how most 'responsive viewer extensions' work. The benefits is :

  • Don't have to deal with cookie as you are in the same origin setup

  • Don't deal with the page content, the page render itself into an iframe

  • Your wrapper is kinda isolated form the iframe as you can clear the initial page if you want with document.open();document.close()

But there are also tons of issues:

  1. You lose the favicon and the title of the page

  2. You render the page two times and so it is slow

  3. If the page use service workers and all those new things they will wonder why the page is not rendering as expected

  4. You still have the issue of the iframe trying to communicate with the parent

  5. You still have the CSP issue

  6. Firefox and Chrome don't work the same and you have to do code for each browser

For 1. you can wait for the html to be rendered, query for the title tag and the favicon and then insert it into your wrapping content.

For 2. to make it fast, as there is no way to modify the initial html page, you have to let the browser download and render it. But you have to do it in the fastest way possible. I found two ways to do it:

  • Generate a CSP that will prevent anything to be loaded except when ressources come from your domain. That will also prevent inline script execution and ajax requests to be sent. I haven't figured out if this works well with service workers, even when you include the connect-src directive. You need to add a nonce to the favicon if you want it to works

  • Returns {cancel:true} in onbeforerequest when the parentFrameId is -1 and the url is the one you want to intercept. The issue is that on Chrome you don't have access to the page url that made the request. Only to the origin. You have to first cache it. The issue is that inline scripts will still be executed and service workers too. I had less success with this option than the CSP one

For 3. I haven't completely succeeded in fixing that. On Google docs for example the second times I reload the page on Firefox, there is an alert that's popping to tell me there was an issue

For 4. Same as previously

For 5. Same as previously easy to bypass

For 6. For example, if you want to override the page on Chrome, you can use document.open();document.close() from a content script run at "document_start" but on Firefox it doesn't work. Content scripts don't seem to have access to document.open for security reasons. But you can use window.eval('document.open()') which works, but in that case you need to have the CSP unsafe-eval allowed. Or you can insert your content script on window.onload or run the contentscript at document_end, but you lose the duration it takes to render the initial html page. Another issue I had, is that for some reason overriding the CSP doesn't work well when you have UblockOrigin installed on Firefox, the two overrides seem to conflict with each other.

The result is pretty fast, the only page you load twice is the initial html page, so you lose the ping of the first page.

Conclusion

That was overall a pretty stressful experiment. And I'm not 100% happy with the result, the last way seems to work more or less fine, but there are still times when it bugs. It also feels pretty hackish and I'm scared that any new version of Firefox or Chrome may break it. I will maybe try to adapt the UX so that instead of having a wrapper, I inject an overlay. That way I can modify the CSP, add some js and some css and render into the page DOM.

That's also a good reminder that when you intall an extension. This extension can almost do anything it wants with what you do in your browser, so be carefull !

And if you want to tell me that what I have been doing is a stupid idea, do not hesitate to send me an email :)