Use JS proxies for exposing DOM to sandbox
This commit is contained in:
parent
3d002674f1
commit
1257b17ef6
3 changed files with 381 additions and 54 deletions
|
@ -1,12 +1,15 @@
|
|||
/*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright © 2009 Center for History and New Media
|
||||
Copyright © 2012 Center for History and New Media
|
||||
George Mason University, Fairfax, Virginia, USA
|
||||
http://zotero.org
|
||||
|
||||
This file is part of Zotero.
|
||||
|
||||
Portions of this file are derived from Special Powers code,
|
||||
Copyright (C) 2010 Mozilla Corporation. All Rights Reserved.
|
||||
|
||||
Zotero is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
|
@ -33,6 +36,367 @@ const BOMs = {
|
|||
|
||||
Components.utils.import("resource://gre/modules/NetUtil.jsm");
|
||||
|
||||
Zotero.Translate.DOMWrapper = new function() {
|
||||
|
||||
/*
|
||||
* BEGIN SPECIAL POWERS WRAPPING CODE
|
||||
* https://mxr.mozilla.org/mozilla-central/source/testing/mochitest/tests/SimpleTest/specialpowersAPI.js?raw=1
|
||||
*/
|
||||
function isWrappable(x) {
|
||||
if (typeof x === "object")
|
||||
return x !== null;
|
||||
return typeof x === "function";
|
||||
};
|
||||
|
||||
function isWrapper(x) {
|
||||
return isWrappable(x) && (typeof x.SpecialPowers_wrappedObject !== "undefined");
|
||||
};
|
||||
|
||||
function unwrapIfWrapped(x) {
|
||||
return isWrapper(x) ? unwrapPrivileged(x) : x;
|
||||
};
|
||||
|
||||
function isXrayWrapper(x) {
|
||||
return /XrayWrapper/.exec(x.toString());
|
||||
}
|
||||
|
||||
// We can't call apply() directy on Xray-wrapped functions, so we have to be
|
||||
// clever.
|
||||
function doApply(fun, invocant, args) {
|
||||
return Function.prototype.apply.call(fun, invocant, args);
|
||||
}
|
||||
|
||||
function wrapPrivileged(obj) {
|
||||
|
||||
// Primitives pass straight through.
|
||||
if (!isWrappable(obj))
|
||||
return obj;
|
||||
|
||||
// No double wrapping.
|
||||
if (isWrapper(obj))
|
||||
throw "Trying to double-wrap object!";
|
||||
|
||||
// Make our core wrapper object.
|
||||
var handler = new SpecialPowersHandler(obj);
|
||||
|
||||
// If the object is callable, make a function proxy.
|
||||
if (typeof obj === "function") {
|
||||
var callTrap = function() {
|
||||
// The invocant and arguments may or may not be wrappers. Unwrap them if necessary.
|
||||
var invocant = unwrapIfWrapped(this);
|
||||
var unwrappedArgs = Array.prototype.slice.call(arguments).map(unwrapIfWrapped);
|
||||
|
||||
return wrapPrivileged(doApply(obj, invocant, unwrappedArgs));
|
||||
};
|
||||
var constructTrap = function() {
|
||||
// The arguments may or may not be wrappers. Unwrap them if necessary.
|
||||
var unwrappedArgs = Array.prototype.slice.call(arguments).map(unwrapIfWrapped);
|
||||
|
||||
// Constructors are tricky, because we can't easily call apply on them.
|
||||
// As a workaround, we create a wrapper constructor with the same
|
||||
// |prototype| property.
|
||||
var FakeConstructor = function() {
|
||||
doApply(obj, this, unwrappedArgs);
|
||||
};
|
||||
FakeConstructor.prototype = obj.prototype;
|
||||
|
||||
return wrapPrivileged(new FakeConstructor());
|
||||
};
|
||||
|
||||
return Proxy.createFunction(handler, callTrap, constructTrap);
|
||||
}
|
||||
|
||||
// Otherwise, just make a regular object proxy.
|
||||
return Proxy.create(handler);
|
||||
};
|
||||
|
||||
function unwrapPrivileged(x) {
|
||||
|
||||
// We don't wrap primitives, so sometimes we have a primitive where we'd
|
||||
// expect to have a wrapper. The proxy pretends to be the type that it's
|
||||
// emulating, so we can just as easily check isWrappable() on a proxy as
|
||||
// we can on an unwrapped object.
|
||||
if (!isWrappable(x))
|
||||
return x;
|
||||
|
||||
// If we have a wrappable type, make sure it's wrapped.
|
||||
if (!isWrapper(x))
|
||||
throw "Trying to unwrap a non-wrapped object!";
|
||||
|
||||
// Unwrap.
|
||||
return x.SpecialPowers_wrappedObject;
|
||||
};
|
||||
|
||||
function crawlProtoChain(obj, fn) {
|
||||
var rv = fn(obj);
|
||||
if (rv !== undefined)
|
||||
return rv;
|
||||
if (Object.getPrototypeOf(obj))
|
||||
return crawlProtoChain(Object.getPrototypeOf(obj), fn);
|
||||
};
|
||||
|
||||
|
||||
function SpecialPowersHandler(obj) {
|
||||
this.wrappedObject = obj;
|
||||
};
|
||||
|
||||
// Allow us to transitively maintain the membrane by wrapping descriptors
|
||||
// we return.
|
||||
SpecialPowersHandler.prototype.doGetPropertyDescriptor = function(name, own) {
|
||||
|
||||
// Handle our special API.
|
||||
if (name == "SpecialPowers_wrappedObject")
|
||||
return { value: this.wrappedObject, writeable: false, configurable: false, enumerable: false };
|
||||
|
||||
// In general, we want Xray wrappers for content DOM objects, because waiving
|
||||
// Xray gives us Xray waiver wrappers that clamp the principal when we cross
|
||||
// compartment boundaries. However, Xray adds some gunk to toString(), which
|
||||
// has the potential to confuse consumers that aren't expecting Xray wrappers.
|
||||
// Since toString() is a non-privileged method that returns only strings, we
|
||||
// can just waive Xray for that case.
|
||||
var obj = name == 'toString' ? XPCNativeWrapper.unwrap(this.wrappedObject)
|
||||
: this.wrappedObject;
|
||||
|
||||
//
|
||||
// Call through to the wrapped object.
|
||||
//
|
||||
// Note that we have several cases here, each of which requires special handling.
|
||||
//
|
||||
var desc;
|
||||
|
||||
// Case 1: Own Properties.
|
||||
//
|
||||
// This one is easy, thanks to Object.getOwnPropertyDescriptor().
|
||||
if (own)
|
||||
desc = Object.getOwnPropertyDescriptor(obj, name);
|
||||
|
||||
// Case 2: Not own, not Xray-wrapped.
|
||||
//
|
||||
// Here, we can just crawl the prototype chain, calling
|
||||
// Object.getOwnPropertyDescriptor until we find what we want.
|
||||
//
|
||||
// NB: Make sure to check this.wrappedObject here, rather than obj, because
|
||||
// we may have waived Xray on obj above.
|
||||
else if (!isXrayWrapper(this.wrappedObject))
|
||||
try {
|
||||
desc = crawlProtoChain(obj, function(o) {return Object.getOwnPropertyDescriptor(o, name);});
|
||||
} catch(e) {
|
||||
// we hit bug 560072 if DOM is not wrapped
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=560072
|
||||
if (name in obj) {
|
||||
// same guess as below
|
||||
desc = {value: obj[name], writable: false, configurable: true, enumerable: true};
|
||||
}
|
||||
}
|
||||
|
||||
// Case 3: Not own, Xray-wrapped.
|
||||
//
|
||||
// This one is harder, because we Xray wrappers are flattened and don't have
|
||||
// a prototype. Xray wrappers are proxies themselves, so we'd love to just call
|
||||
// through to XrayWrapper<Base>::getPropertyDescriptor(). Unfortunately though,
|
||||
// we don't have any way to do that. :-(
|
||||
//
|
||||
// So we first try with a call to getOwnPropertyDescriptor(). If that fails,
|
||||
// we make up a descriptor, using some assumptions about what kinds of things
|
||||
// tend to live on the prototypes of Xray-wrapped objects.
|
||||
else {
|
||||
desc = Object.getOwnPropertyDescriptor(obj, name);
|
||||
if (!desc) {
|
||||
var getter = Object.prototype.__lookupGetter__.call(obj, name);
|
||||
var setter = Object.prototype.__lookupSetter__.call(obj, name);
|
||||
if (getter || setter)
|
||||
desc = {get: getter, set: setter, configurable: true, enumerable: true};
|
||||
else if (name in obj)
|
||||
desc = {value: obj[name], writable: false, configurable: true, enumerable: true};
|
||||
}
|
||||
}
|
||||
|
||||
// Bail if we've got nothing.
|
||||
if (typeof desc === 'undefined')
|
||||
return undefined;
|
||||
|
||||
// When accessors are implemented as JSPropertyOps rather than JSNatives (ie,
|
||||
// QuickStubs), the js engine does the wrong thing and treats it as a value
|
||||
// descriptor rather than an accessor descriptor. Jorendorff suggested this
|
||||
// little hack to work around it. See bug 520882.
|
||||
if (desc && 'value' in desc && desc.value === undefined)
|
||||
desc.value = obj[name];
|
||||
|
||||
// A trapping proxy's properties must always be configurable, but sometimes
|
||||
// this we get non-configurable properties from Object.getOwnPropertyDescriptor().
|
||||
// Tell a white lie.
|
||||
desc.configurable = true;
|
||||
|
||||
// Transitively maintain the wrapper membrane.
|
||||
function wrapIfExists(key) { if (key in desc) desc[key] = wrapPrivileged(desc[key]); };
|
||||
wrapIfExists('value');
|
||||
wrapIfExists('get');
|
||||
wrapIfExists('set');
|
||||
|
||||
return desc;
|
||||
};
|
||||
|
||||
SpecialPowersHandler.prototype.getOwnPropertyDescriptor = function(name) {
|
||||
return this.doGetPropertyDescriptor(name, true);
|
||||
};
|
||||
|
||||
SpecialPowersHandler.prototype.getPropertyDescriptor = function(name) {
|
||||
return this.doGetPropertyDescriptor(name, false);
|
||||
};
|
||||
|
||||
function doGetOwnPropertyNames(obj, props) {
|
||||
|
||||
// Insert our special API. It's not enumerable, but getPropertyNames()
|
||||
// includes non-enumerable properties.
|
||||
var specialAPI = 'SpecialPowers_wrappedObject';
|
||||
if (props.indexOf(specialAPI) == -1)
|
||||
props.push(specialAPI);
|
||||
|
||||
// Do the normal thing.
|
||||
var flt = function(a) { return props.indexOf(a) == -1; };
|
||||
props = props.concat(Object.getOwnPropertyNames(obj).filter(flt));
|
||||
|
||||
// If we've got an Xray wrapper, include the expandos as well.
|
||||
if ('wrappedJSObject' in obj)
|
||||
props = props.concat(Object.getOwnPropertyNames(obj.wrappedJSObject)
|
||||
.filter(flt));
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
SpecialPowersHandler.prototype.getOwnPropertyNames = function() {
|
||||
return doGetOwnPropertyNames(this.wrappedObject, []);
|
||||
};
|
||||
|
||||
SpecialPowersHandler.prototype.getPropertyNames = function() {
|
||||
|
||||
// Manually walk the prototype chain, making sure to add only property names
|
||||
// that haven't been overridden.
|
||||
//
|
||||
// There's some trickiness here with Xray wrappers. Xray wrappers don't have
|
||||
// a prototype, so we need to unwrap them if we want to get all of the names
|
||||
// with Object.getOwnPropertyNames(). But we don't really want to unwrap the
|
||||
// base object, because that will include expandos that are inaccessible via
|
||||
// our implementation of get{,Own}PropertyDescriptor(). So we unwrap just
|
||||
// before accessing the prototype. This ensures that we get Xray vision on
|
||||
// the base object, and no Xray vision for the rest of the way up.
|
||||
var obj = this.wrappedObject;
|
||||
var props = [];
|
||||
while (obj) {
|
||||
props = doGetOwnPropertyNames(obj, props);
|
||||
obj = Object.getPrototypeOf(XPCNativeWrapper.unwrap(obj));
|
||||
}
|
||||
return props;
|
||||
};
|
||||
|
||||
SpecialPowersHandler.prototype.defineProperty = function(name, desc) {
|
||||
return Object.defineProperty(this.wrappedObject, name, desc);
|
||||
};
|
||||
|
||||
SpecialPowersHandler.prototype.delete = function(name) {
|
||||
return delete this.wrappedObject[name];
|
||||
};
|
||||
|
||||
SpecialPowersHandler.prototype.fix = function() { return undefined; /* Throws a TypeError. */ };
|
||||
|
||||
// Per the ES5 spec this is a derived trap, but it's fundamental in spidermonkey
|
||||
// for some reason. See bug 665198.
|
||||
SpecialPowersHandler.prototype.enumerate = function() {
|
||||
var t = this;
|
||||
var filt = function(name) { return t.getPropertyDescriptor(name).enumerable; };
|
||||
return this.getPropertyNames().filter(filt);
|
||||
};
|
||||
/*
|
||||
* END SPECIAL POWERS WRAPPING CODE
|
||||
*/
|
||||
|
||||
/**
|
||||
* Abstracts DOM wrapper support for avoiding XOWs<br/>
|
||||
* In Firefox 3.6, we use FX36DOMWrapper, defined below<br/>
|
||||
* In Firefox 4+, we use some proxy code taken from Special Powers
|
||||
* @param {XPCCrossOriginWrapper} obj
|
||||
* @return {Object} An obj that is no longer Xrayed
|
||||
*/
|
||||
this.wrap = function(obj) {
|
||||
if(Zotero.isFx4) {
|
||||
Zotero.debug(obj.toString());
|
||||
var newObj = wrapPrivileged(obj);
|
||||
return newObj;
|
||||
} else {
|
||||
return _wrapFx36(obj);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwraps an object
|
||||
*/
|
||||
this.unwrap = function(obj) {
|
||||
if("__wrappedDOMObject" in obj) {
|
||||
return obj.__wrappedDOMObject;
|
||||
} else if(isWrapper(obj)) {
|
||||
return unwrapPrivileged(obj);
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an object is wrapped by a DOM wrapper
|
||||
* @param {XPCCrossOriginWrapper} obj
|
||||
* @return {Boolean} Whether or not the object is wrapped
|
||||
*/
|
||||
this.isWrapped = function(obj) {
|
||||
return "__wrappedDOMObject" in obj || isWrapper(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* A really ugly way of making a DOM object not look like a DOM object, so we can pass it to the
|
||||
* sandbox under Firefox 3.6
|
||||
* @param {XPCCrossOriginWrapper} obj
|
||||
* @param {Object} parent A parent to use as |this| when applying functions
|
||||
* @return {Object} An obj that is no longer Xrayed
|
||||
*/
|
||||
function _wrapFx36(obj, parent) {
|
||||
if(obj === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var wrapFx36 = arguments.callee;
|
||||
var type = typeof obj;
|
||||
if(type === "function") {
|
||||
var val = function() {
|
||||
var nArgs = arguments.length;
|
||||
var args = new Array(nArgs);
|
||||
for(var i=0; i<nArgs; i++) {
|
||||
args[i] = (arguments[i] instanceof Object && arguments[i].__wrappedDOMObject
|
||||
? arguments[i].__wrappedDOMObject : arguments[i]);
|
||||
}
|
||||
return wrapFx36(obj.apply(parent ? parent : null, args));
|
||||
}
|
||||
} else if(type === "object") {
|
||||
if(val instanceof Array) {
|
||||
var val = [];
|
||||
} else {
|
||||
var val = {};
|
||||
}
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
|
||||
val.__wrappedDOMObject = obj;
|
||||
val.__exposedProps__ = {};
|
||||
for(var prop in obj) {
|
||||
let localProp = prop;
|
||||
val.__exposedProps__[localProp] = "r";
|
||||
val.__defineGetter__(localProp, function() {
|
||||
return wrapFx36(obj[localProp], obj);
|
||||
});
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @class Manages the translator sandbox
|
||||
* @param {Zotero.Translate} translate
|
||||
|
@ -60,13 +424,20 @@ Zotero.Translate.SandboxManager = function(sandboxLocation) {
|
|||
var uri = sandboxLocation.location.toString();
|
||||
}
|
||||
|
||||
// get principal from URI
|
||||
var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
|
||||
.getService(Components.interfaces.nsIScriptSecurityManager);
|
||||
// get from nsIURI
|
||||
var ioService = Components.classes["@mozilla.org/network/io-service;1"]
|
||||
.getService(Components.interfaces.nsIIOService);
|
||||
uri = ioService.newURI(uri, "UTF-8", null);
|
||||
var principal = secMan.getCodebasePrincipal(uri);
|
||||
|
||||
if(typeof sandboxLocation === "object" && sandboxLocation.nodePrincipal && Zotero.isFx4) {
|
||||
// if sandbox specified by DOM document, use nodePrincipal property
|
||||
var principal = sandboxLocation.nodePrincipal;
|
||||
} else {
|
||||
// if sandbox specified by URI, get codebase principal from security manager
|
||||
var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
|
||||
.getService(Components.interfaces.nsIScriptSecurityManager);
|
||||
var principal = secMan.getCodebasePrincipal(uri);
|
||||
}
|
||||
|
||||
// initialize DOM parser
|
||||
var _DOMParser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
|
||||
|
@ -77,7 +448,7 @@ Zotero.Translate.SandboxManager = function(sandboxLocation) {
|
|||
this.__exposedProps__ = {"parseFromString":"r"};
|
||||
if(Zotero.isFx5) {
|
||||
this.parseFromString = function(str, contentType) {
|
||||
return Zotero.Translate.SandboxManager.Fx5DOMWrapper(_DOMParser.parseFromString(str, contentType));
|
||||
return Zotero.Translate.DOMWrapper.wrap(_DOMParser.parseFromString(str, contentType));
|
||||
}
|
||||
} else {
|
||||
this.parseFromString = function(str, contentType) _DOMParser.parseFromString(str, contentType);
|
||||
|
@ -88,50 +459,6 @@ Zotero.Translate.SandboxManager = function(sandboxLocation) {
|
|||
this.sandbox.DOMParser.prototype = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* A really ugly way of making a DOM object not look like a DOM object, so we can pass it to the
|
||||
* sandbox under Firefox 5
|
||||
*/
|
||||
Zotero.Translate.SandboxManager.Fx5DOMWrapper = function(obj, parent) {
|
||||
if(obj === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var type = typeof obj;
|
||||
if(type === "function") {
|
||||
var me = this;
|
||||
var val = function() {
|
||||
var nArgs = arguments.length;
|
||||
var args = new Array(nArgs);
|
||||
for(var i=0; i<nArgs; i++) {
|
||||
args[i] = (arguments[i] instanceof Object && arguments[i].__wrappedDOMObject
|
||||
? arguments[i].__wrappedDOMObject : arguments[i]);
|
||||
}
|
||||
return Zotero.Translate.SandboxManager.Fx5DOMWrapper(obj.apply(parent ? parent : null, args));
|
||||
}
|
||||
} else if(type === "object") {
|
||||
if(val instanceof Array) {
|
||||
var val = [];
|
||||
} else {
|
||||
var val = {};
|
||||
}
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
|
||||
val.__wrappedDOMObject = obj;
|
||||
val.__exposedProps__ = {};
|
||||
for(var prop in obj) {
|
||||
let localProp = prop;
|
||||
val.__exposedProps__[localProp] = "r";
|
||||
val.__defineGetter__(localProp, function() {
|
||||
return Zotero.Translate.SandboxManager.Fx5DOMWrapper(obj[localProp], obj);
|
||||
});
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
Zotero.Translate.SandboxManager.prototype = {
|
||||
/**
|
||||
* Evaluates code in the sandbox
|
||||
|
|
|
@ -924,8 +924,8 @@ Zotero.Utilities = {
|
|||
var element = elements[i];
|
||||
|
||||
// Firefox 5 hack, so we will preserve Fx5DOMWrappers
|
||||
var useFx5DOMWrapper = !!element.__wrappedDOMObject;
|
||||
if(useFx5DOMWrapper) element = element.__wrappedDOMObject;
|
||||
var isWrapped = Zotero.Translate.DOMWrapper && Zotero.Translate.DOMWrapper.isWrapped(element);
|
||||
if(isWrapped) element = Zotero.Translate.DOMWrapper.unwrap(element);
|
||||
|
||||
if(element.ownerDocument) {
|
||||
var rootDoc = element.ownerDocument;
|
||||
|
@ -946,7 +946,7 @@ Zotero.Utilities = {
|
|||
var newEl;
|
||||
while(newEl = xpathObject.iterateNext()) {
|
||||
// Firefox 5 hack
|
||||
results.push(useFx5DOMWrapper ? Zotero.Translate.SandboxManager.Fx5DOMWrapper(newEl) : newEl);
|
||||
results.push(isWrapped ? Zotero.Translate.DOMWrapper.wrap(newEl) : newEl);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -248,7 +248,7 @@ Zotero.Utilities.Translate.prototype.processDocuments = function(urls, processor
|
|||
var newLoc = doc.location;
|
||||
if(Zotero.isFx && (protocol != newLoc.protocol || host != newLoc.host)) {
|
||||
// Cross-site; need to wrap
|
||||
processor(Zotero.Translate.SandboxManager.Fx5DOMWrapper(doc), newLoc.toString());
|
||||
processor(Zotero.Translate.DOMWrapper.wrap(doc), newLoc.toString());
|
||||
} else {
|
||||
// Not cross-site; no need to wrap
|
||||
processor(doc, newLoc.toString());
|
||||
|
|
Loading…
Add table
Reference in a new issue