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.
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.
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);
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."