mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-30 00:01:50 +00:00
Bug 1582520: Part 4 - Update cross-origin-objects web platform tests for cross-origin this objects. r=bzbarsky
Same origin native functions called with a compatible cross-origin `this` object are meant to apply the same security checks as if a property getter for the method had been called on the `this` object directly. Firefox has some tests for this behavior, but the web platform test suite does not. This patch adds comprehensive tests for all getters/setters/methods on Window and Location objects for both the allowed and forbidden cases. Differential Revision: https://phabricator.services.mozilla.com/D46736 --HG-- extra : moz-landing-system : lando
This commit is contained in:
parent
036cfced10
commit
7edcb6f3a3
@ -95,8 +95,20 @@ function addTest(func, desc) {
|
||||
promiseTest: false});
|
||||
}
|
||||
|
||||
function addPromiseTest(func, desc) {
|
||||
testList.push(
|
||||
{tests: [
|
||||
{func: func.bind(null, C),
|
||||
desc: desc + " (cross-origin)"},
|
||||
{func: func.bind(null, E),
|
||||
desc: desc + " (same-origin + document.domain)"},
|
||||
{func: func.bind(null, G),
|
||||
desc: desc + " (cross-site)"}],
|
||||
promiseTest: true});
|
||||
}
|
||||
|
||||
/**
|
||||
* A similar helper, but for the subframes that load frame-with-then.html
|
||||
* Similar helpers, but for the subframes that load frame-with-then.html
|
||||
*/
|
||||
function addThenTest(func, desc) {
|
||||
testList.push(
|
||||
@ -110,8 +122,17 @@ function addThenTest(func, desc) {
|
||||
promiseTest: false});
|
||||
}
|
||||
|
||||
function addPromiseTest(func, desc) {
|
||||
testList.push({tests:[{func, desc}], promiseTest: true}); }
|
||||
function addPromiseThenTest(func, desc) {
|
||||
testList.push(
|
||||
{tests: [
|
||||
{func: func.bind(null, D),
|
||||
desc: desc + " (cross-origin)"},
|
||||
{func: func.bind(null, F),
|
||||
desc: desc + " (same-origin + document.domain)"},
|
||||
{func: func.bind(null, H),
|
||||
desc: desc + " (cross-site)"}],
|
||||
promiseTest: true});
|
||||
}
|
||||
|
||||
/*
|
||||
* Basic sanity testing.
|
||||
@ -135,20 +156,63 @@ addTest(function(win) {
|
||||
* Also tests for [[GetOwnProperty]] and [[HasOwnProperty]] behavior.
|
||||
*/
|
||||
|
||||
var whitelistedWindowIndices = ['0', '1'];
|
||||
var whitelistedWindowPropNames = ['location', 'postMessage', 'window', 'frames', 'self', 'top', 'parent',
|
||||
'opener', 'closed', 'close', 'blur', 'focus', 'length', 'then'];
|
||||
whitelistedWindowPropNames = whitelistedWindowPropNames.concat(whitelistedWindowIndices);
|
||||
whitelistedWindowPropNames.sort();
|
||||
var whitelistedLocationPropNames = ['href', 'replace', 'then'];
|
||||
whitelistedLocationPropNames.sort();
|
||||
var whitelistedSymbols = [Symbol.toStringTag, Symbol.hasInstance,
|
||||
Symbol.isConcatSpreadable];
|
||||
var whitelistedWindowProps = whitelistedWindowPropNames.concat(whitelistedSymbols);
|
||||
var windowWhitelists = {
|
||||
indices: ['0', '1'],
|
||||
getters: ['location', 'window', 'frames', 'self', 'top', 'parent',
|
||||
'opener', 'closed', 'length'],
|
||||
setters: ['location'],
|
||||
methods: ['postMessage', 'close', 'blur', 'focus'],
|
||||
// These are methods which return promises and, therefore, when called with a
|
||||
// cross-origin `this` object, do not throw immediately, but instead return a
|
||||
// Promise which rejects with the same SecurityError that they would
|
||||
// otherwise throw. They are not, however, cross-origin accessible.
|
||||
promiseMethods: ['createImageBitmap', 'fetch'],
|
||||
}
|
||||
windowWhitelists.propNames = Array.from(new Set([...windowWhitelists.indices,
|
||||
...windowWhitelists.getters,
|
||||
...windowWhitelists.setters,
|
||||
...windowWhitelists.methods,
|
||||
'then'])).sort();
|
||||
windowWhitelists.props = windowWhitelists.propNames.concat(whitelistedSymbols);
|
||||
|
||||
var locationWhitelists = {
|
||||
getters: [],
|
||||
setters: ['href'],
|
||||
methods: ['replace'],
|
||||
promiseMethods: [],
|
||||
}
|
||||
locationWhitelists.propNames = Array.from(new Set([...locationWhitelists.getters,
|
||||
...locationWhitelists.setters,
|
||||
...locationWhitelists.methods,
|
||||
'then'])).sort();
|
||||
|
||||
// Define various sets of arguments to call cross-origin methods with. Arguments
|
||||
// for any cross-origin-callable method must be valid, and should aim to have no
|
||||
// side-effects. Any method without an entry in this list will be called with
|
||||
// an empty arguments list.
|
||||
var methodArgs = new Map(Object.entries({
|
||||
// As a basic smoke test, we call one cross-origin-inaccessible method with
|
||||
// both valid and invalid arguments to make sure that it rejects with the
|
||||
// same SecurityError regardless.
|
||||
assign: [
|
||||
[],
|
||||
["javascript:undefined"],
|
||||
],
|
||||
// Note: If we post a message to frame.html with a matching origin, its
|
||||
// "onmessage" handler will change its `document.domain`, and potentially
|
||||
// invalidate subsequent tests, so be sure to only pass non-matching origins.
|
||||
postMessage: [
|
||||
["foo", "http://does-not.exist/"],
|
||||
["foo", {}],
|
||||
],
|
||||
replace: [["javascript:undefined"]],
|
||||
}));
|
||||
|
||||
addTest(function(win) {
|
||||
for (var prop in window) {
|
||||
if (whitelistedWindowProps.indexOf(prop) != -1) {
|
||||
if (windowWhitelists.props.indexOf(prop) != -1) {
|
||||
win[prop]; // Shouldn't throw.
|
||||
Object.getOwnPropertyDescriptor(win, prop); // Shouldn't throw.
|
||||
assert_true(Object.prototype.hasOwnProperty.call(win, prop), "hasOwnProperty for " + String(prop));
|
||||
@ -186,6 +250,60 @@ addTest(function(win) {
|
||||
}
|
||||
}, "Only whitelisted properties are accessible cross-origin");
|
||||
|
||||
addPromiseTest(async function(win, test_obj) {
|
||||
async function checkProperties(objName, whitelists) {
|
||||
var localObj = window[objName];
|
||||
var otherObj = win[objName];
|
||||
|
||||
for (var prop in localObj) {
|
||||
let desc;
|
||||
for (let obj = localObj; !desc; obj = Object.getPrototypeOf(obj)) {
|
||||
desc = Object.getOwnPropertyDescriptor(obj, prop);
|
||||
|
||||
}
|
||||
|
||||
if ("value" in desc) {
|
||||
if (typeof desc.value === "function" && String(desc.value).includes("[native code]")) {
|
||||
if (whitelists.promiseMethods.includes(prop)) {
|
||||
await promise_rejects(test_obj, "SecurityError", desc.value.call(otherObj),
|
||||
`Should throw when calling ${objName}.${prop} with cross-origin this object`);
|
||||
} else if (!whitelists.methods.includes(prop)) {
|
||||
for (let args of methodArgs.get(prop) || [[]]) {
|
||||
assert_throws("SecurityError", desc.value.bind(otherObj, ...args),
|
||||
`Should throw when calling ${objName}.${prop} with cross-origin this object`);
|
||||
}
|
||||
|
||||
} else {
|
||||
for (let args of methodArgs.get(prop) || [[]]) {
|
||||
desc.value.apply(otherObj, args); // Shouldn't throw.
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (desc.get) {
|
||||
if (whitelists.getters.includes(prop)) {
|
||||
desc.get.call(otherObj); // Shouldn't throw.
|
||||
} else {
|
||||
assert_throws("SecurityError", desc.get.bind(otherObj),
|
||||
`Should throw when calling ${objName}.${prop} getter with cross-origin this object`);
|
||||
}
|
||||
}
|
||||
if (desc.set) {
|
||||
if (whitelists.setters.includes(prop)) {
|
||||
desc.set.call(otherObj, "javascript:undefined"); // Shouldn't throw.
|
||||
} else {
|
||||
assert_throws("SecurityError", desc.set.bind(otherObj, "foo"),
|
||||
`Should throw when calling ${objName}.${prop} setter with cross-origin this object`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await checkProperties("location", locationWhitelists);
|
||||
await checkProperties("window", windowWhitelists);
|
||||
}, "Only whitelisted properties are usable as cross-origin this objects");
|
||||
|
||||
/*
|
||||
* ES Internal Methods.
|
||||
*/
|
||||
@ -291,7 +409,7 @@ function checkPropertyDescriptor(desc, propName, expectWritable) {
|
||||
}
|
||||
|
||||
addTest(function(win) {
|
||||
whitelistedWindowProps.forEach(function(prop) {
|
||||
windowWhitelists.props.forEach(function(prop) {
|
||||
var desc = Object.getOwnPropertyDescriptor(win, prop);
|
||||
checkPropertyDescriptor(desc, prop, prop == 'location');
|
||||
});
|
||||
@ -367,9 +485,9 @@ addTest(function(win) {
|
||||
let i = 0;
|
||||
for (var prop in win) {
|
||||
i++;
|
||||
assert_true(whitelistedWindowIndices.includes(prop), prop + " is not safelisted for a cross-origin Window");
|
||||
assert_true(windowWhitelists.indices.includes(prop), prop + " is not safelisted for a cross-origin Window");
|
||||
}
|
||||
assert_equals(i, whitelistedWindowIndices.length, "Enumerate all enumerable safelisted cross-origin Window properties");
|
||||
assert_equals(i, windowWhitelists.indices.length, "Enumerate all enumerable safelisted cross-origin Window properties");
|
||||
i = 0;
|
||||
for (var prop in win.location) {
|
||||
i++;
|
||||
@ -383,13 +501,13 @@ addTest(function(win) {
|
||||
|
||||
addTest(function(win) {
|
||||
assert_array_equals(Object.getOwnPropertyNames(win).sort(),
|
||||
whitelistedWindowPropNames,
|
||||
windowWhitelists.propNames,
|
||||
"Object.getOwnPropertyNames() gives the right answer for cross-origin Window");
|
||||
assert_array_equals(Object.keys(win).sort(),
|
||||
whitelistedWindowIndices,
|
||||
windowWhitelists.indices,
|
||||
"Object.keys() gives the right answer for cross-origin Window");
|
||||
assert_array_equals(Object.getOwnPropertyNames(win.location).sort(),
|
||||
whitelistedLocationPropNames,
|
||||
locationWhitelists.propNames,
|
||||
"Object.getOwnPropertyNames() gives the right answer for cross-origin Location");
|
||||
assert_equals(Object.keys(win.location).length, 0,
|
||||
"Object.keys() gives the right answer for cross-origin Location");
|
||||
@ -405,16 +523,16 @@ addTest(function(win) {
|
||||
|
||||
addTest(function(win) {
|
||||
var allWindowProps = Reflect.ownKeys(win);
|
||||
indexedWindowProps = allWindowProps.slice(0, whitelistedWindowIndices.length);
|
||||
indexedWindowProps = allWindowProps.slice(0, windowWhitelists.indices.length);
|
||||
stringWindowProps = allWindowProps.slice(0, -1 * whitelistedSymbols.length);
|
||||
symbolWindowProps = allWindowProps.slice(-1 * whitelistedSymbols.length);
|
||||
// stringWindowProps should have "then" last in this case. Do this
|
||||
// check before we call stringWindowProps.sort() below.
|
||||
assert_equals(stringWindowProps[stringWindowProps.length - 1], "then",
|
||||
"'then' property should be added to the end of the string list if not there");
|
||||
assert_array_equals(indexedWindowProps, whitelistedWindowIndices,
|
||||
assert_array_equals(indexedWindowProps, windowWhitelists.indices,
|
||||
"Reflect.ownKeys should start with the indices exposed on the cross-origin window.");
|
||||
assert_array_equals(stringWindowProps.sort(), whitelistedWindowPropNames,
|
||||
assert_array_equals(stringWindowProps.sort(), windowWhitelists.propNames,
|
||||
"Reflect.ownKeys should continue with the cross-origin window properties for a cross-origin Window.");
|
||||
assert_array_equals(symbolWindowProps, whitelistedSymbols,
|
||||
"Reflect.ownKeys should end with the cross-origin symbols for a cross-origin Window.");
|
||||
@ -422,7 +540,7 @@ addTest(function(win) {
|
||||
var allLocationProps = Reflect.ownKeys(win.location);
|
||||
stringLocationProps = allLocationProps.slice(0, -1 * whitelistedSymbols.length);
|
||||
symbolLocationProps = allLocationProps.slice(-1 * whitelistedSymbols.length);
|
||||
assert_array_equals(stringLocationProps.sort(), whitelistedLocationPropNames,
|
||||
assert_array_equals(stringLocationProps.sort(), locationWhitelists.propNames,
|
||||
"Reflect.ownKeys should start with the cross-origin window properties for a cross-origin Location.")
|
||||
assert_array_equals(symbolLocationProps, whitelistedSymbols,
|
||||
"Reflect.ownKeys should end with the cross-origin symbols for a cross-origin Location.")
|
||||
@ -507,59 +625,23 @@ addTest(function(win) {
|
||||
assert_equals({}.toString.call(win.location), "[object Object]");
|
||||
}, "{}.toString.call() does the right thing on cross-origin objects");
|
||||
|
||||
addPromiseTest(function() {
|
||||
return Promise.resolve(C).then((arg) => {
|
||||
assert_equals(arg, C);
|
||||
addPromiseTest(function(win) {
|
||||
return Promise.resolve(win).then((arg) => {
|
||||
assert_equals(arg, win);
|
||||
});
|
||||
}, "Resolving a promise with a cross-origin window without a 'then' subframe should work (cross-origin).");
|
||||
}, "Resolving a promise with a cross-origin window without a 'then' subframe should work");
|
||||
|
||||
addPromiseTest(function() {
|
||||
return Promise.resolve(E).then((arg) => {
|
||||
assert_equals(arg, E);
|
||||
addPromiseThenTest(function(win) {
|
||||
return Promise.resolve(win).then((arg) => {
|
||||
assert_equals(arg, win);
|
||||
});
|
||||
}, "Resolving a promise with a cross-origin window without a 'then' subframe should work (same-origin + document.domain).");
|
||||
}, "Resolving a promise with a cross-origin window with a 'then' subframe should work");
|
||||
|
||||
addPromiseTest(function() {
|
||||
return Promise.resolve(G).then((arg) => {
|
||||
assert_equals(arg, G);
|
||||
addPromiseThenTest(function(win) {
|
||||
return Promise.resolve(win.location).then((arg) => {
|
||||
assert_equals(arg, win.location);
|
||||
});
|
||||
}, "Resolving a promise with a cross-origin window without a 'then' subframe should work (cross-site).");
|
||||
|
||||
addPromiseTest(function() {
|
||||
return Promise.resolve(D).then((arg) => {
|
||||
assert_equals(arg, D);
|
||||
});
|
||||
}, "Resolving a promise with a cross-origin window with a 'then' subframe should work (cross-origin).");
|
||||
|
||||
addPromiseTest(function() {
|
||||
return Promise.resolve(F).then((arg) => {
|
||||
assert_equals(arg, F);
|
||||
});
|
||||
}, "Resolving a promise with a cross-origin window with a 'then' subframe should work (same-origin + document.domain).");
|
||||
|
||||
addPromiseTest(function() {
|
||||
return Promise.resolve(H).then((arg) => {
|
||||
assert_equals(arg, H);
|
||||
});
|
||||
}, "Resolving a promise with a cross-origin window with a 'then' subframe should work (cross-site).");
|
||||
|
||||
addPromiseTest(function() {
|
||||
return Promise.resolve(D.location).then((arg) => {
|
||||
assert_equals(arg, D.location);
|
||||
});
|
||||
}, "Resolving a promise with a cross-origin location should work (cross-origin).");
|
||||
|
||||
addPromiseTest(function() {
|
||||
return Promise.resolve(F.location).then((arg) => {
|
||||
assert_equals(arg, F.location);
|
||||
});
|
||||
}, "Resolving a promise with a cross-origin location should work (same-origin + document.domain).");
|
||||
|
||||
addPromiseTest(function() {
|
||||
return Promise.resolve(H.location).then((arg) => {
|
||||
assert_equals(arg, H.location);
|
||||
});
|
||||
}, "Resolving a promise with a cross-origin location should work (cross-site).");
|
||||
}, "Resolving a promise with a cross-origin location should work");
|
||||
|
||||
addTest(function(win) {
|
||||
var desc = Object.getOwnPropertyDescriptor(window, "onmouseenter");
|
||||
@ -598,19 +680,22 @@ function testDone() {
|
||||
}
|
||||
}
|
||||
|
||||
function runNextTest() {
|
||||
async function runNextTest() {
|
||||
var entry = testList.shift();
|
||||
if (entry.promiseTest) {
|
||||
test(function() {
|
||||
assert_equals(entry.tests.length, 1, "We can't handle this yet");
|
||||
});
|
||||
promise_test(() => entry.tests[0].func().finally(testDone), entry.tests[0].desc);
|
||||
for (let t of entry.tests) {
|
||||
await new Promise(resolve => {
|
||||
promise_test(test_obj => {
|
||||
return new Promise(res => res(t.func(test_obj))).finally(resolve);
|
||||
}, t.desc);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for (t of entry.tests) {
|
||||
for (let t of entry.tests) {
|
||||
test(t.func, t.desc);
|
||||
}
|
||||
testDone();
|
||||
}
|
||||
testDone();
|
||||
}
|
||||
reloadSubframes(runNextTest);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user