Start allowing some cross-origin scripting.

There are a few things allowed in cross origin scripting, the most important
being window.postMessage and window.parent.

This commit changes window-returning functions (e.g. window.top, window.parent
iframe.contentWindow) from always returning a *Window, to conditionally
returning a *Window or a *CrossOriginWindow. The CrossOriginWindow only allows
a few methods (e.g. postMessage).
This commit is contained in:
Karl Seguin
2026-03-24 12:20:18 +08:00
parent 35be9f897f
commit b19f30d865
6 changed files with 135 additions and 7 deletions

View File

@@ -299,7 +299,9 @@ pub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void
._performance = Performance.init(), ._performance = Performance.init(),
._screen = screen, ._screen = screen,
._visual_viewport = visual_viewport, ._visual_viewport = visual_viewport,
._cross_origin_wrapper = undefined,
}); });
self.window._cross_origin_wrapper = .{ .window = self.window };
self._style_manager = try StyleManager.init(self); self._style_manager = try StyleManager.init(self);
errdefer self._style_manager.deinit(); errdefer self._style_manager.deinit();

View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<iframe src=support/frame1.html></iframe>
<script id=post_message type=module>
const state = await testing.async();
{
const ALT_BASE = testing.BASE_URL.replace('127.0.0.1', 'localhost');
{
let iframe2 = document.createElement('iframe');
iframe2.src = ALT_BASE + 'window/support/frame1.html';
document.documentElement.appendChild(iframe2);
}
{
let iframe3 = document.createElement('iframe');
iframe3.src = ALT_BASE + 'window/support/frame2.html';
document.documentElement.appendChild(iframe3);
}
let captures = [];
window.addEventListener('message', (e) => {
captures.push(e.data);
if (captures.length == 3) {
state.resolve();
}
});
await state.done(() => {
const expected_urls = [
testing.BASE_URL + 'window/support/frame1.html',
ALT_BASE + 'window/support/frame1.html',
ALT_BASE + 'window/support/frame2.html',
];
// No strong order guarantee for messaages, and we don't care about the order
// so long as it's the correct data.
testing.expectEqual(expected_urls.sort(), captures.map((c) => {return c.url}).sort());
captures.forEach((c) => {
if (c.url.includes(testing.BASE_URL)) {
testing.expectEqual(false, c.document_is_undefined);
} else {
testing.expectEqual(true, c.document_is_undefined);
}
});
});
}
</script>

View File

@@ -0,0 +1,7 @@
<!DOCTYPE html>
<script>
window.parent.postMessage({
url: location.toString(),
document_is_undefined: window.parent.document === undefined,
}, '*')
</script>

View File

@@ -0,0 +1,7 @@
<!DOCTYPE html>
<script>
window.top.postMessage({
url: location.toString(),
document_is_undefined: window.top.document === undefined,
}, '*')
</script>

View File

@@ -49,6 +49,10 @@ const IS_DEBUG = builtin.mode == .Debug;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
pub fn registerTypes() []const type {
return &.{ Window, CrossOriginWindow };
}
const Window = @This(); const Window = @This();
_proto: *EventTarget, _proto: *EventTarget,
@@ -87,6 +91,8 @@ _scroll_pos: struct {
.y = 0, .y = 0,
.state = .done, .state = .done,
}, },
// A cross origin wrapper for this window
_cross_origin_wrapper: CrossOriginWindow,
pub fn asEventTarget(self: *Window) *EventTarget { pub fn asEventTarget(self: *Window) *EventTarget {
return self._proto; return self._proto;
@@ -104,19 +110,19 @@ pub fn getWindow(self: *Window) *Window {
return self; return self;
} }
pub fn getTop(self: *Window) *Window { pub fn getTop(self: *Window, page: *Page) Access {
var p = self._page; var p = self._page;
while (p.parent) |parent| { while (p.parent) |parent| {
p = parent; p = parent;
} }
return p.window; return Access.init(page.window, p.window);
} }
pub fn getParent(self: *Window) *Window { pub fn getParent(self: *Window, page: *Page) Access {
if (self._page.parent) |p| { if (self._page.parent) |p| {
return p.window; return Access.init(page.window, p.window);
} }
return self; return .{ .window = self };
} }
pub fn getDocument(self: *Window) *Document { pub fn getDocument(self: *Window) *Document {
@@ -606,6 +612,25 @@ pub fn unhandledPromiseRejection(self: *Window, no_handler: bool, rejection: js.
} }
} }
pub const Access = union(enum) {
window: *Window,
cross_origin: *CrossOriginWindow,
pub fn init(callee: *Window, accessing: *Window) Access {
if (callee == accessing) {
// common enough that it's worth the check
return .{ .window = accessing };
}
if (callee._page.js.origin == accessing._page.js.origin) {
// two different windows, but same origin, return the full window
return .{ .window = accessing };
}
return .{ .cross_origin = &accessing._cross_origin_wrapper };
}
};
const ScheduleOpts = struct { const ScheduleOpts = struct {
repeat: bool, repeat: bool,
params: []js.Value.Temp, params: []js.Value.Temp,
@@ -892,6 +917,41 @@ pub const JsApi = struct {
}.prompt, .{}); }.prompt, .{});
}; };
const CrossOriginWindow = struct {
window: *Window,
pub fn postMessage(self: *CrossOriginWindow, message: js.Value.Temp, target_origin: ?[]const u8, page: *Page) !void {
return self.window.postMessage(message, target_origin, page);
}
pub fn getTop(self: *CrossOriginWindow, page: *Page) Access {
return self.window.getParent(page);
}
pub fn getParent(self: *CrossOriginWindow, page: *Page) Access {
return self.window.getParent(page);
}
pub fn getFramesLength(self: *const CrossOriginWindow) u32 {
return self.window.getFramesLength();
}
pub const JsApi = struct {
pub const bridge = js.Bridge(CrossOriginWindow);
pub const Meta = struct {
pub const name = "CrossOriginWindow";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const postMessage = bridge.function(CrossOriginWindow.postMessage, .{});
pub const top = bridge.accessor(CrossOriginWindow.getTop, null, .{});
pub const parent = bridge.accessor(CrossOriginWindow.getParent, null, .{});
pub const length = bridge.accessor(CrossOriginWindow.getFramesLength, null, .{});
};
};
const testing = @import("../../testing.zig"); const testing = @import("../../testing.zig");
test "WebApi: Window" { test "WebApi: Window" {
try testing.htmlRunner("window", .{}); try testing.htmlRunner("window", .{});

View File

@@ -39,8 +39,9 @@ pub fn asNode(self: *IFrame) *Node {
return self.asElement().asNode(); return self.asElement().asNode();
} }
pub fn getContentWindow(self: *const IFrame) ?*Window { pub fn getContentWindow(self: *const IFrame, page: *Page) ?Window.Access {
return self._window; const frame_window = self._window orelse return null;
return Window.Access.init(page.window, frame_window);
} }
pub fn getContentDocument(self: *const IFrame) ?*Document { pub fn getContentDocument(self: *const IFrame) ?*Document {