Firefox Addon SDK: Loading addon file into iframe

2019-01-18 00:14发布

问题:

I want to load a resource:// link, respectively a local file from my Firefox addon into an iframe in a web page.

The reason is, that the resource should be visually embedded into the web page while not giving the website access to it's DOM for security reasons.

The issue has been discussed in various places in the past, e.g. here (without solution): https://bugzilla.mozilla.org/show_bug.cgi?id=792479

As most of the postings are rather old, I want to ask, if in the meantime there are any new solutions or workarounds.

回答1:

I think I suggested in the bug or in the ML of jetpack a terrible workaround, that basically is convert your resource:// in a data: url (using data.load to load the HTML content, and then encode and append as prefix, so something like that should works:

/* main.js */
const { data } = require('sdk/self');

// just an example, you can use `tab.attach` too
require('sdk/page-mod').PageMod({
  include: '*',
  contentScriptFile: data.url('content.js'),
  contentScriptOptions: {
    content: encodeURIComponent(data.load('index.html'))
  }
});

/* content.js */
let iframe = document.body.appendChild(document.createElement('iframe'));

iframe.setAttribute('sandbox', 'allow-scripts');
// maybe you want also use the seamless attribute, see:
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe

iframe.contentWindow.location.href = 'data:text/html;charset=utf-8,' + self.options.content;

It's a workaround of course, and I hope that the bug you mentioned will be fixed soon. Notice that in this way you cannot communicate directly from the iframe to the parent's document, but it means also that you can't do the way around, that is what you want to prevent.

Of course, you can still use the add-on code to communicate between your iframe and the parent's document (you need to attach content scripts and use port and postMessage).

Edit: changed the way the url is set, otherwise getting the src attribute from the parent's document is still possible, and contains whole HTML.



回答2:

I used a solution proposed here.

It is implemented by creating a special protocol that is handled by your Firefox addon, which in turn requests the resources from its folder.
Note that in case the resource folder may contain something non-public then I would add additional checks to allow only these resources that really are intended to be web-accessible.

The custom-protocol code attached to the post mentioned above is available here:

/*
Makes any file within the data directory available to use in an iframe.
Replace this: require("sdk/self").data.url(...)
With this: require("name-of-this-file").url(...)
*/
var { Class } = require('sdk/core/heritage');
var { Unknown, Factory } = require('sdk/platform/xpcom');
var { Cc, Ci, Cr } = require('chrome');
var self = require("sdk/self");

var resourceProtocolHandler = Cc["@mozilla.org/network/io-service;1"]
    .getService(Ci.nsIIOService)
    .getProtocolHandler('resource');

var scheme = "res-" + self.id.toLowerCase().replace(/[^a-z0-9+\-\.]/g, "-");

var AddonProtocolHandler = Class({
    extends: Unknown,
    interfaces: ['nsIProtocolHandler'],

    scheme: scheme,
    defaultPort: -1,
    protocolFlags: Ci.nsIProtocolHandler.URI_STD
        | Ci.nsIProtocolHandler.URI_LOADABLE_BY_ANYONE
        | Ci.nsIProtocolHandler.URI_SAFE_TO_LOAD_IN_SECURE_CONTEXT,

    newURI: function(spec, originCharset, baseURI) {
        let uri = Cc["@mozilla.org/network/standard-url;1"].createInstance(Ci.nsIStandardURL);
        uri.init(uri.URLTYPE_STANDARD, this.defaultPort, spec, originCharset, baseURI);
        return uri.QueryInterface(Ci.nsIURI);
    },

    newChannel: function(uri) {
        if (uri.spec.indexOf(exports.url("")) != 0) {
            throw Cr.NS_ERROR_ILLEGAL_VALUE;
        }
        var resourceUri = resourceProtocolHandler.newURI(uri.spec.replace(scheme + "://", "resource://"), uri.originCharset, null);
        var channel = resourceProtocolHandler.newChannel(resourceUri);
        channel.originalURI = uri;
        return channel;
    },

    allowPort: (port, scheme) => false
});

Factory({
    contract: "@mozilla.org/network/protocol;1?name=" + scheme,
    Component: AddonProtocolHandler
});

exports.url = function(url) {
    return self.data.url(url).replace("resource://", scheme + "://");
};

Additional note: for page scripts (not content scripts) this custom protocol helps in loading an iframe and other HTML elements, but not in loading a XMLHttpRequest or Worker. For the latter, still cross-origin restrictions apply and a security error is raised: "Access to restricted URI denied" in case of XMLHttpRequest, and "The operation is insecure." for Worker.
In contrast, under Chrome the XMLHttpRequest to web_accessible_resources is permitted. Worker does not implement access to web_accessible_resources under Chrome either.
BUT for Workers you can use this custom-protocol url for importScripts method. Using that you can also work around the problem of loading custom resources into Workers. This code works also in Chrome, as an alternative to using XMLHttpRequest.

var code = "self.onmessage = function (message)\
{\
    self.onmessage = null;\
    self.importScripts(message.data);\
};";

var blob = new Blob([code], {type: 'application/javascript'});
var blobUrl = URL.createObjectURL(blob);
var w = new Worker(blobUrl);
w.postMessage(*webAccessibleResourceUrl*);
URL.revokeObjectURL(blobUrl);


回答3:

I'm using zer0's idea. However, on pages with a content-security-policy I got an exception (...result = 2153644038). For that reason, I add my domain to the content-security-policy of the response header:

//main.js 
var { Cc, Ci } = require('chrome');

var observer = {
    observe : function(aSubject, aTopic, aData) {
      if (aTopic == "http-on-examine-response") {
        aSubject.QueryInterface(Ci.nsIHttpChannel);
        var csp = aSubject.getResponseHeader("content-security-policy");
        if(csp.indexOf('frame-src') > -1) {
            var cspParts = csp.split(';');
            for (var i=0; i<cspParts.length; i++) {
                if(cspParts[i].indexOf('frame-src') > -1) {
                    cspParts[i] += ' yourdomain.tld';
                    break;
                }
            }
            aSubject.setResponseHeader("content-security-policy", cspParts.join(';'), false);
        } 
      }
    }
}; 
var observerService = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
observerService.addObserver(observer, "http-on-examine-response", false);

//content-script.js 
iframe.src = yourdomain.tld/...

EDIT: This code might be rejected by a FF reviewer with: "Modifying 'content-security-policy' is not allowed for security reasons."