webapi: Fix cross-origin MessageEvent.source WindowProxy identity

MessageEvent.getSource returned the bare *Window, the only window accessor
that did not go through Window.Access.init. Every other accessor
(iframe.contentWindow, window.parent, window.top) wraps a cross-origin target
as *CrossOriginWindow. JS object identity is keyed by Zig pointer
(identity_map), so cross-origin event.source (the *Window) and
iframe.contentWindow (&window._cross_origin_wrapper) resolved to different JS
objects: `event.source === iframe.contentWindow` was false cross-origin while
true same-origin.

The WHATWG HTML spec requires these to be the same object regardless of origin
(one WindowProxy per browsing context; MessageEvent.source is that
WindowProxy). The mismatch breaks postMessage handshakes that authenticate the
sender by reference identity, e.g. keycloak-js's 3rd-party-cookie check
(`if (iframe.contentWindow !== event.source) return;`), which then times out
and fails OIDC init.

Route getSource through Window.Access.init(frame.window, source) — exactly like
IFrame.getContentWindow — so both paths resolve to the same per-frame proxy.
Same-origin behaviour is unchanged. Verified against Chrome 149 (identity holds
cross- and same-origin). Adds a regression test, cross_origin_message_source.html.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Boyd Ebsworthy
2026-06-26 17:51:54 +02:00
parent 97cef1e900
commit b94052dfa7
3 changed files with 57 additions and 2 deletions

View File

@@ -0,0 +1,49 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<!--
MessageEvent.source is the WindowProxy of the frame the message came from, so it
is reference-equal to that frame's iframe.contentWindow — for a cross-origin
sender as well as a same-origin one. The two children below differ only by origin:
localhost is cross-origin to the 127.0.0.1 test page, 127.0.0.1 is same-origin;
both must satisfy event.source === iframe.contentWindow.
-->
<script id="message_source_identity" type=module>
{
const state = await testing.async();
const ALT_BASE = testing.BASE_URL.replace('127.0.0.1', 'localhost');
const ALT_ORIGIN = testing.ORIGIN.replace('127.0.0.1', 'localhost');
function loadAndAwait(src, expectedOrigin) {
return new Promise((resolve) => {
const iframe = document.createElement('iframe');
const onmsg = (e) => {
if (e.origin !== expectedOrigin) return;
window.removeEventListener('message', onmsg);
resolve({ iframe, event: e });
};
window.addEventListener('message', onmsg);
iframe.src = src;
document.documentElement.appendChild(iframe);
});
}
// localhost is a different origin than the 127.0.0.1 test page: cross-origin sender.
const cross = await loadAndAwait(ALT_BASE + 'frames/support/post_to_parent.html', ALT_ORIGIN);
// 127.0.0.1 is the test page's own origin: same-origin control.
const same = await loadAndAwait(testing.BASE_URL + 'frames/support/post_to_parent.html', testing.ORIGIN);
state.resolve();
await state.done(() => {
testing.expectEqual('hello', cross.event.data);
testing.expectEqual(ALT_ORIGIN, cross.event.origin);
testing.expectFalse(cross.event.source === null);
testing.expectTrue(cross.event.source === cross.iframe.contentWindow);
testing.expectEqual('hello', same.event.data);
testing.expectEqual(testing.ORIGIN, same.event.origin);
testing.expectTrue(same.event.source === same.iframe.contentWindow);
});
}
</script>

View File

@@ -0,0 +1,4 @@
<!DOCTYPE html>
<script>
parent.postMessage('hello', '*');
</script>

View File

@@ -21,6 +21,7 @@ const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Frame = @import("../../Frame.zig");
const Event = @import("../Event.zig");
const MessagePort = @import("../MessagePort.zig");
@@ -116,8 +117,9 @@ pub fn getOrigin(self: *const MessageEvent) []const u8 {
return self._origin;
}
pub fn getSource(self: *const MessageEvent) ?*Window {
return self._source;
pub fn getSource(self: *const MessageEvent, frame: *Frame) ?Window.Access {
const source = self._source orelse return null;
return Window.Access.init(frame.window, source);
}
pub fn getPorts(self: *const MessageEvent) []const *MessagePort {