From 238d4a53a32f4992beaf34eaa52f3f6711621981 Mon Sep 17 00:00:00 2001 From: nikneym Date: Thu, 17 Sep 2026 16:08:30 +0300 Subject: [PATCH] `cdp`: move a kept isolated-world context onto the navigated origin --- .../tests/cdp/isolated_world_redirect.html | 1 + src/server/cdp/domains/page.zig | 94 ++++++++++++++++++- src/testing.zig | 13 +++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 src/browser/tests/cdp/isolated_world_redirect.html diff --git a/src/browser/tests/cdp/isolated_world_redirect.html b/src/browser/tests/cdp/isolated_world_redirect.html new file mode 100644 index 000000000..7f7205a01 --- /dev/null +++ b/src/browser/tests/cdp/isolated_world_redirect.html @@ -0,0 +1 @@ +Parent jobs diff --git a/src/server/cdp/domains/page.zig b/src/server/cdp/domains/page.zig index 49ce8b888..e2dac0960 100644 --- a/src/server/cdp/domains/page.zig +++ b/src/server/cdp/domains/page.zig @@ -778,7 +778,11 @@ pub fn frameNavigated(arena: Allocator, bc: *CDP.BrowserContext, event: *const N // navigation as blink rebuilds a detached isolated-world window proxy. for (bc.isolated_worlds.items) |isolated_world| { if (isolated_world.contextFor(frame)) |js_context| { - // The context was already created ahead of time (createIsolatedWorld). + // The context was already created ahead of time and still carries + // the origin the frame had then. Move it onto the navigated origin. + js_context.setOrigin(frame.origin) catch |err| { + log.warn(.cdp, "isolated world origin", .{ .err = err, .world = isolated_world.name, .frame_id = frame._frame_id }); + }; // A child keeps the id the client was given. The root's id was just // invalidated by executionContextsCleared: the first navigation of a // pristine about:blank keeps the Frame and its contexts. @@ -1543,6 +1547,94 @@ test "cdp.frame: isolated world survives the in-place first navigation" { try ctx.expectSentResult(.{ .result = .{ .type = "string", .value = "Jobs page one" } }, .{ .id = 45 }); } +// #1550: the kept context was created on the bootstrap about:blank and still +// carried its opaque origin after the in-place navigation. A world context +// shares its frame's Origin (same V8 security token, one Page.origins entry). +test "cdp.frame: isolated world follows the in-place first navigation's origin" { + var ctx = try testing.context(); + defer ctx.deinit(); + + try ctx.processMessage(.{ .id = 40, .method = "Target.setAutoAttach", .params = .{ .autoAttach = true, .waitForDebuggerOnStart = false } }); + try ctx.processMessage(.{ .id = 41, .method = "Target.createTarget", .params = .{ .url = "about:blank" } }); + const bc = &ctx.cdp().browser_context.?; + const session_id = bc.session_id.?; + const root = bc.mainFrame() orelse unreachable; + const root_id = id.toFrameId(root._frame_id); + + try ctx.processMessage(.{ .id = 42, .method = "Runtime.enable", .sessionId = session_id }); + try ctx.processMessage(.{ .id = 43, .method = "Page.createIsolatedWorld", .sessionId = session_id, .params = .{ + .frameId = &root_id, + .worldName = "utility", + .grantUniveralAccess = true, + } }); + const world = bc.isolated_worlds.items[0]; + + // The bootstrap about:blank has an opaque origin: one Origin per context. + try testing.expectEqual(null, root.origin); + try testing.expect(world.contextFor(root).?.origin != root.js.origin); + + try ctx.processMessage(.{ .id = 44, .method = "Page.navigate", .sessionId = session_id, .params = .{ + .url = "http://127.0.0.1:9582/src/browser/tests/cdp/isolated_world_one.html", + } }); + try testing.waitForPage(bc); + try testing.expectEqual(root, bc.mainFrame().?); + try testing.expectEqualSlices(u8, "http://127.0.0.1:9582", root.origin.?); + + // Kept context, now on the navigated origin with the main world. + const world_context = world.contextFor(root) orelse return error.ContextNotFound; + try testing.expectEqual(root.js.origin, world_context.origin); + try testing.expectEqualSlices(u8, "http://127.0.0.1:9582", world_context.origin.key); +} + +// A child's world is created while its load is in flight (drivers do it on +// Page.frameAttached). A cross-origin redirect then moves frame.origin before +// frame_navigated: the kept context must follow, as the root's does. +test "cdp.frame: isolated world follows a child's redirected origin" { + var ctx = try testing.context(); + defer ctx.deinit(); + + const bc = try ctx.loadBrowserContext(.{ .id = "BID-IW4", .url = "hi.html", .target_id = "FID-000000000X".* }); + try ctx.processMessage(.{ .id = 70, .method = "Runtime.enable", .sessionId = "SID-X" }); + try ctx.processMessage(.{ .id = 71, .method = "Page.navigate", .sessionId = "SID-X", .params = .{ + .url = "http://127.0.0.1:9582/src/browser/tests/cdp/isolated_world_redirect.html", + } }); + + // The parent is parsed while the child's redirected, server-delayed + // response has yet to arrive: the child exists on its request origin. + var runner = bc.session.runner(.{}); + try runner.waitForFrame(bc.page_handle.?.frame_id, 2000, .{ .until = .domcontentloaded }); + const root = bc.mainFrame() orelse unreachable; + try testing.expectEqual(1, root.child_frames.items.len); + const child = root.child_frames.items[0]; + const child_id = id.toFrameId(child._frame_id); + try testing.expectEqualSlices(u8, "http://127.0.0.1:9582", child.origin.?); + + try ctx.processMessage(.{ .id = 72, .method = "Page.createIsolatedWorld", .params = .{ + .frameId = &child_id, + .worldName = "utility", + .grantUniveralAccess = true, + } }); + const child_ctx = try isolatedWorldContextId(bc, child); + const world = bc.isolated_worlds.items[0]; + try testing.expectEqualSlices(u8, "http://127.0.0.1:9582", world.contextFor(child).?.origin.key); + + try testing.waitForPage(bc); + try testing.expectEqual(child, root.child_frames.items[0]); + try testing.expectEqualSlices(u8, "http://localhost:9582", child.origin.?); + + // Same context (the child keeps its id), now on the navigated origin. + try testing.expectEqual(child_ctx, try isolatedWorldContextId(bc, child)); + const world_context = world.contextFor(child) orelse return error.ContextNotFound; + try testing.expectEqual(child.js.origin, world_context.origin); + try testing.expectEqualSlices(u8, "http://localhost:9582", world_context.origin.key); + + try ctx.processMessage(.{ .id = 73, .method = "Runtime.evaluate", .sessionId = "SID-X", .params = .{ + .expression = "document.title", + .contextId = child_ctx, + } }); + try ctx.expectSentResult(.{ .result = .{ .type = "string", .value = "Jobs page one" } }, .{ .id = 73 }); +} + // A committed root navigation tears the old Page down later, with the same // frame id as the live page. That teardown must not take the live page's // world context with it. diff --git a/src/testing.zig b/src/testing.zig index bea812062..f2c578c9d 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -744,6 +744,19 @@ fn testHTTPHandler(req: *std.http.Server.Request) !void { }); } + // Bounces to the same path on the other loopback host, e.g. for an iframe + // whose origin must change between its request and its response. + if (std.mem.startsWith(u8, path, "/redirect-cross-origin/")) { + var location_buf: [1024]u8 = undefined; + const location = try std.fmt.bufPrint(&location_buf, "http://localhost:9582/{s}", .{path["/redirect-cross-origin/".len..]}); + return req.respond("", .{ + .status = .found, + .extra_headers = &.{ + .{ .name = "Location", .value = location }, + }, + }); + } + if (std.mem.eql(u8, path, "/echo-x-hop")) { var it = req.iterateHeaders(); var value: []const u8 = "NONE";