diff options
Diffstat (limited to 'testing/web-platform/tests/html/browsers/origin/cross-origin-objects')
13 files changed, 1248 insertions, 0 deletions
diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-due-to-document-domain-only.html b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-due-to-document-domain-only.html new file mode 100644 index 0000000000..425374faec --- /dev/null +++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-due-to-document-domain-only.html @@ -0,0 +1,33 @@ +<!doctype html> +<title>Cross-origin due to document.domain</title> +<meta charset=utf-8> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id=log></div> +<iframe src=resources/cross-origin-due-to-document-domain-only-helper.html></iframe> +<script> +async_test(t => { + onload = t.step_func_done(() => { + const frame = document.querySelector("iframe"); + const innerSelf = self[0]; + const innerLocation = innerSelf.location; + const innerDocument = innerSelf.document; + assert_equals(innerLocation.host, location.host); + assert_true(innerSelf.expandosForever); + assert_true(innerLocation.expandosForever); + assert_equals(frame.contentWindow, innerSelf); + assert_equals(frame.contentDocument, innerDocument); + innerSelf.setDocumentDomain(); + assert_throws_dom("SecurityError", () => innerSelf.expandosForever); + assert_throws_dom("SecurityError", () => innerLocation.expandosForever); + assert_throws_dom("SecurityError", () => innerLocation.host); + assert_equals(innerSelf.parent, self); + assert_throws_dom("SecurityError", () => innerSelf.frameElement); + assert_throws_dom("SecurityError", () => innerLocation.reload()); + assert_equals(frame.contentWindow, innerSelf); + assert_equals(frame.contentDocument, null); + // Cross-origin Document object obtained before it became cross-origin has no protections + assert_equals(innerDocument.URL, frame.src); + }); +}); +</script> diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-caching.html b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-caching.html new file mode 100644 index 0000000000..a8af18d106 --- /dev/null +++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-caching.html @@ -0,0 +1,49 @@ +<!doctype html> +<meta charset=utf-8> +<title>Cross-origin methods and accessors are cached per Realm via[[CrossOriginPropertyDescriptorMap]]</title> +<link rel="help" href="https://html.spec.whatwg.org/multipage/browsers.html#crossorigingetownpropertyhelper-(-o,-p-)"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="cross-origin-objects-function-common.js"></script> +<div id=log></div> +<script> +"use strict"; + +promise_test(async t => { + const w = await makeCrossOriginWindow(t); + for (const {key} of crossOriginWindowMethods) { + assert_equals(w[key], w[key], `w.${key} via [[Get]]`); + const desc1 = Object.getOwnPropertyDescriptor(w, key); + const desc2 = Object.getOwnPropertyDescriptor(w, key); + assert_equals(desc1.value, desc2.value, `w.${key} via [[GetOwnProperty]]`); + } +}, "Cross-origin Window methods are cached"); + +promise_test(async t => { + const w = await makeCrossOriginWindow(t); + for (const {key} of crossOriginWindowAccessors) { + const desc1 = Object.getOwnPropertyDescriptor(w, key); + const desc2 = Object.getOwnPropertyDescriptor(w, key); + assert_equals(desc1.get, desc2.get, `w.${key} getter`); + if (key === "location") { + assert_equals(desc1.set, desc2.set, `w.${key} setter`); + } + } +}, "Cross-origin Window accessors are cached"); + +promise_test(async t => { + const w = await makeCrossOriginWindow(t); + assert_equals(w.location.replace, w.location.replace, "via [[Get]]"); + const desc1 = Object.getOwnPropertyDescriptor(w.location, "replace"); + const desc2 = Object.getOwnPropertyDescriptor(w.location, "replace"); + assert_equals(desc1.value, desc2.value, "via [[GetOwnProperty]]"); +}, "Cross-origin Location `replace` method is cached"); + +promise_test(async t => { + const w = await makeCrossOriginWindow(t); + const desc1 = Object.getOwnPropertyDescriptor(w.location, "href"); + const desc2 = Object.getOwnPropertyDescriptor(w.location, "href"); + assert_equals(desc1.set, desc2.set); +}, "Cross-origin Location `href` setter is cached"); +</script> diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-common.js b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-common.js new file mode 100644 index 0000000000..3b93b498a2 --- /dev/null +++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-common.js @@ -0,0 +1,34 @@ +"use strict"; + +const crossOriginWindowMethods = [ + {key: "close", length: 0}, + {key: "focus", length: 0}, + {key: "blur", length: 0}, + {key: "postMessage", length: 1}, +]; + +const crossOriginWindowAccessors = [ + "window", + "self", + "location", + "closed", + "frames", + "length", + "top", + "opener", + "parent", +].map(key => ({key})); + +const makeCrossOriginWindow = t => { + const iframe = document.createElement("iframe"); + const path = location.pathname.slice(0, location.pathname.lastIndexOf("/")) + "/frame.html"; + iframe.src = get_host_info().HTTP_REMOTE_ORIGIN + path; + + return new Promise((resolve, reject) => { + iframe.onload = () => { resolve(iframe.contentWindow); }; + iframe.onerror = reject; + + document.body.append(iframe); + t.add_cleanup(() => { iframe.remove(); }); + }); +}; diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-length.html b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-length.html new file mode 100644 index 0000000000..466915a461 --- /dev/null +++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-length.html @@ -0,0 +1,45 @@ +<!doctype html> +<meta charset=utf-8> +<title>Cross-origin methods and accessors are created with correct 'length' property</title> +<link rel="help" href="https://html.spec.whatwg.org/multipage/browsers.html#crossorigingetownpropertyhelper-(-o,-p-)"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="cross-origin-objects-function-common.js"></script> +<div id=log></div> +<script> +"use strict"; + +promise_test(async t => { + const w = await makeCrossOriginWindow(t); + for (const {key, length} of crossOriginWindowMethods) { + assert_equals(w[key].length, length, `w.${key} via [[Get]]`); + const desc = Object.getOwnPropertyDescriptor(w, key); + assert_equals(desc.value.length, length, `w.${key} via [[GetOwnProperty]]`); + } +}, "Cross-origin Window methods have correct 'length'"); + +promise_test(async t => { + const w = await makeCrossOriginWindow(t); + for (const {key} of crossOriginWindowAccessors) { + const desc = Object.getOwnPropertyDescriptor(w, key); + assert_equals(desc.get.length, 0, `w.${key}`); + if (key === "location") { + assert_equals(desc.set.length, 1, `w.${key}`); + } + } +}, "Cross-origin Window accessors have correct 'length'"); + +promise_test(async t => { + const w = await makeCrossOriginWindow(t); + assert_equals(w.location.replace.length, 1); + const desc = Object.getOwnPropertyDescriptor(w.location, "replace"); + assert_equals(desc.value.length, 1); +}, "Cross-origin Location `replace` method has correct 'length'"); + +promise_test(async t => { + const w = await makeCrossOriginWindow(t); + const desc = Object.getOwnPropertyDescriptor(w.location, "href"); + assert_equals(desc.set.length, 1); +}, "Cross-origin Location `href` setter has correct 'length'"); +</script> diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-name.html b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-name.html new file mode 100644 index 0000000000..167c30e8f3 --- /dev/null +++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-function-name.html @@ -0,0 +1,45 @@ +<!doctype html> +<meta charset=utf-8> +<title>Cross-origin methods and accessors are created with correct 'name' property</title> +<link rel="help" href="https://html.spec.whatwg.org/multipage/browsers.html#crossorigingetownpropertyhelper-(-o,-p-)"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<script src="cross-origin-objects-function-common.js"></script> +<div id=log></div> +<script> +"use strict"; + +promise_test(async t => { + const w = await makeCrossOriginWindow(t); + for (const {key} of crossOriginWindowMethods) { + assert_equals(w[key].name, key, `w.${key} via [[Get]]`); + const desc = Object.getOwnPropertyDescriptor(w, key); + assert_equals(desc.value.name, key, `w.${key} via [[GetOwnProperty]]`); + } +}, "Cross-origin Window methods have correct 'name'"); + +promise_test(async t => { + const w = await makeCrossOriginWindow(t); + for (const {key} of crossOriginWindowAccessors) { + const desc = Object.getOwnPropertyDescriptor(w, key); + assert_equals(desc.get.name, `get ${key}`); + if (key === "location") { + assert_equals(desc.set.name, `set ${key}`); + } + } +}, "Cross-origin Window accessors have correct 'name'"); + +promise_test(async t => { + const w = await makeCrossOriginWindow(t); + assert_equals(w.location.replace.name, "replace"); + const desc = Object.getOwnPropertyDescriptor(w.location, "replace"); + assert_equals(desc.value.name, "replace"); +}, "Cross-origin Location `replace` method has correct 'name'"); + +promise_test(async t => { + const w = await makeCrossOriginWindow(t); + const desc = Object.getOwnPropertyDescriptor(w.location, "href"); + assert_equals(desc.set.name, "set href"); +}, "Cross-origin Location `href` setter has correct 'name'"); +</script> diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-on-new-window.html b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-on-new-window.html new file mode 100644 index 0000000000..3ad0de6a3a --- /dev/null +++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects-on-new-window.html @@ -0,0 +1,25 @@ +<!doctype html> +<meta charset=utf-8> +<meta name="timeout" content="long"> +<title>Cross-origin behavior of Window and Location on new Window</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> +<div id=log></div> +<script> +setup({explicit_done: true}); + +window.addEventListener('message', function onmessage(evt) { + window.removeEventListener('message', onmessage); + test(function() { + var results = evt.data; + assert_true(results.length > 0, 'Need results'); + results.forEach(function(r) { assert_true(r.pass, r.message); }); + }, "Cross-origin object identity preserved across document.domain"); + win.close(); + done(); +}); +var win = window.open('win-documentdomain.sub.html'); +</script> diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects.html b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects.html new file mode 100644 index 0000000000..d1b6cabc0f --- /dev/null +++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/cross-origin-objects.html @@ -0,0 +1,718 @@ +<!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> +<iframe id="E"></iframe> +<iframe id="F"></iframe> +<iframe id="G"></iframe> +<iframe id="H"></iframe> +<script> + +/* + * Setup boilerplate. This gives us a same-origin window "B", cross-origin + * windows "C" and "D", initially same-origin but then changing document.domain + * windows "E" and "F", and not-same-site (also cross-origin, of course) windows + * "G" and "H". + */ +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; +var E = document.getElementById('E').contentWindow; +var F = document.getElementById('F').contentWindow; +var G = document.getElementById('G').contentWindow; +var H = document.getElementById('H').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; +E.frameElement.uriToLoad = path + "?setdomain"; +F.frameElement.uriToLoad = pathWithThen + "?setdomain"; +G.frameElement.uriToLoad = get_host_info().HTTP_NOTSAMESITE_ORIGIN + path; +H.frameElement.uriToLoad = get_host_info().HTTP_NOTSAMESITE_ORIGIN + pathWithThen; + +function winName(win) { + var iframes = document.getElementsByTagName('iframe'); + iframes.find = Array.prototype.find; + var found = iframes.find(function (ifr) { + return ifr.contentWindow == win; + }); + if (found) { + return found.id; + } + return "UNKNOWN"; +} + +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. + */ + +/* + * List of tests. Each test is actually a pair: an array of tests to run and a + * boolean for whether these are promise tests. We reload all the subframes in + * between running each toplevel test. This is done to avoid having to reload + * all the subframes for every single test, which is overkill: some of these + * tests are known to touch only one subframe. And doing it makes the test + * really slow. + */ +var testList = []; +function addTest(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: 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}); +} + +/** + * Similar helpers, but for the subframes that load frame-with-then.html + */ +function addThenTest(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: false}); +} + +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 smoke tests for same-origin and cross-origin behaviors. + */ + +addTest(function(win) { + // 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(win.parent, window, "window.parent works cross-origin"); + assert_equals(B.location.pathname, path, "location.href works same-origin"); + assert_throws_dom("SecurityError", function() { win.location.pathname; }, "location.pathname throws cross-origin"); + assert_equals(B.frames, 'override', "Overrides visible in the same-origin case"); + assert_equals(win.frames, win, "Overrides invisible in the cross-origin case"); + assert_equals(B.focus, 'override', "Overrides visible in the same-origin case"); + checkFunction(win.focus, Function.prototype); + assert_not_equals(win.focus, focus, "Overrides invisible in the cross-origin case"); +}, "Basic sanity-checking"); + +/* + * Tests regarding which properties are allowed cross-origin. + * + * Also tests for [[GetOwnProperty]] and [[HasOwnProperty]] behavior. + */ + +var allowedSymbols = [Symbol.toStringTag, Symbol.hasInstance, + Symbol.isConcatSpreadable]; +var windowAllowlists = { + namedFrames: ['donotleakme'], + 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'], +} +windowAllowlists.propNames = Array.from(new Set([...windowAllowlists.indices, + ...windowAllowlists.getters, + ...windowAllowlists.setters, + ...windowAllowlists.methods, + 'then'])).sort(); +windowAllowlists.props = windowAllowlists.propNames.concat(allowedSymbols); + +var locationAllowlists = { + getters: [], + setters: ['href'], + methods: ['replace'], + promiseMethods: [], +} +locationAllowlists.propNames = Array.from(new Set([...locationAllowlists.getters, + ...locationAllowlists.setters, + ...locationAllowlists.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 (windowAllowlists.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)); + } else { + assert_throws_dom("SecurityError", function() { win[prop]; }, "Should throw when accessing " + String(prop) + " on Window"); + assert_throws_dom("SecurityError", function() { Object.getOwnPropertyDescriptor(win, prop); }, + "Should throw when accessing property descriptor for " + prop + " on Window"); + assert_throws_dom("SecurityError", function() { Object.prototype.hasOwnProperty.call(win, prop); }, + "Should throw when invoking hasOwnProperty for " + prop + " on Window"); + } + if (prop != 'location') + assert_throws_dom("SecurityError", function() { win[prop] = undefined; }, "Should throw when writing to " + prop + " on Window"); + } + for (var prop of windowAllowlists.namedFrames) { + win[prop]; // Shouldn't throw. + var desc = Object.getOwnPropertyDescriptor(win, prop); + assert_false(desc.writable, "[[Writable]] for named frame " + String(prop)); + assert_false(desc.enumerable, "[[Enumerable]] for named frame " + String(prop)); + assert_true(desc.configurable, "[[Configurable]] for named frame " + String(prop)); + assert_true(Object.prototype.hasOwnProperty.call(win, prop), "hasOwnProperty for " + String(prop)); + } + for (var prop in location) { + if (prop == 'replace') { + win.location[prop]; // Shouldn't throw. + Object.getOwnPropertyDescriptor(win.location, prop); // Shouldn't throw. + assert_true(Object.prototype.hasOwnProperty.call(win.location, prop), "hasOwnProperty for " + prop); + assert_throws_dom("SecurityError", function() { win.location[prop] = undefined; }, "Should throw when writing to " + prop + " on Location"); + } + else if (prop == 'href') { + Object.getOwnPropertyDescriptor(win.location, prop); // Shouldn't throw. + assert_true(Object.prototype.hasOwnProperty.call(win.location, prop), "hasOwnProperty for " + prop); + assert_throws_dom("SecurityError", function() { win.location[prop] }, + "Should throw reading href on Location"); + } + else { + assert_throws_dom("SecurityError", function() { win.location[prop]; }, "Should throw when accessing " + prop + " on Location"); + assert_throws_dom("SecurityError", function() { Object.getOwnPropertyDescriptor(win.location, prop); }, + "Should throw when accessing property descriptor for " + prop + " on Location"); + assert_throws_dom("SecurityError", function() { Object.prototype.hasOwnProperty.call(win.location, prop); }, + "Should throw when invoking hasOwnProperty for " + prop + " on Location"); + assert_throws_dom("SecurityError", function() { win.location[prop] = undefined; }, "Should throw when writing to " + prop + " on Location"); + } + } +}, "Only certain properties are accessible cross-origin"); + +addPromiseTest(async function(win, test_obj) { + async function checkProperties(objName, allowedlists) { + 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 (allowedlists.promiseMethods.includes(prop)) { + await promise_rejects_dom(test_obj, "SecurityError", desc.value.call(otherObj), + `Should throw when calling ${objName}.${prop} with cross-origin this object`); + } else if (!allowedlists.methods.includes(prop)) { + for (let args of methodArgs.get(prop) || [[]]) { + assert_throws_dom("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 (allowedlists.getters.includes(prop)) { + desc.get.call(otherObj); // Shouldn't throw. + } else { + assert_throws_dom("SecurityError", desc.get.bind(otherObj), + `Should throw when calling ${objName}.${prop} getter with cross-origin this object`); + } + } + if (desc.set) { + if (allowedlists.setters.includes(prop)) { + desc.set.call(otherObj, "javascript:undefined"); // Shouldn't throw. + } else { + assert_throws_dom("SecurityError", desc.set.bind(otherObj, "foo"), + `Should throw when calling ${objName}.${prop} setter with cross-origin this object`); + } + } + } + } + } + + await checkProperties("location", locationAllowlists); + await checkProperties("window", windowAllowlists); +}, "Only certain properties are usable as cross-origin this objects"); + +/* + * ES Internal Methods. + */ + +/* + * [[GetPrototypeOf]] + */ +addTest(function(win) { + assert_equals(Object.getPrototypeOf(win), null, "cross-origin Window proto is null"); + assert_equals(Object.getPrototypeOf(win.location), null, "cross-origin Location proto is null (__proto__)"); + var protoGetter = Object.getOwnPropertyDescriptor(Object.prototype, '__proto__').get; + assert_equals(protoGetter.call(win), null, "cross-origin Window proto is null"); + assert_equals(protoGetter.call(win.location), null, "cross-origin Location proto is null (__proto__)"); + assert_throws_dom("SecurityError", function() { win.__proto__; }, "__proto__ property not available cross-origin"); + assert_throws_dom("SecurityError", function() { win.location.__proto__; }, "__proto__ property not available cross-origin"); + +}, "[[GetPrototypeOf]] should return null"); + +/* + * [[SetPrototypeOf]] + */ +addTest(function(win) { + assert_throws_dom("SecurityError", function() { win.__proto__ = new Object(); }, "proto set on cross-origin Window"); + assert_throws_dom("SecurityError", function() { win.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_js(TypeError, function() { protoSetter.call(win, new Object()); }, "proto setter |call| on cross-origin Window"); + assert_throws_js(TypeError, function() { protoSetter.call(win.location, new Object()); }, "proto setter |call| on cross-origin Location"); + }); + // Hack to avoid "duplicate test name" harness issues. + setters.forEach(function(protoSetter) { + test(function() { protoSetter.call(win, null); }, + "proto setter |call| on cross-origin Window with null (" + protoSetter + ", " + winName(win) + ")"); + test(function() { protoSetter.call(win.location, null); }, + "proto setter |call| on cross-origin Location with null (" + protoSetter + ", " + winName(win) + ")"); + }); + if (Reflect.setPrototypeOf) { + assert_false(Reflect.setPrototypeOf(win, new Object()), + "Reflect.setPrototypeOf on cross-origin Window"); + assert_true(Reflect.setPrototypeOf(win, null), + "Reflect.setPrototypeOf on cross-origin Window with null"); + assert_false(Reflect.setPrototypeOf(win.location, new Object()), + "Reflect.setPrototypeOf on cross-origin Location"); + assert_true(Reflect.setPrototypeOf(win.location, null), + "Reflect.setPrototypeOf on cross-origin Location with null"); + } +}, "[[SetPrototypeOf]] should return false"); + +/* + * [[IsExtensible]] + */ +addTest(function(win) { + assert_true(Object.isExtensible(win), "cross-origin Window should be extensible"); + assert_true(Object.isExtensible(win.location), "cross-origin Location should be extensible"); +}, "[[IsExtensible]] should return true for cross-origin objects"); + +/* + * [[PreventExtensions]] + */ +addTest(function(win) { + assert_throws_js(TypeError, function() { Object.preventExtensions(win) }, + "preventExtensions on cross-origin Window should throw"); + assert_throws_js(TypeError, function() { Object.preventExtensions(win.location) }, + "preventExtensions on cross-origin Location should throw"); + assert_false(Reflect.preventExtensions(win), + "Reflect.preventExtensions on cross-origin Window"); + assert_false(Reflect.preventExtensions(win.location), + "Reflect.preventExtensions on cross-origin Location"); +}, "[[PreventExtensions]] should return false cross-origin objects"); + +/* + * [[GetOwnProperty]] + */ + +addTest(function(win) { + assert_true(isObject(Object.getOwnPropertyDescriptor(win, 'close')), "win.close is |own|"); + assert_true(isObject(Object.getOwnPropertyDescriptor(win, 'top')), "win.top is |own|"); + assert_true(isObject(Object.getOwnPropertyDescriptor(win.location, 'href')), "win.location.href is |own|"); + assert_true(isObject(Object.getOwnPropertyDescriptor(win.location, 'replace')), "win.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(win) { + windowAllowlists.props.forEach(function(prop) { + var desc = Object.getOwnPropertyDescriptor(win, prop); + checkPropertyDescriptor(desc, prop, prop == 'location'); + }); + checkPropertyDescriptor(Object.getOwnPropertyDescriptor(win.location, 'replace'), 'replace', false); + checkPropertyDescriptor(Object.getOwnPropertyDescriptor(win.location, 'href'), 'href', true); + assert_equals(typeof Object.getOwnPropertyDescriptor(win.location, 'href').get, 'undefined', "Cross-origin location should have no href getter"); + allowedSymbols.forEach(function(prop) { + var desc = Object.getOwnPropertyDescriptor(win.location, prop); + checkPropertyDescriptor(desc, prop, false); + }); +}, "[[GetOwnProperty]] - Property descriptors for cross-origin properties should be set up correctly"); + +addThenTest(function(win) { + assert_equals(typeof win.then, "object"); +}, "[[GetOwnProperty]] - Subframe named 'then' should shadow the default 'then' value"); + +addThenTest(function(win) { + assert_equals(typeof win.close, "function"); + assert_equals(typeof win.open, "object"); +}, "[[GetOwnProperty]] - Subframes should be visible cross-origin only if their names don't match the names of cross-origin-exposed IDL properties"); + +addTest(function(win) { + assert_equals(typeof Object.getOwnPropertyDescriptor(win, '0').value, "object"); + assert_equals(typeof Object.getOwnPropertyDescriptor(win, '1').value, "object"); + assert_throws_dom("SecurityError", function() { + Object.getOwnPropertyDescriptor(win, '2'); + }); +}, "[[GetOwnProperty]] - Should be able to get a property descriptor for an indexed property only if it corresponds to a child window."); + +/* + * [[Delete]] + */ +addTest(function(win) { + assert_throws_dom("SecurityError", function() { delete win[0]; }, "Can't delete cross-origin indexed property"); + assert_throws_dom("SecurityError", function() { delete win[100]; }, "Can't delete cross-origin indexed property"); + assert_throws_dom("SecurityError", function() { delete win.location; }, "Can't delete cross-origin property"); + assert_throws_dom("SecurityError", function() { delete win.parent; }, "Can't delete cross-origin property"); + assert_throws_dom("SecurityError", function() { delete win.length; }, "Can't delete cross-origin property"); + assert_throws_dom("SecurityError", function() { delete win.document; }, "Can't delete cross-origin property"); + assert_throws_dom("SecurityError", function() { delete win.foopy; }, "Can't delete cross-origin property"); + assert_throws_dom("SecurityError", function() { delete win.location.href; }, "Can't delete cross-origin property"); + assert_throws_dom("SecurityError", function() { delete win.location.replace; }, "Can't delete cross-origin property"); + assert_throws_dom("SecurityError", function() { delete win.location.port; }, "Can't delete cross-origin property"); + assert_throws_dom("SecurityError", function() { delete win.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_dom("SecurityError", function() { Object.defineProperty(obj, prop, valueDesc); }, "Can't define cross-origin value property " + prop); + assert_throws_dom("SecurityError", function() { Object.defineProperty(obj, prop, accessorDesc); }, "Can't define cross-origin accessor property " + prop); +} +addTest(function(win) { + checkDefine(win, 'length'); + checkDefine(win, 'parent'); + checkDefine(win, 'location'); + checkDefine(win, 'document'); + checkDefine(win, 'foopy'); + checkDefine(win.location, 'href'); + checkDefine(win.location, 'replace'); + checkDefine(win.location, 'port'); + checkDefine(win.location, 'foopy'); +}, "[[DefineOwnProperty]] Should throw for cross-origin objects"); + +/* + * EnumerateObjectProperties (backed by [[OwnPropertyKeys]]) + */ + +addTest(function(win) { + let i = 0; + for (var prop in win) { + i++; + assert_true(windowAllowlists.indices.includes(prop), prop + " is not safelisted for a cross-origin Window"); + } + assert_equals(i, windowAllowlists.indices.length, "Enumerate all enumerable safelisted cross-origin Window properties"); + i = 0; + for (var prop in win.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(win) { + assert_array_equals(Object.getOwnPropertyNames(win).sort(), + windowAllowlists.propNames, + "Object.getOwnPropertyNames() gives the right answer for cross-origin Window"); + assert_array_equals(Object.keys(win).sort(), + windowAllowlists.indices, + "Object.keys() gives the right answer for cross-origin Window"); + assert_array_equals(Object.getOwnPropertyNames(win.location).sort(), + locationAllowlists.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"); +}, "[[OwnPropertyKeys]] should return all properties from cross-origin objects"); + +addTest(function(win) { + assert_array_equals(Object.getOwnPropertySymbols(win), allowedSymbols, + "Object.getOwnPropertySymbols() should return the three symbol-named properties that are exposed on a cross-origin Window"); + assert_array_equals(Object.getOwnPropertySymbols(win.location), + allowedSymbols, + "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(win) { + var allWindowProps = Reflect.ownKeys(win); + indexedWindowProps = allWindowProps.slice(0, windowAllowlists.indices.length); + stringWindowProps = allWindowProps.slice(0, -1 * allowedSymbols.length); + symbolWindowProps = allWindowProps.slice(-1 * allowedSymbols.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, windowAllowlists.indices, + "Reflect.ownKeys should start with the indices exposed on the cross-origin window."); + assert_array_equals(stringWindowProps.sort(), windowAllowlists.propNames, + "Reflect.ownKeys should continue with the cross-origin window properties for a cross-origin Window."); + assert_array_equals(symbolWindowProps, allowedSymbols, + "Reflect.ownKeys should end with the cross-origin symbols for a cross-origin Window."); + + var allLocationProps = Reflect.ownKeys(win.location); + stringLocationProps = allLocationProps.slice(0, -1 * allowedSymbols.length); + symbolLocationProps = allLocationProps.slice(-1 * allowedSymbols.length); + assert_array_equals(stringLocationProps.sort(), locationAllowlists.propNames, + "Reflect.ownKeys should start with the cross-origin window properties for a cross-origin Location.") + assert_array_equals(symbolLocationProps, allowedSymbols, + "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"); + +addThenTest(function(win) { + var stringProps = Object.getOwnPropertyNames(win); + // Named frames are not exposed via [[OwnPropertyKeys]]. + assert_equals(stringProps.indexOf("a"), -1); + assert_equals(stringProps.indexOf("b"), -1); + assert_equals(typeof win.a, "object"); + assert_equals(typeof win.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(win) { + assert_equals(B.eval('parent.' + winName(win)), win, "A and B observe the same identity for C's Window"); + assert_equals(B.eval('parent.' + winName(win) + '.location'), win.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(win) { + checkFunction(win.close, Function.prototype); + checkFunction(win.location.replace, Function.prototype); +}, "Cross-origin functions get local Function.prototype"); + +addTest(function(win) { + assert_true(isObject(Object.getOwnPropertyDescriptor(win, 'parent')), + "Need to be able to use Object.getOwnPropertyDescriptor do this test"); + checkFunction(Object.getOwnPropertyDescriptor(win, 'parent').get, Function.prototype); + checkFunction(Object.getOwnPropertyDescriptor(win.location, 'href').set, Function.prototype); +}, "Cross-origin Window accessors get local Function.prototype"); + +addTest(function(win) { + checkFunction(close, Function.prototype); + assert_not_equals(close, B.close, 'same-origin Window functions get their own object'); + assert_not_equals(close, win.close, 'cross-origin Window functions get their own object'); + var close_B = B.eval('parent.' + winName(win) + '.close'); + assert_not_equals(close, close_B, 'close_B is unique when viewed by the parent'); + assert_not_equals(close_B, win.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, win.location.replace, "cross-origin Location functions get their own object"); + var replace_B = B.eval('parent.' + winName(win) + '.location.replace'); + assert_not_equals(replace_B, win.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(win) { + assert_true(isObject(Object.getOwnPropertyDescriptor(win, '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(win, 'parent').get; + var get_parent_B = B.eval('Object.getOwnPropertyDescriptor(parent.' + winName(win) + ', "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(win) { + var set_self_href = Object.getOwnPropertyDescriptor(window.location, 'href').set; + var set_href_A = Object.getOwnPropertyDescriptor(win.location, 'href').set; + var set_href_B = B.eval('Object.getOwnPropertyDescriptor(parent.' + winName(win) + '.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(win) { + assert_equals({}.toString.call(win), "[object Object]"); + assert_equals({}.toString.call(win.location), "[object Object]"); +}, "{}.toString.call() does the right thing on cross-origin objects"); + +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"); + +addPromiseThenTest(function(win) { + return Promise.resolve(win).then((arg) => { + assert_equals(arg, win); + }); +}, "Resolving a promise with a cross-origin window with a 'then' subframe should work"); + +addPromiseThenTest(function(win) { + return Promise.resolve(win.location).then((arg) => { + assert_equals(arg, win.location); + }); +}, "Resolving a promise with a cross-origin location should work"); + +addTest(function(win) { + var desc = Object.getOwnPropertyDescriptor(window, "onmouseenter"); + var f = () => {}; + + // Check that it has [LegacyLenientThis] behavior + assert_equals(desc.get.call({}), undefined, "getter should return undefined"); + desc.set.call({}, f); // Should not throw. + + // Check that we can apply it to a same-origin window. + assert_equals(desc.get.call(B), null, "Should be able to read the value"); + desc.set.call(B, f); + assert_equals(desc.get.call(B), f, "Value should have updated"); + // And reset it for our next test + desc.set.call(B, null); + assert_equals(desc.get.call(B), null, "Should have been reset"); + + // Check that applying it to a cross-origin window throws instead of doing + // the [LegacyLenientThis] behavior. + assert_throws_dom("SecurityError", () => { + desc.get.call(win); + }, "Should throw when getting cross-origin"); + assert_throws_dom("SecurityError", () => { + desc.set.call(win, f); + }, "Should throw when setting cross-origin"); +}, "LegacyLenientThis behavior"); + +// 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(); + } +} + +async function runNextTest() { + var entry = testList.shift(); + if (entry.promiseTest) { + 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 (let t of entry.tests) { + test(t.func, t.desc); + } + } + testDone(); +} +reloadSubframes(runNextTest); + +</script> diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/frame-with-then.html b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/frame-with-then.html new file mode 100644 index 0000000000..3eedeca38a --- /dev/null +++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/frame-with-then.html @@ -0,0 +1,19 @@ +<!doctype html> +<html> + <script> + if (location.search == "?setdomain") { + document.domain = document.domain; + } + </script> + <body> + <!--- Some frames to test ordering --> + <iframe name="a"></iframe> + <!-- A subframe to test "then" behavior --> + <iframe name="then"></iframe> + <iframe name="b"></iframe> + <!-- Two subframes with names corresponding to IDL-defined properties; one + a cross-origin-exposed property and one not exposed cross-origin --> + <iframe name="close"></iframe> + <iframe name="open"></iframe> + </body> +</html> diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/frame.html b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/frame.html new file mode 100644 index 0000000000..ca2dd8ebf8 --- /dev/null +++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/frame.html @@ -0,0 +1,51 @@ +<!doctype html> +<html> +<head> +<script> + if (location.search == "?setdomain") { + document.domain = document.domain; + } + + // Override the |frames| and |focus| property to test that such overrides are + // properly ignored cross-origin. + window.frames = "override"; + window.focus = "override"; + + // Also add a |then| property to test that it doesn't get exposed. + window.then = "something"; + window.location.then = "something-else"; + + // If we get a postMessage, we grab references to everything and set + // document.domain to trim off our topmost subdomain. + window.onmessage = function(evt) { + window.windowReferences = []; + window.locationReferences = []; + for (var i = 0; i < parent.length; ++i) { + windowReferences.push(parent[i]); + locationReferences.push(parent[i].location); + } + try { + document.domain = document.domain.substring(document.domain.indexOf('.') + 1); + evt.source.postMessage('PASS', '*'); + } catch (e) { + evt.source.postMessage('FAIL: cannot trim off document.domain: ' + e, '*'); + } + } + + function checkWindowReferences() { + for (var i = 0; i < parent.length; ++i) { + if (windowReferences[i] != parent[i]) + throw new Error("Window references don't match for " + i + " after document.domain"); + if (locationReferences[i] != parent[i].location) + throw new Error("Location references don't match for " + i + " after document.domain"); + } + return true; + } +</script> +</head> +<body> + <!-- Two subframes to give us some indexed properties --> + <iframe></iframe> + <iframe name=donotleakme></iframe><!-- "donotleakme" is excluded as cross-origin named property due to [[HideFromKeys]] --> +</body> +</html> diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/location-properties-smoke-test.window.js b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/location-properties-smoke-test.window.js new file mode 100644 index 0000000000..45b61d07f5 --- /dev/null +++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/location-properties-smoke-test.window.js @@ -0,0 +1,51 @@ +// META: variant=?assign +// META: variant=?customproperty +// META: variant=?hash +// META: variant=?host +// META: variant=?hostname +// META: variant=?pathname +// META: variant=?port +// META: variant=?protocol +// META: variant=?reload +// META: variant=?search +// META: variant=?toString +// META: variant=?valueOf +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/common/dispatcher/dispatcher.js + +const property = window.location.search.substr(1); + +promise_test(async t => { + const iframeContext = new RemoteContext(token()); + const iframe = document.createElement("iframe"); + iframe.src = get_host_info().REMOTE_ORIGIN + + "/common/dispatcher/remote-executor.html?uuid=" + iframeContext.context_id; + document.body.appendChild(iframe); + + // Wait for the cross-origin document to be loaded inside the iframe. + assert_equals( + await iframeContext.execute_script(() => "Document loaded") , + "Document loaded" + ); + + assert_throws_dom("SecurityError", () => { + const unused = iframe.contentWindow.location[property]; + }, "Cross origin get of a location property should throw a security error"); + + assert_throws_dom("SecurityError", () => { + iframe.contentWindow.location[property] = "Random string"; + }, "Cross origin set of a location property should throw a security error"); + + // Verify that the property was indeed not modified. + assert_not_equals( + await iframeContext.execute_script(property => location[property], + [property]), + "Random string", + ); + + assert_throws_dom("SecurityError", () => { + const unused = Object.getOwnPropertyDescriptor( + iframe.contentWindow.location, property); + }, "Cross origin get of descriptors should throw a security error"); +}, `Verifying that cross-origin access of '${property}' is restricted`); diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/resources/cross-origin-due-to-document-domain-only-helper.html b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/resources/cross-origin-due-to-document-domain-only-helper.html new file mode 100644 index 0000000000..10ac8ece0e --- /dev/null +++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/resources/cross-origin-due-to-document-domain-only-helper.html @@ -0,0 +1,9 @@ +<!doctype html> +<meta charset=utf-8> +<script> +self.expandosForever = true +self.location.expandosForever = true +function setDocumentDomain() { + document.domain = document.domain +} +</script> diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/win-documentdomain.sub.html b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/win-documentdomain.sub.html new file mode 100644 index 0000000000..a315e21208 --- /dev/null +++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/win-documentdomain.sub.html @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/common/get-host-info.sub.js"></script> + <script> + function loadFrames() { + window.A = document.getElementById('A').contentWindow; + window.B = document.getElementById('B').contentWindow; + window.C = document.getElementById('C').contentWindow; + window.D = document.getElementById('D').contentWindow; + + var path = location.pathname.substring(0, location.pathname.lastIndexOf('/')) + '/frame.html'; + A.location = 'frame.html'; + B.location = '//{{domains[www2]}}:' + get_port(location) + path; + C.location = '//{{domains[www2]}}:' + get_port(location) + path; + D.location = '//{{domains[www1]}}:' + get_port(location) + path; + + var loadCount = 0; + function frameLoaded() { + if (++loadCount == 4) + go(); + } + var iframes = document.getElementsByTagName('iframe'); + for (var i = 0; i < iframes.length; i++) { + iframes[i].onload = frameLoaded; + } + } + + var results = []; + function assert(cond, msg) { + results.push({pass: !!cond, message: msg}); + } + + function go() { + window.onmessage = function(evt) { + try { + assert(evt.data == "PASS", "frame.html processing should be PASS but got " + evt.data); + assert(B.checkWindowReferences(), "B's Window references are still self-consistent after document.domain"); + for (var i = 0; i < window.length; ++i) { + assert(window[i] === B.windowReferences[i], + "Window reference " + i + " consistent between globals after document.domain"); + assert(window[i].location === B.locationReferences[i], + "Location reference " + i + " consistent between globals after document.domain"); + } + } catch(e) { + assert(false, "Should not receive exception: " + e); + } + opener.postMessage(results, '*'); + }; + A.document.domain = A.document.domain; + document.domain = document.domain; + B.postMessage('', '*'); + } + + </script> +</head> +<body onload="loadFrames()"> + <iframe id="A"></iframe> + <iframe id="B"></iframe> + <iframe id="C"></iframe> + <iframe id="D"></iframe> +</body> +</html> diff --git a/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/window-location-and-location-href-cross-realm-set.html b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/window-location-and-location-href-cross-realm-set.html new file mode 100644 index 0000000000..f03550a141 --- /dev/null +++ b/testing/web-platform/tests/html/browsers/origin/cross-origin-objects/window-location-and-location-href-cross-realm-set.html @@ -0,0 +1,106 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Cross-realm [[Set]] to window.location and location.href throws an error of correct realm</title> +<link rel="help" href="https://html.spec.whatwg.org/multipage/#window"> +<link rel="help" href="https://webidl.spec.whatwg.org/#Unforgeable"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/get-host-info.sub.js"></script> +<body> +<script> +const URL_SAME_ORIGIN = get_host_info().ORIGINAL_HOST; +const URL_CROSS_ORIGIN = get_host_info().HTTP_REMOTE_ORIGIN; +const URL_VALID = "#foo"; +const URL_INVALID = "http://#"; + +const { get: locationGet, set: locationSet } = Object.getOwnPropertyDescriptor(window, "location"); +const { get: hrefGet, set: hrefSet } = Object.getOwnPropertyDescriptor(location, "href"); + + +promise_test(async t => { + const sameOriginWindow = await makeWindow(t, URL_SAME_ORIGIN); + assert_throws_js(sameOriginWindow.TypeError, () => { Object.create(sameOriginWindow).location; }); + assert_throws_js(sameOriginWindow.TypeError, () => { Reflect.get(sameOriginWindow, "location", {}); }); + assert_throws_js(TypeError, () => { locationGet.call({}); }); +}, "Same-origin window.location getter throws TypeError in holder's realm on invalid |this| value"); + +promise_test(async t => { + const sameOriginWindow = await makeWindow(t, URL_SAME_ORIGIN); + assert_throws_js(sameOriginWindow.TypeError, () => { Object.create(sameOriginWindow.location).href; }); + assert_throws_js(sameOriginWindow.TypeError, () => { Reflect.get(sameOriginWindow.location, "href", {}); }); + assert_throws_js(TypeError, () => { hrefGet(); }); +}, "Same-origin location.href getter throws TypeError in holder's realm on invalid |this| value"); + +promise_test(async t => { + const crossOriginWindow = await makeWindow(t, URL_CROSS_ORIGIN); + assert_throws_dom("SECURITY_ERR", () => { crossOriginWindow.location.href; }); + assert_throws_dom("SECURITY_ERR", () => { hrefGet.call(crossOriginWindow.location); }); + assert_equals(Object.getOwnPropertyDescriptor(crossOriginWindow.location, "href").get, undefined); +}, "Cross-origin location.href getter throws SecurityError in lexical realm"); + + +promise_test(async t => { + const sameOriginWindow = await makeWindow(t, URL_SAME_ORIGIN); + assert_throws_js(sameOriginWindow.TypeError, () => { Object.create(sameOriginWindow).location = URL_VALID; }); + assert_throws_js(sameOriginWindow.TypeError, () => { Reflect.set(sameOriginWindow, "location", URL_VALID, {}); }); + assert_throws_js(TypeError, () => { locationSet.call(() => {}, URL_VALID); }); +}, "Same-origin window.location setter throws TypeError in holder's realm on invalid |this| value"); + +promise_test(async t => { + const sameOriginWindow = await makeWindow(t, URL_SAME_ORIGIN); + assert_throws_js(sameOriginWindow.TypeError, () => { Object.create(sameOriginWindow.location).href = URL_VALID; }); + assert_throws_js(sameOriginWindow.TypeError, () => { Reflect.set(sameOriginWindow.location, "href", URL_VALID, {}); }); + assert_throws_js(TypeError, () => { hrefSet.call(undefined, URL_VALID); }); +}, "Same-origin location.href setter throws TypeError in holder's realm on invalid |this| value"); + +promise_test(async t => { + const crossOriginWindow = await makeWindow(t, URL_CROSS_ORIGIN); + assert_throws_js(TypeError, () => { Object.create(crossOriginWindow).location = URL_VALID; }); + assert_throws_js(TypeError, () => { Reflect.set(crossOriginWindow, "location", URL_VALID, {}); }); + assert_throws_js(TypeError, () => { locationSet.call([], URL_VALID); }); +}, "Cross-origin window.location setter throws TypeError in lexical realm on invalid |this| value"); + +promise_test(async t => { + const crossOriginWindow = await makeWindow(t, URL_CROSS_ORIGIN); + assert_throws_js(TypeError, () => { Object.create(crossOriginWindow.location).href = URL_VALID; }); + assert_throws_js(TypeError, () => { Reflect.set(crossOriginWindow.location, "href", URL_VALID, {}); }); + assert_throws_js(TypeError, () => { hrefSet.call(null, URL_VALID); }); +}, "Cross-origin location.href setter throws TypeError in lexical realm on invalid |this| value"); + + +promise_test(async t => { + const sameOriginWindow = await makeWindow(t, URL_SAME_ORIGIN); + assert_throws_js(sameOriginWindow.TypeError, () => { sameOriginWindow.location = Symbol(); }); + + // The error originates in sameOriginWindow.location.href setter, hence it's not in realm of locationSet. + assert_throws_js(sameOriginWindow.TypeError, () => { locationSet.call(sameOriginWindow, Symbol()); }); +}, "Same-origin window.location` setter throws TypeError in holder's realm on non-coercible URL argument"); + +promise_test(async t => { + const sameOriginWindow = await makeWindow(t, URL_SAME_ORIGIN); + assert_throws_js(sameOriginWindow.TypeError, () => { sameOriginWindow.location.href = Symbol(); }); + assert_throws_js(TypeError, () => { hrefSet.call(sameOriginWindow.location, Symbol()); }); +}, "Same-origin location.href setter throws TypeError in holder's realm on non-coercible URL argument"); + +promise_test(async t => { + const crossOriginWindow = await makeWindow(t, URL_CROSS_ORIGIN); + assert_throws_js(TypeError, () => { crossOriginWindow.location = Symbol(); }); + assert_throws_js(TypeError, () => { locationSet.call(crossOriginWindow, Symbol()); }); +}, "Cross-origin window.location setter throws TypeError in lexical realm on non-coercible URL argument"); + +promise_test(async t => { + const crossOriginWindow = await makeWindow(t, URL_CROSS_ORIGIN); + assert_throws_js(TypeError, () => { crossOriginWindow.location.href = Symbol(); }); + assert_throws_js(TypeError, () => { hrefSet.call(crossOriginWindow.location, Symbol()); }); +}, "Cross-origin location.href setter throws TypeError in lexical realm on non-coercible URL argument"); + +function makeWindow(t, src) { + return new Promise(resolve => { + const iframe = document.createElement("iframe"); + t.add_cleanup(() => { iframe.remove(); }); + iframe.onload = () => { resolve(iframe.contentWindow); }; + iframe.src = src; + document.body.append(iframe); + }); +} +</script> |