blob: caac56a176de5b05aed6cc83c9c7b730310870aa [file] [log] [blame]
<!doctype html>
<meta charset=utf-8>
<meta name="timeout" content="long">
<title>Cross-origin behavior of Window and Location</title>
<link rel="author" title="Bobby Holley (:bholley)" href="bobbyholley@gmail.com">
<link rel="help" href="https://html.spec.whatwg.org/multipage/#security-window">
<link rel="help" href="https://html.spec.whatwg.org/multipage/#security-location">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/get-host-info.sub.js"></script>
<div id=log></div>
<iframe id="B"></iframe>
<iframe id="C"></iframe>
<iframe id="D"></iframe>
<script>
/*
* Setup boilerplate. This gives us a same-origin window "B" and a cross-origin
* window "C".
*/
var host_info = get_host_info();
setup({explicit_done: true});
path = location.pathname.substring(0, location.pathname.lastIndexOf('/')) + '/frame.html';
pathWithThen = location.pathname.substring(0, location.pathname.lastIndexOf('/')) + '/frame-with-then.html';
var B = document.getElementById('B').contentWindow;
var C = document.getElementById('C').contentWindow;
var D = document.getElementById('D').contentWindow;
B.frameElement.uriToLoad = path;
C.frameElement.uriToLoad = get_host_info().HTTP_REMOTE_ORIGIN + path;
D.frameElement.uriToLoad = get_host_info().HTTP_REMOTE_ORIGIN + pathWithThen;
function reloadSubframes(cb) {
var iframes = document.getElementsByTagName('iframe');
iframes.forEach = Array.prototype.forEach;
var count = 0;
function frameLoaded() {
this.onload = null;
if (++count == iframes.length)
cb();
}
iframes.forEach(function(ifr) { ifr.onload = frameLoaded; ifr.setAttribute('src', ifr.uriToLoad); });
}
function isObject(x) { return Object(x) === x; }
/*
* Note: we eschew assert_equals in a lot of these tests, since the harness ends
* up throwing when it tries to format a message involving a cross-origin object.
*/
var testList = [];
function addTest(func, desc) { testList.push({func, desc, promiseTest: false}); }
function addPromiseTest(func, desc) { testList.push({func, desc, promiseTest: true}); }
/*
* Basic sanity testing.
*/
addTest(function() {
// Note: we do not check location.host as its default port semantics are hard to reflect statically
assert_equals(location.hostname, host_info.ORIGINAL_HOST, 'Need to run the top-level test from domain ' + host_info.ORIGINAL_HOST);
assert_equals(get_port(location), host_info.HTTP_PORT, 'Need to run the top-level test from port ' + host_info.HTTP_PORT);
assert_equals(B.parent, window, "window.parent works same-origin");
assert_equals(C.parent, window, "window.parent works cross-origin");
assert_equals(B.location.pathname, path, "location.href works same-origin");
assert_throws("SecurityError", function() { C.location.pathname; }, "location.pathname throws cross-origin");
assert_equals(B.frames, 'override', "Overrides visible in the same-origin case");
assert_equals(C.frames, C, "Overrides invisible in the cross-origin case");
}, "Basic sanity-checking");
/*
* Whitelist behavior.
*
* 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);
addTest(function() {
for (var prop in window) {
if (whitelistedWindowProps.indexOf(prop) != -1) {
C[prop]; // Shouldn't throw.
Object.getOwnPropertyDescriptor(C, prop); // Shouldn't throw.
assert_true(Object.prototype.hasOwnProperty.call(C, prop), "hasOwnProperty for " + String(prop));
} else {
assert_throws("SecurityError", function() { C[prop]; }, "Should throw when accessing " + String(prop) + " on Window");
assert_throws("SecurityError", function() { Object.getOwnPropertyDescriptor(C, prop); },
"Should throw when accessing property descriptor for " + prop + " on Window");
assert_throws("SecurityError", function() { Object.prototype.hasOwnProperty.call(C, prop); },
"Should throw when invoking hasOwnProperty for " + prop + " on Window");
}
if (prop != 'location')
assert_throws("SecurityError", function() { C[prop] = undefined; }, "Should throw when writing to " + prop + " on Window");
}
for (var prop in location) {
if (prop == 'replace') {
C.location[prop]; // Shouldn't throw.
Object.getOwnPropertyDescriptor(C.location, prop); // Shouldn't throw.
assert_true(Object.prototype.hasOwnProperty.call(C.location, prop), "hasOwnProperty for " + prop);
}
else {
assert_throws("SecurityError", function() { C[prop]; }, "Should throw when accessing " + prop + " on Location");
assert_throws("SecurityError", function() { Object.getOwnPropertyDescriptor(C, prop); },
"Should throw when accessing property descriptor for " + prop + " on Location");
assert_throws("SecurityError", function() { Object.prototype.hasOwnProperty.call(C, prop); },
"Should throw when invoking hasOwnProperty for " + prop + " on Location");
}
if (prop != 'href')
assert_throws("SecurityError", function() { C[prop] = undefined; }, "Should throw when writing to " + prop + " on Location");
}
}, "Only whitelisted properties are accessible cross-origin");
/*
* ES Internal Methods.
*/
/*
* [[GetPrototypeOf]]
*/
addTest(function() {
assert_equals(Object.getPrototypeOf(C), null, "cross-origin Window proto is null");
assert_equals(Object.getPrototypeOf(C.location), null, "cross-origin Location proto is null (__proto__)");
var protoGetter = Object.getOwnPropertyDescriptor(Object.prototype, '__proto__').get;
assert_equals(protoGetter.call(C), null, "cross-origin Window proto is null");
assert_equals(protoGetter.call(C.location), null, "cross-origin Location proto is null (__proto__)");
assert_throws("SecurityError", function() { C.__proto__; }, "__proto__ property not available cross-origin");
assert_throws("SecurityError", function() { C.location.__proto__; }, "__proto__ property not available cross-origin");
}, "[[GetPrototypeOf]] should return null");
/*
* [[SetPrototypeOf]]
*/
addTest(function() {
assert_throws("SecurityError", function() { C.__proto__ = new Object(); }, "proto set on cross-origin Window");
assert_throws("SecurityError", function() { C.location.__proto__ = new Object(); }, "proto set on cross-origin Location");
var setters = [Object.getOwnPropertyDescriptor(Object.prototype, '__proto__').set];
if (Object.setPrototypeOf)
setters.push(function(p) { Object.setPrototypeOf(this, p); });
setters.forEach(function(protoSetter) {
assert_throws(new TypeError, function() { protoSetter.call(C, new Object()); }, "proto setter |call| on cross-origin Window");
assert_throws(new TypeError, function() { protoSetter.call(C.location, new Object()); }, "proto setter |call| on cross-origin Location");
});
if (Reflect.setPrototypeOf) {
assert_false(Reflect.setPrototypeOf(C, new Object()),
"Reflect.setPrototypeOf on cross-origin Window");
assert_false(Reflect.setPrototypeOf(C.location, new Object()),
"Reflect.setPrototypeOf on cross-origin Location");
}
}, "[[SetPrototypeOf]] should return false");
/*
* [[IsExtensible]]
*/
addTest(function() {
assert_true(Object.isExtensible(C), "cross-origin Window should be extensible");
assert_true(Object.isExtensible(C.location), "cross-origin Location should be extensible");
}, "[[IsExtensible]] should return true for cross-origin objects");
/*
* [[PreventExtensions]]
*/
addTest(function() {
assert_throws(new TypeError, function() { Object.preventExtensions(C) },
"preventExtensions on cross-origin Window should throw");
assert_throws(new TypeError, function() { Object.preventExtensions(C.location) },
"preventExtensions on cross-origin Location should throw");
}, "[[PreventExtensions]] should throw for cross-origin objects");
/*
* [[GetOwnProperty]]
*/
addTest(function() {
assert_true(isObject(Object.getOwnPropertyDescriptor(C, 'close')), "C.close is |own|");
assert_true(isObject(Object.getOwnPropertyDescriptor(C, 'top')), "C.top is |own|");
assert_true(isObject(Object.getOwnPropertyDescriptor(C.location, 'href')), "C.location.href is |own|");
assert_true(isObject(Object.getOwnPropertyDescriptor(C.location, 'replace')), "C.location.replace is |own|");
}, "[[GetOwnProperty]] - Properties on cross-origin objects should be reported |own|");
function checkPropertyDescriptor(desc, propName, expectWritable) {
const isSymbol = typeof(propName) === "symbol";
const isArrayIndexPropertyName = !isSymbol && !isNaN(parseInt(propName, 10));
propName = String(propName);
assert_true(isObject(desc), "property descriptor for " + propName + " should exist");
assert_equals(desc.configurable, true, "property descriptor for " + propName + " should be configurable");
if (!isArrayIndexPropertyName) {
assert_equals(desc.enumerable, false, "property descriptor for " + propName + " should not be enumerable");
if (isSymbol || propName == "then") {
assert_true("value" in desc,
"property descriptor for " + propName + " should be a value descriptor");
assert_equals(desc.value, undefined,
"symbol-named cross-origin visible prop " + propName +
" should come back as undefined");
}
} else {
assert_equals(desc.enumerable, true, "property descriptor for " + propName + " should be enumerable");
}
if ('value' in desc)
assert_equals(desc.writable, expectWritable, "property descriptor for " + propName + " should have writable: " + expectWritable);
else
assert_equals(typeof desc.set != 'undefined', expectWritable,
"property descriptor for " + propName + " should " + (expectWritable ? "" : "not ") + "have setter");
}
addTest(function() {
whitelistedWindowProps.forEach(function(prop) {
var desc = Object.getOwnPropertyDescriptor(C, prop);
checkPropertyDescriptor(desc, prop, prop == 'location');
});
checkPropertyDescriptor(Object.getOwnPropertyDescriptor(C.location, 'replace'), 'replace', false);
checkPropertyDescriptor(Object.getOwnPropertyDescriptor(C.location, 'href'), 'href', true);
assert_equals(typeof Object.getOwnPropertyDescriptor(C.location, 'href').get, 'undefined', "Cross-origin location should have no href getter");
whitelistedSymbols.forEach(function(prop) {
var desc = Object.getOwnPropertyDescriptor(C.location, prop);
checkPropertyDescriptor(desc, prop, false);
});
}, "[[GetOwnProperty]] - Property descriptors for cross-origin properties should be set up correctly");
addTest(function() {
assert_equals(typeof D.then, "object");
}, "[[GetOwnProperty]] - Subframe named 'then' should shadow the default 'then' value");
/*
* [[Delete]]
*/
addTest(function() {
assert_throws("SecurityError", function() { delete C[0]; }, "Can't delete cross-origin indexed property");
assert_throws("SecurityError", function() { delete C[100]; }, "Can't delete cross-origin indexed property");
assert_throws("SecurityError", function() { delete C.location; }, "Can't delete cross-origin property");
assert_throws("SecurityError", function() { delete C.parent; }, "Can't delete cross-origin property");
assert_throws("SecurityError", function() { delete C.length; }, "Can't delete cross-origin property");
assert_throws("SecurityError", function() { delete C.document; }, "Can't delete cross-origin property");
assert_throws("SecurityError", function() { delete C.foopy; }, "Can't delete cross-origin property");
assert_throws("SecurityError", function() { delete C.location.href; }, "Can't delete cross-origin property");
assert_throws("SecurityError", function() { delete C.location.replace; }, "Can't delete cross-origin property");
assert_throws("SecurityError", function() { delete C.location.port; }, "Can't delete cross-origin property");
assert_throws("SecurityError", function() { delete C.location.foopy; }, "Can't delete cross-origin property");
}, "[[Delete]] Should throw on cross-origin objects");
/*
* [[DefineOwnProperty]]
*/
function checkDefine(obj, prop) {
var valueDesc = { configurable: true, enumerable: false, writable: false, value: 2 };
var accessorDesc = { configurable: true, enumerable: false, get: function() {} };
assert_throws("SecurityError", function() { Object.defineProperty(obj, prop, valueDesc); }, "Can't define cross-origin value property " + prop);
assert_throws("SecurityError", function() { Object.defineProperty(obj, prop, accessorDesc); }, "Can't define cross-origin accessor property " + prop);
}
addTest(function() {
checkDefine(C, 'length');
checkDefine(C, 'parent');
checkDefine(C, 'location');
checkDefine(C, 'document');
checkDefine(C, 'foopy');
checkDefine(C.location, 'href');
checkDefine(C.location, 'replace');
checkDefine(C.location, 'port');
checkDefine(C.location, 'foopy');
}, "[[DefineOwnProperty]] Should throw for cross-origin objects");
/*
* EnumerateObjectProperties (backed by [[OwnPropertyKeys]])
*/
addTest(function() {
let i = 0;
for (var prop in C) {
i++;
assert_true(whitelistedWindowIndices.includes(prop), prop + " is not safelisted for a cross-origin Window");
}
assert_equals(i, whitelistedWindowIndices.length, "Enumerate all enumerable safelisted cross-origin Window properties");
i = 0;
for (var prop in C.location) {
i++;
}
assert_equals(i, 0, "There's nothing to enumerate for cross-origin Location properties");
}, "Can only enumerate safelisted enumerable properties");
/*
* [[OwnPropertyKeys]]
*/
addTest(function() {
assert_array_equals(Object.getOwnPropertyNames(C).sort(),
whitelistedWindowPropNames,
"Object.getOwnPropertyNames() gives the right answer for cross-origin Window");
assert_array_equals(Object.keys(C).sort(),
whitelistedWindowIndices,
"Object.keys() gives the right answer for cross-origin Window");
assert_array_equals(Object.getOwnPropertyNames(C.location).sort(),
whitelistedLocationPropNames,
"Object.getOwnPropertyNames() gives the right answer for cross-origin Location");
assert_equals(Object.keys(C.location).length, 0,
"Object.keys() gives the right answer for cross-origin Location");
}, "[[OwnPropertyKeys]] should return all properties from cross-origin objects");
addTest(function() {
assert_array_equals(Object.getOwnPropertySymbols(C), whitelistedSymbols,
"Object.getOwnPropertySymbols() should return the three symbol-named properties that are exposed on a cross-origin Window");
assert_array_equals(Object.getOwnPropertySymbols(C.location),
whitelistedSymbols,
"Object.getOwnPropertySymbols() should return the three symbol-named properties that are exposed on a cross-origin Location");
}, "[[OwnPropertyKeys]] should return the right symbol-named properties for cross-origin objects");
addTest(function() {
var allWindowProps = Reflect.ownKeys(C);
indexedWindowProps = allWindowProps.slice(0, whitelistedWindowIndices.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,
"Reflect.ownKeys should start with the indices exposed on the cross-origin window.");
assert_array_equals(stringWindowProps.sort(), whitelistedWindowPropNames,
"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.");
var allLocationProps = Reflect.ownKeys(C.location);
stringLocationProps = allLocationProps.slice(0, -1 * whitelistedSymbols.length);
symbolLocationProps = allLocationProps.slice(-1 * whitelistedSymbols.length);
assert_array_equals(stringLocationProps.sort(), whitelistedLocationPropNames,
"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.")
}, "[[OwnPropertyKeys]] should place the symbols after the property names after the subframe indices");
addTest(function() {
var stringProps = Object.getOwnPropertyNames(D);
// Named frames are not exposed via [[OwnPropertyKeys]].
assert_equals(stringProps.indexOf("a"), -1);
assert_equals(stringProps.indexOf("b"), -1);
assert_equals(typeof D.a, "object");
assert_equals(typeof D.b, "object");
assert_equals(stringProps[stringProps.length - 1], "then");
assert_equals(stringProps.indexOf("then"), stringProps.lastIndexOf("then"));
}, "[[OwnPropertyKeys]] should not reorder where 'then' appears if it's a named subframe, nor add another copy of 'then'");
addTest(function() {
assert_equals(B.eval('parent.C'), C, "A and B observe the same identity for C's Window");
assert_equals(B.eval('parent.C.location'), C.location, "A and B observe the same identity for C's Location");
}, "A and B jointly observe the same identity for cross-origin Window and Location");
function checkFunction(f, proto) {
var name = f.name || '<missing name>';
assert_equals(typeof f, 'function', name + " is a function");
assert_equals(Object.getPrototypeOf(f), proto, f.name + " has the right prototype");
}
addTest(function() {
checkFunction(C.close, Function.prototype);
checkFunction(C.location.replace, Function.prototype);
}, "Cross-origin functions get local Function.prototype");
addTest(function() {
assert_true(isObject(Object.getOwnPropertyDescriptor(C, 'parent')),
"Need to be able to use Object.getOwnPropertyDescriptor do this test");
checkFunction(Object.getOwnPropertyDescriptor(C, 'parent').get, Function.prototype);
checkFunction(Object.getOwnPropertyDescriptor(C.location, 'href').set, Function.prototype);
}, "Cross-origin Window accessors get local Function.prototype");
addTest(function() {
checkFunction(close, Function.prototype);
assert_not_equals(close, B.close, 'same-origin Window functions get their own object');
assert_not_equals(close, C.close, 'cross-origin Window functions get their own object');
var close_B = B.eval('parent.C.close');
assert_not_equals(close, close_B, 'close_B is unique when viewed by the parent');
assert_not_equals(close_B, C.close, 'different Window functions per-incumbent script settings object');
checkFunction(close_B, B.Function.prototype);
checkFunction(location.replace, Function.prototype);
assert_not_equals(location.replace, C.location.replace, "cross-origin Location functions get their own object");
var replace_B = B.eval('parent.C.location.replace');
assert_not_equals(replace_B, C.location.replace, 'different Location functions per-incumbent script settings object');
checkFunction(replace_B, B.Function.prototype);
}, "Same-origin observers get different functions for cross-origin objects");
addTest(function() {
assert_true(isObject(Object.getOwnPropertyDescriptor(C, 'parent')),
"Need to be able to use Object.getOwnPropertyDescriptor do this test");
var get_self_parent = Object.getOwnPropertyDescriptor(window, 'parent').get;
var get_parent_A = Object.getOwnPropertyDescriptor(C, 'parent').get;
var get_parent_B = B.eval('Object.getOwnPropertyDescriptor(parent.C, "parent").get');
assert_not_equals(get_self_parent, get_parent_A, 'different Window accessors per-incumbent script settings object');
assert_not_equals(get_parent_A, get_parent_B, 'different Window accessors per-incumbent script settings object');
checkFunction(get_self_parent, Function.prototype);
checkFunction(get_parent_A, Function.prototype);
checkFunction(get_parent_B, B.Function.prototype);
}, "Same-origin observers get different accessors for cross-origin Window");
addTest(function() {
var set_self_href = Object.getOwnPropertyDescriptor(window.location, 'href').set;
var set_href_A = Object.getOwnPropertyDescriptor(C.location, 'href').set;
var set_href_B = B.eval('Object.getOwnPropertyDescriptor(parent.C.location, "href").set');
assert_not_equals(set_self_href, set_href_A, 'different Location accessors per-incumbent script settings object');
assert_not_equals(set_href_A, set_href_B, 'different Location accessors per-incumbent script settings object');
checkFunction(set_self_href, Function.prototype);
checkFunction(set_href_A, Function.prototype);
checkFunction(set_href_B, B.Function.prototype);
}, "Same-origin observers get different accessors for cross-origin Location");
addTest(function() {
assert_equals({}.toString.call(C), "[object Object]");
assert_equals({}.toString.call(C.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);
});
}, "Resolving a promise with a cross-origin window without a 'then' subframe should work.");
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.");
addPromiseTest(function() {
return Promise.resolve(D.location).then((arg) => {
assert_equals(arg, D.location);
});
}, "Resolving a promise with a cross-origin location should work.");
// We do a fresh load of the subframes for each test to minimize side-effects.
// It would be nice to reload ourselves as well, but we can't do that without
// disrupting the test harness.
function testDone() {
if (testList.length != 0) {
reloadSubframes(runNextTest);
} else {
done();
}
}
function runNextTest() {
var entry = testList.shift();
if (entry.promiseTest) {
promise_test(() => entry.func().finally(testDone), entry.desc);
} else {
test(entry.func, entry.desc);
testDone();
}
}
reloadSubframes(runNextTest);
</script>