/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
Services.prefs.setBoolPref(
"security.allow_eval_with_system_principal",
true);
registerCleanupFunction(() => {
Services.prefs.clearUserPref(
"security.allow_eval_with_system_principal");
});
// Run test_unsafe_grips twice, one against a system principal debuggee
// and another time with a null principal debuggee
// The following tests work like this:
// - The specified code is evaluated in a system principal.
// `Cu`, `systemPrincipal` and `Services` are provided as global variables.
// - The resulting object is debugged in a system or null principal debuggee,
// depending on in which list the test is placed.
// It is tested according to the specified test parameters.
// - An ordinary object that inherits from the resulting one is also debugged.
// This is just to check that it can be normally debugged even with an unsafe
// object in the prototype. The specified test parameters do not apply.
// The following tests are defined via properties with the following defaults.
const defaults = {
// The class of the grip.
class:
"Restricted",
// The stringification of the object
string:
"",
// Whether the object (not its grip) has class "Function".
isFunction:
false,
// Whether the grip has a preview property.
hasPreview:
true,
// Code that assigns the object to be tested into the obj variable.
code:
"var obj = {}",
// The type of the grip of the prototype.
protoType:
"null",
// Whether the object has some own string properties.
hasOwnPropertyNames:
false,
// Whether the object has some own symbol properties.
hasOwnPropertySymbols:
false,
// The descriptor obtained when retrieving property "x" or Symbol("x").
property: undefined,
// Code evaluated after the test, whose result is expected to be true.
afterTest:
"true == true",
};
// The following tests use a system principal debuggee.
const systemPrincipalTests = [
{
// Dead objects throw a TypeError when accessing properties.
class:
"DeadObject",
string:
"",
code: `
var obj = Cu.Sandbox(
null);
Cu.nukeSandbox(obj);
`,
property: descriptor({ value:
"TypeError" }),
},
{
// This proxy checks that no trap runs (using a second proxy as the handler
// there is no need to maintain a list of all possible traps).
class:
"Proxy",
string:
"",
code: `
var trapDidRun =
false;
var obj =
new Proxy({},
new Proxy({}, {get: (_, trap) => {
trapDidRun =
true;
throw new Error(
"proxy trap '" + trap +
"' was called.");
}}));
`,
afterTest:
"trapDidRun === false",
},
{
// Like the previous test, but now the proxy has a Function class.
class:
"Proxy",
string:
"",
isFunction:
true,
code: `
var trapDidRun =
false;
var obj =
new Proxy(
function(){},
new Proxy({}, {get: (_, trap) => {
trapDidRun =
true;
throw new Error(
"proxy trap '" + trap +
"' was called.(function)");
}}));
`,
afterTest:
"trapDidRun === false",
},
{
// Invisisible-to-debugger objects can't be unwrapped, so we don't know if
// they are proxies. Thus they shouldn't be accessed.
class:
"InvisibleToDebugger: Array",
string:
"",
hasPreview:
false,
code: `
var s = Cu.Sandbox(systemPrincipal, {invisibleToDebugger:
true});
var obj = s.eval(
"[1, 2, 3]");
`,
},
{
// Like the previous test, but now the object has a Function class.
class:
"InvisibleToDebugger: Function",
string:
"",
isFunction:
true,
hasPreview:
false,
code: `
var s = Cu.Sandbox(systemPrincipal, {invisibleToDebugger:
true});
var obj = s.eval(
"(function func(arg){})");
`,
},
{
// Cu.Sandbox is a WrappedNative that throws when accessing properties.
class:
"nsXPCComponents_utils_Sandbox",
string:
"[object nsXPCComponents_utils_Sandbox]",
code: `
var obj = Cu.Sandbox;`,
protoType:
"object",
},
];
// The following tests run code in a system principal, but the resulting object
// is debugged in a null principal.
const nullPrincipalTests = [
{
// The null principal gets undefined when attempting to access properties.
string:
"[object Object]",
code: `
var obj = {x: -1};`,
},
{
// For arrays it's an error instead of undefined.
string:
"[object Object]",
code: `
var obj = [1, 2, 3];`,
property: descriptor({ value:
"Error" }),
},
{
// For functions it's also an error.
string:
"function func(arg){}",
isFunction:
true,
hasPreview:
false,
code: `
var obj =
function func(arg){};`,
property: descriptor({ value:
"Error" }),
},
{
// Check that no proxy trap runs.
string:
"[object Object]",
code: `
var trapDidRun =
false;
var obj =
new Proxy([],
new Proxy({}, {get: (_, trap) => {
trapDidRun =
true;
throw new Error(
"proxy trap '" + trap +
"' was called.");
}}));
`,
property: descriptor({ value:
"Error" }),
afterTest: `trapDidRun ===
false`,
},
{
// Like the previous test, but now the object is a callable Proxy.
string:
"function () {\n [native code]\n}",
isFunction:
true,
hasPreview:
false,
code: `
var trapDidRun =
false;
var obj =
new Proxy(
function(){},
new Proxy({}, {get: (_, trap) => {
trapDidRun =
true;
throw new Error(
"proxy trap '" + trap +
"' was called.");
}}));
`,
property: descriptor({ value:
"Error" }),
afterTest: `trapDidRun ===
false`,
},
{
// Cross-origin Window objects do expose some properties and have a preview.
string:
"[object Object]",
code: `
var obj = Services.appShell.createWindowlessBrowser().document.defaultView;`,
hasOwnPropertyNames:
true,
hasOwnPropertySymbols:
true,
property: descriptor({ value:
"SecurityError" }),
previewUrl:
"about:blank",
},
{
// Cross-origin Location objects do expose some properties and have a preview.
string:
"[object Object]",
code: `
var obj = Services.appShell.createWindowlessBrowser().document.defaultView
.location;`,
hasOwnPropertyNames:
true,
hasOwnPropertySymbols:
true,
property: descriptor({ value:
"SecurityError" }),
},
];
function descriptor(descr) {
return Object.assign(
{
configurable:
false,
writable:
false,
enumerable:
false,
value: undefined,
},
descr
);
}
async
function test_unsafe_grips(
{ threadFront, debuggee, isWorkerServer },
tests
) {
debuggee.eval(
// These arguments are tested.
// eslint-disable-next-line no-unused-vars
function stopMe(arg1, arg2) {
debugger;
}.toString()
);
for (let data of tests) {
data = { ...defaults, ...data };
// Run the code and test the results.
const sandbox = Cu.Sandbox(systemPrincipal);
Object.assign(sandbox, { Services, systemPrincipal, Cu });
sandbox.eval(data.code);
debuggee.obj = sandbox.obj;
const inherits = `Object.create(obj, {
x: {value: 1},
[Symbol.
for(
"x")]: {value: 2}
})`;
const packet = await executeOnNextTickAndWaitForPause(
() => debuggee.eval(`stopMe(obj, ${inherits});`),
threadFront
);
const [objGrip, inheritsGrip] = packet.frame.arguments;
for (
const grip of [objGrip, inheritsGrip]) {
const isUnsafe = grip === objGrip;
// If `isUnsafe` is true, the parameters in `data` will be used to assert
// against `objGrip`, the grip of the object `obj` created by the test.
// Otherwise, the grip will refer to `inherits`, an ordinary object which
// inherits from `obj`. Then all checks are hardcoded because in every test
// all methods are expected to work the same on `inheritsGrip`.
check_grip(grip, data, isUnsafe, isWorkerServer);
const objClient = threadFront.pauseGrip(grip);
let response, slice;
response = await objClient.getPrototypeAndProperties();
check_properties(response.ownProperties, data, isUnsafe);
check_symbols(response.ownSymbols, data, isUnsafe);
check_prototype(response.prototype, data, isUnsafe, isWorkerServer);
response = await objClient.enumProperties({
ignoreIndexedProperties:
true,
});
slice = await response.slice(0, response.count);
check_properties(slice.ownProperties, data, isUnsafe);
response = await objClient.enumProperties({});
slice = await response.slice(0, response.count);
check_properties(slice.ownProperties, data, isUnsafe);
response = await objClient.getProperty(
"x");
check_property(response.descriptor, data, isUnsafe);
response = await objClient.enumSymbols();
slice = await response.slice(0, response.count);
check_symbol_names(slice.ownSymbols, data, isUnsafe);
response = await objClient.getProperty(Symbol.
for(
"x"));
check_symbol(response.descriptor, data, isUnsafe);
response = await objClient.getPrototype();
check_prototype(response.prototype, data, isUnsafe, isWorkerServer);
}
await threadFront.resume();
ok(sandbox.eval(data.afterTest),
"Check after test passes");
}
}
function check_grip(grip, data, isUnsafe, isWorkerServer) {
if (isUnsafe) {
strictEqual(grip.
class, data.
class,
"The grip has the proper class.");
strictEqual(
"preview" in grip, data.hasPreview,
"Check preview presence.");
// preview.url isn't populated on worker server.
if (data.previewUrl && !isWorkerServer) {
console.trace();
strictEqual(
grip.preview.url,
data.previewUrl,
`Check preview.url
for "${data.code}".`
);
}
}
else {
strictEqual(grip.
class,
"Object",
"The grip has 'Object' class.");
ok(
"preview" in grip,
"The grip has a preview.");
}
}
function check_properties(props, data, isUnsafe) {
const propNames = Reflect.ownKeys(props);
check_property_names(propNames, data, isUnsafe);
if (isUnsafe) {
deepEqual(props.x, undefined,
"The property does not exist.");
}
else {
strictEqual(props.x.value, 1,
"The property has the right value.");
}
}
function check_property_names(props, data, isUnsafe) {
if (isUnsafe) {
strictEqual(
!!props.length,
data.hasOwnPropertyNames,
"Check presence of own string properties."
);
}
else {
strictEqual(props.length, 1,
"1 own property was retrieved.");
strictEqual(props[0],
"x",
"The property has the right name.");
}
}
function check_property(descr, data, isUnsafe) {
if (isUnsafe) {
deepEqual(descr, data.property,
"Got the right property descriptor.");
}
else {
strictEqual(descr.value, 1,
"The property has the right value.");
}
}
function check_symbols(symbols, data, isUnsafe) {
check_symbol_names(symbols, data, isUnsafe);
if (!isUnsafe) {
check_symbol(symbols[0].descriptor, data, isUnsafe);
}
}
function check_symbol_names(props, data, isUnsafe) {
if (isUnsafe) {
strictEqual(
!!props.length,
data.hasOwnPropertySymbols,
"Check presence of own symbol properties."
);
}
else {
strictEqual(props.length, 1,
"1 own symbol property was retrieved.");
strictEqual(props[0].name,
"Symbol(x)",
"The symbol has the right name.");
}
}
function check_symbol(descr, data, isUnsafe) {
if (isUnsafe) {
deepEqual(
descr,
data.property,
"Got the right symbol property descriptor."
);
}
else {
strictEqual(descr.value, 2,
"The symbol property has the right value.");
}
}
function check_prototype(proto, data, isUnsafe, isWorkerServer) {
const protoGrip = proto && proto.getGrip ? proto.getGrip() : proto;
if (isUnsafe) {
deepEqual(protoGrip.type, data.protoType,
"Got the right prototype type.");
}
else {
check_grip(protoGrip, data,
true, isWorkerServer);
}
}
// threadFrontTest uses systemPrincipal by default, but let's be explicit here.
add_task(
threadFrontTest(
options => {
return test_unsafe_grips(options, systemPrincipalTests,
"system");
},
{ principal: systemPrincipal }
)
);
const nullPrincipal = Services.scriptSecurityManager.createNullPrincipal({});
add_task(
threadFrontTest(
options => {
return test_unsafe_grips(options, nullPrincipalTests,
"null");
},
{ principal: nullPrincipal }
)
);