Skip to content

Commit

Permalink
Portals: Block cross-origin postMessage
Browse files Browse the repository at this point in the history
Updates implementation to drop messages sent to/from a portal that is
cross origin with its host, and updates WPTs to reflect this change.

Bug: 1108793
Change-Id: Ie0056c7a01a4d65cccdbfb0a0044b3d6afc87497
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2382064
Commit-Queue: Adithya Srinivasan <[email protected]>
Reviewed-by: Jeremy Roman <[email protected]>
Reviewed-by: Lucas Gadani <[email protected]>
Reviewed-by: Alex Moshchuk <[email protected]>
Reviewed-by: Ken Buchanan <[email protected]>
Cr-Commit-Position: refs/heads/master@{#804405}
  • Loading branch information
a4sriniv authored and chromium-wpt-export-bot committed Sep 3, 2020
1 parent 06b4935 commit f346773
Show file tree
Hide file tree
Showing 8 changed files with 71 additions and 119 deletions.
9 changes: 5 additions & 4 deletions fetch/metadata/portal.https.sub.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<script src=/resources/testdriver-vendor.js></script>
<script src=/fetch/metadata/resources/helper.js></script>
<script src=/common/utils.js></script>
<script src=/portals/resources/stash-utils.sub.js></script>
<body>
<script>
const USER = true;
Expand All @@ -15,12 +16,12 @@
assert_implements("HTMLPortalElement" in window, "Portals are not supported.");

let p = document.createElement('portal');
p.addEventListener('message', t.step_func(e => {
assert_header_equals(e.data, expectations, `{{host}} -> ${host} portal`);
t.done();
const key = token();
StashUtils.takeValue(key).then(t.step_func_done(value => {
assert_header_equals(value, expectations, `{{host}} -> ${host} portal`);
}));

let url = `https://${host}/fetch/metadata/resources/post-to-owner.py`;
let url = `https://${host}/fetch/metadata/resources/post-to-owner.py?key=${key}`;
p.src = url;
document.body.appendChild(p);
}, `{{host}} -> ${host} portal`);
Expand Down
12 changes: 8 additions & 4 deletions fetch/metadata/resources/post-to-owner.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,26 @@ def main(request, response):
(b"Content-Type", b"text/html"),
(b"Cache-Control", b"no-cache, no-store, must-revalidate")
]
key = request.GET.first(b"key", None)

body = u"""
<!DOCTYPE html>
<script src="/portals/resources/stash-utils.sub.js"></script>
<script>
var data = %s;
if (window.opener)
window.opener.postMessage(data, "*");
if (window.top != window)
window.top.postMessage(data, "*");
if (window.portalHost)
window.portalHost.postMessage(data, "*");
const key = %s;
if (key)
StashUtils.putValue(key, data);
</script>
""" % json.dumps({
""" % (json.dumps({
u"dest": isomorphic_decode(request.headers.get(b"sec-fetch-dest", b"")),
u"mode": isomorphic_decode(request.headers.get(b"sec-fetch-mode", b"")),
u"site": isomorphic_decode(request.headers.get(b"sec-fetch-site", b"")),
u"user": isomorphic_decode(request.headers.get(b"sec-fetch-user", b"")),
})
}), json.dumps(key))
return headers, body
98 changes: 30 additions & 68 deletions portals/portals-host-post-message.sub.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,35 +26,23 @@
return waitForResponse;
}

const crossOriginUrl = "http://{{hosts[alt][www]}}:{{ports[http][0]}}/portals/resources/portal-host-post-message.sub.html";
const sameOriginUrl = "resources/portal-host-post-message.html";
const crossOriginUrl = "http://{{hosts[alt][www]}}:{{ports[http][0]}}/portals/resources/portal-host-post-message-x-origin.html";

promise_test(async () => {
var {data, origin} = await createPortalAndLoopMessage(
"resources/portal-host-post-message.sub.html", ["test", "*"]);
var {data, origin} = await createPortalAndLoopMessage(sameOriginUrl,
["test", "*"]);
assert_equals(data, "test");
assert_equals(origin, "http://{{host}}:{{ports[http][0]}}");
}, "Message received after postMessage from portal host");

promise_test(async () => {
var {data, origin} = await createPortalAndLoopMessage(
crossOriginUrl, ["test", "*"]);
assert_equals(data, "test");
assert_equals(origin, "http://{{hosts[alt][www]}}:{{ports[http][0]}}");
}, "Message received after postMessage from portal host in cross-origin-portal");

promise_test(async () => {
var {data, origin} = await createPortalAndLoopMessage(
"resources/portal-host-post-message.sub.html", ["test"]);
var {data, origin} = await createPortalAndLoopMessage(sameOriginUrl,
["test"]);
assert_equals(data, "test");
assert_equals(origin, "http://{{host}}:{{ports[http][0]}}");
}, "Message received from same-origin portal host with no target origin specified");

promise_test(async () => {
var {data, origin} = await createPortalAndLoopMessage(
crossOriginUrl, ["test", "http://{{host}}:{{ports[http][0]}}"]);
assert_equals(data, "test");
}, "Message received from cross-origin portal host with target origin correctly specified");

promise_test(async () => {
var message = {
prop1: "value1",
Expand All @@ -64,10 +52,7 @@
prop4_1: "value4_1"
}
};
var {data} = await createPortalAndLoopMessage(
"resources/portal-host-post-message.sub.html", [message]);
assert_object_equals(data, message);
var {data} = await createPortalAndLoopMessage(crossOriginUrl,
var {data} = await createPortalAndLoopMessage(sameOriginUrl,
[message, "*"]);
assert_object_equals(data, message);
}, "postMessage with object message");
Expand All @@ -82,58 +67,39 @@
});
}

var {ports} = await createPortalAndLoopMessage(
"resources/portal-host-post-message.sub.html",
{type: "message-port"});
await checkPort(ports[0]);

var {ports} = await createPortalAndLoopMessage(
crossOriginUrl,
{type: "message-port"});
var {ports} = await createPortalAndLoopMessage(sameOriginUrl, {
type: "message-port"
});
await checkPort(ports[0]);
}, "postMessage with message ports");

promise_test(async () => {
var {data} = await createPortalAndLoopMessage(
"resources/portal-host-post-message.sub.html", {
type: "array-buffer-without-transfer",
array: [0, 1, 2, 3, 4]
});
assert_array_equals([0, 1, 2, 3, 4], new Int8Array(data.arrayBuffer));

var {data} = await createPortalAndLoopMessage(crossOriginUrl, {
type: "array-buffer-without-transfer",
array: [0, 1, 2, 3, 4]
var {data} = await createPortalAndLoopMessage(sameOriginUrl, {
type: "array-buffer-without-transfer",
array: [0, 1, 2, 3, 4]
});
assert_array_equals([0, 1, 2, 3, 4], new Int8Array(data.arrayBuffer));
}, "postMessage with array buffer without transfer");

promise_test(async () => {
var {data} = await createPortalAndLoopMessage(
"resources/portal-host-post-message.sub.html", {
type: "array-buffer-with-transfer",
array: [0, 1, 2, 3, 4]
});
assert_array_equals([0, 1, 2, 3, 4], new Int8Array(data.arrayBuffer));

var {data} = await createPortalAndLoopMessage(crossOriginUrl, {
type: "array-buffer-with-transfer",
array: [0, 1, 2, 3, 4]
var {data} = await createPortalAndLoopMessage(sameOriginUrl, {
type: "array-buffer-with-transfer",
array: [0, 1, 2, 3, 4]
});
assert_array_equals([0, 1, 2, 3, 4], new Int8Array(data.arrayBuffer));
}, "postMessage with array buffer with transfer");

promise_test(async () => {
var {data} = await createPortalAndLoopMessage(
"resources/portal-host-post-message.sub.html",
{type: "invalid-message"});
var {data} = await createPortalAndLoopMessage(sameOriginUrl, {
type: "invalid-message"
});
assert_equals(data.errorType, "DataCloneError");
}, "postMessage should throw error when serialization fails");

promise_test(async () => {
var {data} = await createPortalAndLoopMessage(
"resources/portal-host-post-message.sub.html",
{type: "invalid-port"});
var {data} = await createPortalAndLoopMessage(sameOriginUrl,{
type: "invalid-port"
});
assert_equals(data.errorType, "TypeError");
}, "postMessage with invalid transferable should throw error");

Expand Down Expand Up @@ -166,18 +132,14 @@
return waitForMessages;
}, "postMessage before and after portal navigation should work");

async_test(t => {
createPortalAndLoopMessage("resources/portal-host-post-message.sub.html",
["test", "http://{{hosts[alt][www]}}:{{ports[http][0]}}"])
.then(t.step_func(() => { assert_unreached("message delivered"); }));
t.step_timeout(t.done, 2000);
}, "Message should not be received from portal host with target set to different origin");
const TIMEOUT_DURATION_MS = 1000;

async_test(t => {
createPortalAndLoopMessage(crossOriginUrl, ["test"]).then(t.step_func(() => {
assert_unreached("message delivered");
}));
t.step_timeout(t.done, 2000);
}, "Message should not be received cross origin-portal host with no target origin set");
promise_test(t => new Promise((resolve, reject) => {
const portal = document.createElement('portal');
portal.src = crossOriginUrl;
portal.onmessage = () => reject('should not have received message');
document.body.appendChild(portal);
t.step_timeout(resolve, TIMEOUT_DURATION_MS);
}), "postMessage from portal host in cross-origin-portal should be blocked");
</script>
</body>
54 changes: 12 additions & 42 deletions portals/portals-post-message.sub.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="resources/stash-utils.sub.js"></script>
<script src="/common/utils.js"></script>
<body>
<input id="input"/>
<script>
const sameOriginUrl = "resources/portal-post-message-portal.html"
const crossOriginUrl = "http://{{hosts[alt][www]}}:{{ports[http][0]}}/portals/resources/portal-post-message-portal.html"
const crossOriginUrl = "http://{{hosts[alt][www]}}:{{ports[http][0]}}/portals/resources/portal-post-message-x-origin-portal.html"

async function createAndInsertPortal(portalSrc) {
assert_implements("HTMLPortalElement" in self);
Expand Down Expand Up @@ -51,15 +53,6 @@
assert_true(sourceIsPortalHost);
}, "postMessage message received by portalHost");

promise_test(async () => {
var portal = await createAndInsertPortal(crossOriginUrl);
var message = "test message";
var {origin, data, sourceIsPortalHost} = await postMessage(portal, message, "*");
assert_equals(data, message);
assert_equals(origin, window.location.origin);
assert_true(sourceIsPortalHost);
}, "postMessage message received by portalHost in cross-origin portal");

promise_test(async () => {
var portal = await createAndInsertPortal(sameOriginUrl);
var message = "test message";
Expand All @@ -72,14 +65,6 @@
assert_equals(data, message);
}, "postMessage received by portal host in same-origin portal for multiple valid target origins");

promise_test(async () => {
var portal = await createAndInsertPortal(crossOriginUrl);
var message = "test message";
var {data} = await postMessage(portal, message,
"http://{{hosts[alt][www]}}:{{ports[http][0]}}");
assert_equals(data, message);
}, "postMessage received by portal host in cross-origin portal when target origin is specified");

promise_test(async () => {
var portal = await createAndInsertPortal(sameOriginUrl);
var message = {
Expand All @@ -101,13 +86,6 @@
assert_equals(data, message);
}, "postMessage with message ports and same-origin portal");

promise_test(async () => {
var portal = await createAndInsertPortal(crossOriginUrl);
var message = "test message";
var {data} = await postMessageWithMessagePorts(portal, message, "*");
assert_equals(data, message);
}, "postMessage with message ports and cross-origin portal");

promise_test(async () => {
var portal = await createAndInsertPortal(sameOriginUrl);
var arrayBuffer = new ArrayBuffer(5);
Expand Down Expand Up @@ -206,26 +184,18 @@
assert_equals(error, "InvalidStateError");
}, "postMessage after activate throws error");

// TODO(adithyas): Use async_test instead of promise_test (for tests that
// use a timeout) once we implement postMessage in the other direction and
// no longer need to use broadcast channel.
const TIMEOUT_DURATION_MS = 1000;

promise_test(async t => {
var portal = await createAndInsertPortal(crossOriginUrl);
return new Promise((resolve, reject) => {
postMessage(portal, "test").then(() => { reject("message delivered"); });
t.step_timeout(resolve, TIMEOUT_DURATION_MS);
});
}, "message should not be delivered to cross-origin portal when targetOrigin is not specified");
const key = token();
const portal = await createAndInsertPortal(`${crossOriginUrl}?key=${key}`);
portal.postMessage('test message', '*');
t.step_timeout(() => {
StashUtils.putValue(key, 'passed');
}, TIMEOUT_DURATION_MS);
const result = await StashUtils.takeValue(key);
assert_equals(result, 'passed');
}, 'postMessage should be blocked for cross-origin portals');

promise_test(async t => {
var portal = await createAndInsertPortal(sameOriginUrl);
return new Promise((resolve, reject) => {
postMessage(portal, "test", "http://differentorigin.com:8002").then(
() => { reject("message delivered"); });
t.step_timeout(resolve, TIMEOUT_DURATION_MS);
});
}, "message should not be delivered to portal when targetOrigin does not match");
</script>
</body>
4 changes: 4 additions & 0 deletions portals/resources/portal-host-post-message-x-origin.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<!DOCTYPE html>
<script>
window.portalHost.postMessage('test message', '*');
</script>
11 changes: 11 additions & 0 deletions portals/resources/portal-post-message-x-origin-portal.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!DOCTYPE html>
<script src="stash-utils.sub.js"></script>
<script>
const queryParams = new URLSearchParams(window.location.search);
const key = queryParams.get('key');
if (key) {
window.portalHost.onmessage = () => {
StashUtils.putValue(key, 'failed');
};
}
</script>
2 changes: 1 addition & 1 deletion portals/resources/stash-utils.sub.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const STASH_RESPONDER = "ws://{{host}}:{{ports[ws][0]}}/stash_responder_blocking";
const STASH_RESPONDER = "wss://{{host}}:{{ports[wss][0]}}/stash_responder_blocking";

class StashUtils {
/**
Expand Down

0 comments on commit f346773

Please sign in to comment.