Skip to content

Safely hooking the loader of an iframe #155

@andyearnshaw

Description

@andyearnshaw

I'd like to put this idea forward to get some opinions about it. I work in the ad-serving industry, where I manage scripts that deliver ads to pages (as cross-origin iframes) and libraries for creative developers. My involvement in both these areas drives me to try and improve how the 2 things work together.

Being able to hook parts of the loader for one of these iframes would allow me to provide a lot of convenience for creative developers. This is an off-shoot of a similar problem to the one I talk about in whatwg/html#2161.

Probably oversimplified example:

Parent script

const API_BASE = 'http://location.of/widgetapi';

class Widget {
    constructor(manifest) {
        this.iframe = document.createElement('iframe');
        this.iframe.src = manifest.url;

        // Hook the iframe loader to provide "built-in" modules to the widget
        this.iframe.loader.hook('@@widget/api', {
            resolve: () => `${ API_BASE }/${ manifest.apiVersion }/api.js`
        });
        this.iframe.loader.hook('@@widget/manifest', {
            fetch: () => `export default ${ JSON.stringify(manifest) }`
        });
        this.iframe.loader.hook('@@widget/foo-component', {
            resolve: () => `${ API_BASE }/${ manifest.apiVersion }/components/foo.js`
        });
    }
    appendTo(el) {
        el.appendChild(this.iframe);
    }
}

let widget = new Widget({
        name: 'widgetA',
        url: 'http://some.other.com/widgets/widgetA.html',
        apiVersion: '^1.0.0'
    });

Iframe snippet

<script type="module">
    import api from '@@widget/api';
    import Foo from '@@widget/foo-component';

    let foo = new Foo();
    api.getLoggedInUser()
        .then((user) => foo.bar = user);
</script>

This provides a nice application environment for the widget developer without them needing to worry about implementation details. The app environment can give arbitrary data—that it, potentially, already has—to the widget without needing to write clunky message send/receive callbacks or force the widget to make a separate HTTP request for the data.

Other, potentially real-world examples:

Providing an API to advertising creative developers (my main use case):

import { expand } from '@@creative-api';
cta.onclick = () => expand().then(showPage2);

eBay could provide an API to sellers for their item descriptions (eBay forbid external scripts in descriptions to prevent user tracking/abuse)

import eBay from '@@eBay';
eBay.getMyOtherItems().then(renderCarousel);

Hosted apps/games on facebook no longer require special tokens for interacting with the API:

import FB from '@@facebook';
FB.getUser().then(...);

Security

Naturally, security is going to be a concern. Being able to hook and map a window's module loader is potentially dangerous. Concerns can, hopefully, be alleviated by the following restrictions:

  • instantiate cannot be hooked, so objects cannot be passed between origins. This restriction could potentially be lifted for Transferable data (e.g. buffers) to be returned, but throw on other kinds of data.
  • An unambiguous prefix could precede the module identifier. This makes the intent explicit and precludes any attempt of overriding modules that the child might try to request. In the examples above, I used the (admittedly ugly) prefix @@.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions