mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-09-17 08:27:11 -04:00
Merge pull request #3293 from lightpanda-io/nikneym/samesite-strict-bypass
compute site-for-cookies from ancestor chain of a `Frame`
This commit is contained in:
21 files changed
+373
-83
No files matched your search
+85
-15
@@ -43,6 +43,7 @@ const EventTarget = @import("webapi/EventTarget.zig");
|
||||
const Element = @import("webapi/Element.zig");
|
||||
const HtmlElement = @import("webapi/element/Html.zig");
|
||||
const Window = @import("webapi/Window.zig");
|
||||
const Cookie = @import("webapi/storage/Cookie.zig");
|
||||
const Location = @import("webapi/Location.zig");
|
||||
const Document = @import("webapi/Document.zig");
|
||||
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
||||
@@ -603,12 +604,29 @@ fn referrerSource(self: *const Frame) [:0]const u8 {
|
||||
var frame = self;
|
||||
while (std.mem.startsWith(u8, frame.url, "about:")) {
|
||||
// about:blank and about:srcdoc documents aren't valid referrer sources,
|
||||
// use the parents
|
||||
// use the parents.
|
||||
frame = frame.parent orelse return frame.url;
|
||||
}
|
||||
return frame.url;
|
||||
}
|
||||
|
||||
// RFC 6265bis "site for cookies" for SameSite checks on requests this frame does.
|
||||
pub fn siteForCookies(self: *const Frame) Cookie.SiteForCookies {
|
||||
const own_url = self.referrerSource();
|
||||
const own_host = URL.getHostname(own_url);
|
||||
var frame: *const Frame = self;
|
||||
while (frame.parent) |parent| : (frame = parent) {
|
||||
const parent_host = URL.getHostname(parent.url);
|
||||
if (parent_host.len == 0) {
|
||||
continue;
|
||||
}
|
||||
if (Cookie.areHostsSameSite(parent_host, own_host) == false) {
|
||||
return .none;
|
||||
}
|
||||
}
|
||||
return .{ .url = own_url };
|
||||
}
|
||||
|
||||
pub fn getTitle(self: *Frame) !?[]const u8 {
|
||||
if (self.window._document.is(Document.HTMLDocument)) |html_doc| {
|
||||
return try html_doc.getTitle(self);
|
||||
@@ -841,7 +859,7 @@ pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !vo
|
||||
.skip_cache = self.parent == null,
|
||||
.throttle = self.parent == null,
|
||||
.cookie_jar = &session.cookie_jar,
|
||||
.cookie_origin = opts.initiator_url orelse self.url,
|
||||
.cookie_origin = opts.initiator_url orelse .{ .url = self.url },
|
||||
.resource_type = .document,
|
||||
.notification = self._session.notification,
|
||||
.header_callback = frameHeaderDoneCallback,
|
||||
@@ -1039,8 +1057,20 @@ fn scheduleNavigationWithArena(originator: *Frame, arena: *lp.Arena, request_url
|
||||
nav_opts.referer = try referrer.compute(arena.allocator(), originator.referrer_policy, referrer_source, resolved_url);
|
||||
nav_opts.referrer_policy = originator.referrer_policy;
|
||||
}
|
||||
if (nav_opts.initiator_url == null) {
|
||||
nav_opts.initiator_url = try arena.dupeZ(u8, referrer_source);
|
||||
}
|
||||
if (nav_opts.initiator_url == null) {
|
||||
if (target.parent) |parent| {
|
||||
// A subframe navigation's SameSite initiator is the frame's whole
|
||||
// ancestor chain, not the document that triggered the navigation.
|
||||
if (std.mem.startsWith(u8, parent.referrerSource(), "http")) {
|
||||
nav_opts.initiator_url = switch (parent.siteForCookies()) {
|
||||
.none => .none,
|
||||
.url => |u| .{ .url = try arena.dupeZ(u8, u) },
|
||||
};
|
||||
}
|
||||
} else if (std.mem.startsWith(u8, referrer_source, "http")) {
|
||||
// Top-level navigation.
|
||||
nav_opts.initiator_url = .{ .url = try arena.dupeZ(u8, referrer_source) };
|
||||
}
|
||||
}
|
||||
if (nav_opts.initiator_origin == null) {
|
||||
@@ -1981,17 +2011,15 @@ pub fn iframeAddedCallback(self: *Frame, iframe: *IFrame) !void {
|
||||
self.child_frames_sorted = false;
|
||||
|
||||
// Iframe's initial src request carries the parent's URL as Referer
|
||||
// (subject to the parent's Referrer-Policy) and as the SameSite
|
||||
// initiator. When this frame is itself an about: document, the nearest
|
||||
// ancestor's URL is the referrer source. Parent frame outlives this
|
||||
// navigate() call, so the slice is safe; navigate dupes what it keeps.
|
||||
// (subject to the parent's Referrer-Policy). The SameSite initiator is
|
||||
// the parent's ancestor chain.
|
||||
const referrer_source = self.referrerSource();
|
||||
const parent_url: ?[:0]const u8 = if (std.mem.startsWith(u8, referrer_source, "http")) referrer_source else null;
|
||||
const initiator_url: ?Cookie.SiteForCookies = if (std.mem.startsWith(u8, referrer_source, "http")) self.siteForCookies() else null;
|
||||
new_frame.navigate(url, .{
|
||||
.reason = .initialFrameNavigation,
|
||||
.referer = try referrer.compute(self.call_arena, self.referrer_policy, referrer_source, url),
|
||||
.referrer_policy = self.referrer_policy,
|
||||
.initiator_url = parent_url,
|
||||
.initiator_url = initiator_url,
|
||||
.initiator_origin = self.origin,
|
||||
}) catch |err| {
|
||||
// extra defensive..maybe navigate added a new frame, and the index it
|
||||
@@ -2346,7 +2374,7 @@ pub fn loadExternalStylesheet(self: *Frame, link: *Element.Html.Link, href: []co
|
||||
.frame_id = self._frame_id,
|
||||
.loader_id = self._loader_id,
|
||||
.cookie_jar = &session.cookie_jar,
|
||||
.cookie_origin = self.url,
|
||||
.cookie_origin = self.siteForCookies(),
|
||||
.resource_type = .stylesheet,
|
||||
.notification = session.notification,
|
||||
.shutdown_callback = HttpClient.noopShutdown, // syncRequest installs its own
|
||||
@@ -3362,11 +3390,14 @@ pub const NavigateOpts = struct {
|
||||
// can recompute the header. null (e.g. a CDP-supplied referrer) leaves
|
||||
// the Referer untouched across redirects.
|
||||
referrer_policy: ?referrer.Policy = null,
|
||||
// The URL of the document that initiated this navigation, used as the
|
||||
// "site for cookies" when computing SameSite. Distinct from `referer`
|
||||
// The "site for cookies" of the document that initiated this navigation,
|
||||
// used when computing SameSite: for a subframe, the whole ancestor
|
||||
// chain's site (or .none when that chain crosses sites); for a top-level
|
||||
// navigation, the initiating document's URL. Distinct from `referer`
|
||||
// because a Referrer-Policy can suppress the Referer header without
|
||||
// affecting SameSite (which always considers the real initiator).
|
||||
initiator_url: ?[:0]const u8 = null,
|
||||
// affecting SameSite (which always considers the real initiator). null
|
||||
// means browser-initiated, which cookie lookup treats as same-site.
|
||||
initiator_url: ?Cookie.SiteForCookies = null,
|
||||
initiator_origin: ?[]const u8 = null,
|
||||
force: bool = false,
|
||||
kind: NavigationKind = .{ .push = null },
|
||||
@@ -3801,6 +3832,45 @@ test "Page: isSameOrigin" {
|
||||
try testing.expectEqual(false, frame.isSameOrigin("//origin.com/foo"));
|
||||
}
|
||||
|
||||
test "Frame: siteForCookies" {
|
||||
var top: Frame = undefined;
|
||||
top.parent = null;
|
||||
top.url = "http://attacker.example/attacker-nested";
|
||||
|
||||
var middle: Frame = undefined;
|
||||
middle.parent = ⊤
|
||||
middle.url = "http://victim.example/nested-middle";
|
||||
|
||||
var inner: Frame = undefined;
|
||||
inner.parent = &middle;
|
||||
inner.url = "http://victim.example/inner";
|
||||
|
||||
// A top-level document is its own site.
|
||||
try testing.expectEqual("http://attacker.example/attacker-nested", top.siteForCookies().url);
|
||||
|
||||
// Cross-site with the top-level document: no site for cookies — for the
|
||||
// directly-embedded frame and for the same-site-with-parent frame nested
|
||||
// under it alike.
|
||||
try testing.expectEqual(true, middle.siteForCookies() == .none);
|
||||
try testing.expectEqual(true, inner.siteForCookies() == .none);
|
||||
|
||||
// A fully same-site chain (subdomains included) keeps its site.
|
||||
top.url = "http://victim.example/";
|
||||
middle.url = "http://sub.victim.example/nested-middle";
|
||||
try testing.expectEqual("http://victim.example/inner", inner.siteForCookies().url);
|
||||
try testing.expectEqual("http://sub.victim.example/nested-middle", middle.siteForCookies().url);
|
||||
|
||||
// about: documents inherit their creator's origin: transparent as an
|
||||
// ancestor, and judged through their nearest real ancestor themselves.
|
||||
middle.url = "about:blank";
|
||||
try testing.expectEqual("http://victim.example/inner", inner.siteForCookies().url);
|
||||
try testing.expectEqual("http://victim.example/", middle.siteForCookies().url);
|
||||
|
||||
top.url = "http://attacker.example/";
|
||||
try testing.expectEqual(true, inner.siteForCookies() == .none);
|
||||
try testing.expectEqual("http://attacker.example/", middle.siteForCookies().url);
|
||||
}
|
||||
|
||||
test "Frame: static immediate meta refresh navigates" {
|
||||
const page = try testing.pageTest("fixtures/meta_refresh.html", .{});
|
||||
defer page.close();
|
||||
|
||||
@@ -137,7 +137,7 @@ pub fn preloadScript(self: *ScriptManager, element: ?*Element.Html, url: []const
|
||||
.frame_id = frame._frame_id,
|
||||
.loader_id = frame._loader_id,
|
||||
.cookie_jar = &frame._session.cookie_jar,
|
||||
.cookie_origin = frame.url,
|
||||
.cookie_origin = frame.siteForCookies(),
|
||||
.resource_type = .script,
|
||||
.notification = frame._session.notification,
|
||||
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
||||
@@ -353,7 +353,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
||||
.frame_id = frame._frame_id,
|
||||
.loader_id = frame._loader_id,
|
||||
.cookie_jar = &frame._session.cookie_jar,
|
||||
.cookie_origin = frame.url,
|
||||
.cookie_origin = frame.siteForCookies(),
|
||||
.resource_type = .script,
|
||||
.notification = frame._session.notification,
|
||||
.shutdown_callback = HttpClient.noopShutdown, // syncRequest installs its own
|
||||
@@ -402,7 +402,7 @@ pub fn addFromElement(self: *ScriptManager, comptime from_parser: bool, script_e
|
||||
.frame_id = frame._frame_id,
|
||||
.loader_id = frame._loader_id,
|
||||
.cookie_jar = &frame._session.cookie_jar,
|
||||
.cookie_origin = frame.url,
|
||||
.cookie_origin = frame.siteForCookies(),
|
||||
.resource_type = .script,
|
||||
.notification = frame._session.notification,
|
||||
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
||||
|
||||
@@ -27,6 +27,7 @@ const Session = @import("Session.zig");
|
||||
const Frame = @import("Frame.zig");
|
||||
const ImportMap = @import("ImportMap.zig");
|
||||
const WorkerGlobalScope = @import("webapi/WorkerGlobalScope.zig");
|
||||
const Cookie = @import("webapi/storage/Cookie.zig");
|
||||
|
||||
const Element = @import("webapi/Element.zig");
|
||||
|
||||
@@ -78,6 +79,14 @@ pub const Owner = union(enum) {
|
||||
inline else => |g| g.makeRequest(req),
|
||||
};
|
||||
}
|
||||
|
||||
// `Execution.siteForCookies` ditto.
|
||||
pub fn siteForCookies(self: Owner) Cookie.SiteForCookies {
|
||||
return switch (self) {
|
||||
.frame => |frame| frame.siteForCookies(),
|
||||
.worker => |worker| worker.site_for_cookies,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
owner: Owner,
|
||||
@@ -265,7 +274,7 @@ pub fn preloadImport(self: *ScriptManagerBase, url: [:0]const u8, referrer: []co
|
||||
.frame_id = owner.frameId(),
|
||||
.loader_id = owner.loaderId(),
|
||||
.cookie_jar = &session.cookie_jar,
|
||||
.cookie_origin = owner.url(),
|
||||
.cookie_origin = owner.siteForCookies(),
|
||||
.resource_type = .script,
|
||||
.notification = session.notification,
|
||||
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
||||
@@ -460,7 +469,7 @@ pub fn getAsyncImport(self: *ScriptManagerBase, url: [:0]const u8, cb: ImportAsy
|
||||
.loader_id = owner.loaderId(),
|
||||
.resource_type = .script,
|
||||
.cookie_jar = &session.cookie_jar,
|
||||
.cookie_origin = owner.url(),
|
||||
.cookie_origin = owner.siteForCookies(),
|
||||
.notification = session.notification,
|
||||
.start_callback = if (log.enabled(.http, .debug)) Script.startCallback else null,
|
||||
.header_callback = Script.headerCallback,
|
||||
|
||||
@@ -82,7 +82,7 @@ pub fn image(frame: *Frame, img: *Element.Html.Image, src: []const u8) !void {
|
||||
.frame_id = frame._frame_id,
|
||||
.loader_id = frame._loader_id,
|
||||
.cookie_jar = &session.cookie_jar,
|
||||
.cookie_origin = frame.url,
|
||||
.cookie_origin = frame.siteForCookies(),
|
||||
.resource_type = .image,
|
||||
.notification = session.notification,
|
||||
.headers_only = true,
|
||||
|
||||
@@ -37,6 +37,7 @@ const HttpClient = @import("../../network/HttpClient.zig");
|
||||
const EventManagerBase = @import("../EventManagerBase.zig");
|
||||
|
||||
const Console = @import("../webapi/Console.zig");
|
||||
const Cookie = @import("../webapi/storage/Cookie.zig");
|
||||
const Event = @import("../webapi/Event.zig");
|
||||
const EventTarget = @import("../webapi/EventTarget.zig");
|
||||
const Performance = @import("../webapi/Performance.zig");
|
||||
@@ -132,6 +133,14 @@ pub fn origin(self: *const Execution) ?[]const u8 {
|
||||
};
|
||||
}
|
||||
|
||||
// a Worker inherits its creating document's; `Frame` walks its ancestor chain.
|
||||
pub fn siteForCookies(self: *const Execution) Cookie.SiteForCookies {
|
||||
return switch (self.js.global) {
|
||||
.frame => |frame| frame.siteForCookies(),
|
||||
.worker => |worker| worker.site_for_cookies,
|
||||
};
|
||||
}
|
||||
|
||||
// HttpClient.Owner of the current global (Frame or WGS). Used by code
|
||||
// that needs to register an in-flight network operation against the
|
||||
// owning scope without caring whether it's a Frame or a Worker — e.g.
|
||||
|
||||
@@ -294,7 +294,8 @@ pub fn getCookie(self: *Document, frame: *Frame) ![]const u8 {
|
||||
var aw: std.Io.Writer.Allocating = .init(frame.local_arena);
|
||||
try frame._session.cookie_jar.forRequest(frame.url, &aw.writer, .{
|
||||
.is_http = false,
|
||||
.is_navigation = true,
|
||||
.is_navigation = false,
|
||||
.origin_url = frame.siteForCookies(),
|
||||
});
|
||||
return aw.written();
|
||||
}
|
||||
@@ -314,6 +315,10 @@ pub fn setCookie(self: *Document, cookie_str: []const u8, frame: *Frame) ![]cons
|
||||
c.deinit();
|
||||
return ""; // HttpOnly cookies cannot be set from JS
|
||||
}
|
||||
if (c.same_site != .none and frame.siteForCookies() == .none) {
|
||||
c.deinit();
|
||||
return ""; // SameSite cookies cannot be set from a cross-site context.
|
||||
}
|
||||
try frame._session.cookie_jar.add(c, lp.datetime.timestamp(.real), false);
|
||||
return cookie_str;
|
||||
}
|
||||
@@ -1641,6 +1646,53 @@ test "WebApi: Document.evaluate" {
|
||||
try testing.htmlRunner("xpath/document_evaluate.html", .{});
|
||||
}
|
||||
|
||||
test "Document: cookie access from a cross-site frame" {
|
||||
defer testing.test_session.closeAllPages();
|
||||
const frame = try testing.createFrame();
|
||||
const doc = frame.document;
|
||||
const jar = &frame._session.cookie_jar;
|
||||
defer jar.clearRetainingCapacity();
|
||||
|
||||
// victim.example embedded by attacker.example: the ancestor chain is
|
||||
// cross-site, so the frame has no site for cookies.
|
||||
var top: Frame = undefined;
|
||||
top.parent = null;
|
||||
top.url = "https://attacker.example/";
|
||||
frame.url = "https://victim.example/inner";
|
||||
frame.parent = ⊤
|
||||
defer frame.parent = null;
|
||||
|
||||
try jar.populateFromResponse("https://victim.example/", "strict=1; SameSite=Strict");
|
||||
try jar.populateFromResponse("https://victim.example/", "lax=2; SameSite=Lax");
|
||||
try jar.populateFromResponse("https://victim.example/", "default=3");
|
||||
try jar.populateFromResponse("https://victim.example/", "none=4; SameSite=None; Secure");
|
||||
|
||||
// Reads: only SameSite=None is visible from a cross-site context. Lax
|
||||
// gets no navigation exception for script access.
|
||||
try testing.expectEqual("none=4", try doc.getCookie(frame));
|
||||
|
||||
// Writes: SameSite=None is stored, everything else (including the Lax
|
||||
// default for an unspecified attribute) is silently dropped.
|
||||
_ = try doc.setCookie("set_strict=5; SameSite=Strict", frame);
|
||||
_ = try doc.setCookie("set_lax=6; SameSite=Lax", frame);
|
||||
_ = try doc.setCookie("set_default=7", frame);
|
||||
_ = try doc.setCookie("set_none=8; SameSite=None; Secure", frame);
|
||||
try testing.expectEqual("none=4; set_none=8", try doc.getCookie(frame));
|
||||
|
||||
// The same jar seen from a same-site chain: everything applies, and the
|
||||
// dropped writes really were dropped rather than hidden.
|
||||
top.url = "https://victim.example/";
|
||||
try testing.expectEqual("strict=1; lax=2; default=3; none=4; set_none=8", try doc.getCookie(frame));
|
||||
_ = try doc.setCookie("set_strict=5; SameSite=Strict", frame);
|
||||
_ = try doc.setCookie("set_default=7", frame);
|
||||
try testing.expectEqual("strict=1; lax=2; default=3; none=4; set_none=8; set_strict=5; set_default=7", try doc.getCookie(frame));
|
||||
|
||||
// Back in the cross-site context, the newly written cookies obey the
|
||||
// same visibility rules.
|
||||
top.url = "https://attacker.example/";
|
||||
try testing.expectEqual("none=4; set_none=8", try doc.getCookie(frame));
|
||||
}
|
||||
|
||||
test "Document: isRelaxableTo" {
|
||||
// Pure opt-in (relax to self) is always allowed, including IP hosts.
|
||||
try testing.expectEqual(true, isRelaxableTo("a.example.com", "a.example.com"));
|
||||
|
||||
@@ -108,7 +108,7 @@ pub fn init(frame: *Frame, url: [:0]const u8, name: []const u8, worker_type: Wor
|
||||
.loader_id = self._loader_id,
|
||||
.resource_type = .script,
|
||||
.cookie_jar = &session.cookie_jar,
|
||||
.cookie_origin = owned_url,
|
||||
.cookie_origin = proto.site_for_cookies,
|
||||
.notification = session.notification,
|
||||
.header_callback = httpHeaderCallback,
|
||||
.data_callback = httpDataCallback,
|
||||
|
||||
@@ -107,7 +107,7 @@ pub fn init(url: []const u8, options: ?WorkerOptions, frame: *Frame) !*Worker {
|
||||
.loader_id = self._loader_id,
|
||||
.resource_type = .script,
|
||||
.cookie_jar = &session.cookie_jar,
|
||||
.cookie_origin = resolved_url,
|
||||
.cookie_origin = frame.siteForCookies(),
|
||||
.notification = session.notification,
|
||||
.header_callback = httpHeaderCallback,
|
||||
.data_callback = httpDataCallback,
|
||||
|
||||
@@ -46,6 +46,7 @@ const ErrorEvent = @import("event/ErrorEvent.zig");
|
||||
const Fetch = @import("net/Fetch.zig");
|
||||
const idb = @import("storage/idb/idb.zig");
|
||||
const CookieStore = @import("storage/CookieStore.zig");
|
||||
const Cookie = @import("storage/Cookie.zig");
|
||||
const MessagePort = @import("MessagePort.zig");
|
||||
const SharedWorkerGlobalScope = @import("SharedWorkerGlobalScope.zig");
|
||||
const DedicatedWorkerGlobalScope = @import("DedicatedWorkerGlobalScope.zig");
|
||||
@@ -78,6 +79,8 @@ local_arena: Allocator,
|
||||
url: [:0]const u8,
|
||||
// Same-origin constraint: a worker's origin is inherited from its parent frame.
|
||||
origin: ?[]const u8 = null,
|
||||
// Inherited from the creating frame, like origin.
|
||||
site_for_cookies: Cookie.SiteForCookies,
|
||||
buf: [1024]u8 = undefined, // same size as frame.buf
|
||||
// Document charset (matches Page.charset). Workers default to UTF-8.
|
||||
charset: []const u8 = "UTF-8",
|
||||
@@ -148,6 +151,10 @@ pub fn init(
|
||||
.url = url,
|
||||
.arena = arena,
|
||||
.origin = frame.origin,
|
||||
.site_for_cookies = switch (frame.siteForCookies()) {
|
||||
.none => .none,
|
||||
.url => |u| .{ .url = try arena.dupeZ(u8, u) },
|
||||
},
|
||||
.js = undefined,
|
||||
._call_arena = call_arena,
|
||||
._local_arena = local_arena,
|
||||
@@ -423,7 +430,7 @@ fn importScript(self: *WorkerGlobalScope, arena: Allocator, url: [:0]const u8) !
|
||||
.document_frame_id = self._frame._frame_id,
|
||||
.loader_id = self._loader_id,
|
||||
.cookie_jar = &session.cookie_jar,
|
||||
.cookie_origin = self.url,
|
||||
.cookie_origin = self.site_for_cookies,
|
||||
.resource_type = .script,
|
||||
.notification = session.notification,
|
||||
.shutdown_callback = HttpClient.noopShutdown, // syncRequest installs its own
|
||||
|
||||
@@ -183,7 +183,7 @@ fn connect(self: *EventSource) !void {
|
||||
.frame_id = exec.frameId(),
|
||||
.loader_id = exec.loaderId(),
|
||||
.cookie_jar = if (cookie_support) &session.cookie_jar else null,
|
||||
.cookie_origin = exec.url.*,
|
||||
.cookie_origin = exec.siteForCookies(),
|
||||
.resource_type = .eventsource,
|
||||
.streaming = true,
|
||||
.notification = session.notification,
|
||||
|
||||
@@ -104,7 +104,7 @@ pub fn init(input: Input, options: ?InitOpts, exec: *const Execution) !js.Promis
|
||||
.body = request._body,
|
||||
.resource_type = .fetch,
|
||||
.cookie_jar = cookie_jar,
|
||||
.cookie_origin = exec.url.*,
|
||||
.cookie_origin = exec.siteForCookies(),
|
||||
.redirect = switch (request._redirect) {
|
||||
.follow => .follow,
|
||||
.manual => .manual,
|
||||
|
||||
@@ -259,7 +259,7 @@ fn connect(self: *WebSocket, protocols: [][]const u8) !void {
|
||||
try exec.session.cookie_jar.forRequest(resolved_url, &buf.writer, .{
|
||||
.is_http = true,
|
||||
.is_navigation = false,
|
||||
.origin_url = exec.url.*,
|
||||
.origin_url = exec.siteForCookies(),
|
||||
});
|
||||
if (buf.written().len > 0) {
|
||||
try buf.writer.writeByte(0);
|
||||
|
||||
@@ -320,7 +320,7 @@ pub fn send(self: *XMLHttpRequest, body_: ?BodyInit, exec_: *const Execution) !v
|
||||
.loader_id = exec.loaderId(),
|
||||
.body = self._request_body,
|
||||
.cookie_jar = if (cookie_support) &session.cookie_jar else null,
|
||||
.cookie_origin = exec.url.*,
|
||||
.cookie_origin = exec.siteForCookies(),
|
||||
.resource_type = .xhr,
|
||||
.timeout_ms = self._timeout,
|
||||
.notification = session.notification,
|
||||
|
||||
@@ -451,6 +451,15 @@ pub fn appliesTo(self: *const Cookie, url: *const PreparedUri, same_site: bool,
|
||||
return true;
|
||||
}
|
||||
|
||||
// Kept distinct from a plain URL so "the initiator has no site for cookies"
|
||||
// (.none) can't be confused with "there is no initiating document".
|
||||
pub const SiteForCookies = union(enum) {
|
||||
// Matches no site, used for a frame whose ancestor chain contains a cross-site document.
|
||||
none,
|
||||
// Same-site when the target host is same-site with this URL's host.
|
||||
url: [:0]const u8,
|
||||
};
|
||||
|
||||
pub const Jar = struct {
|
||||
allocator: Allocator,
|
||||
cookies: std.ArrayList(Cookie),
|
||||
@@ -575,11 +584,11 @@ pub const Jar = struct {
|
||||
request_time: ?u64 = null,
|
||||
is_navigation: bool = true,
|
||||
prefix: ?[]const u8 = null,
|
||||
origin_url: ?[:0]const u8 = null,
|
||||
origin_url: ?SiteForCookies = null,
|
||||
};
|
||||
pub fn forRequest(self: *Jar, target_url: [:0]const u8, writer: anytype, opts: LookupOpts) !void {
|
||||
const target = PreparedUri.init(target_url);
|
||||
const same_site = try areSameSite(opts.origin_url, target.host);
|
||||
const same_site = areSameSite(opts.origin_url, target.host);
|
||||
|
||||
removeExpired(self, opts.request_time);
|
||||
|
||||
@@ -641,15 +650,19 @@ fn areCookiesEqual(a: *const Cookie, b: *const Cookie) bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
fn areSameSite(origin_url_: ?[:0]const u8, target_host: []const u8) !bool {
|
||||
const origin_url = origin_url_ orelse return true;
|
||||
const origin_host = URL.getHostname(origin_url);
|
||||
pub fn areSameSite(maybe_origin_url: ?SiteForCookies, target_host: []const u8) bool {
|
||||
const origin_url = switch (maybe_origin_url orelse return true) {
|
||||
.none => return false,
|
||||
.url => |url| url,
|
||||
};
|
||||
return areHostsSameSite(URL.getHostname(origin_url), target_host);
|
||||
}
|
||||
|
||||
// common case
|
||||
pub fn areHostsSameSite(target_host: []const u8, origin_host: []const u8) bool {
|
||||
// Common case.
|
||||
if (std.mem.eql(u8, target_host, origin_host)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return std.mem.eql(u8, findSecondLevelDomain(target_host), findSecondLevelDomain(origin_host));
|
||||
}
|
||||
|
||||
@@ -869,112 +882,112 @@ test "Jar: forRequest" {
|
||||
|
||||
// nothing fancy here
|
||||
try expectCookies("global1=1; global2=2", &jar, test_url, .{ .is_http = true });
|
||||
try expectCookies("global1=1; global2=2", &jar, test_url, .{ .origin_url = test_url, .is_navigation = false, .is_http = true });
|
||||
try expectCookies("global1=1; global2=2", &jar, test_url, .{ .origin_url = .{ .url = test_url }, .is_navigation = false, .is_http = true });
|
||||
|
||||
// We have a cookie where Domain=lightpanda.io
|
||||
// This should _not_ match xyxlightpanda.io
|
||||
try expectCookies("", &jar, "http://anothersitelightpanda.io/", .{
|
||||
.origin_url = test_url,
|
||||
.origin_url = .{ .url = test_url },
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// matching path without trailing /
|
||||
try expectCookies("global1=1; global2=2; path1=3", &jar, "http://lightpanda.io/about", .{
|
||||
.origin_url = test_url,
|
||||
.origin_url = .{ .url = test_url },
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// incomplete prefix path
|
||||
try expectCookies("global1=1; global2=2", &jar, "http://lightpanda.io/abou", .{
|
||||
.origin_url = test_url,
|
||||
.origin_url = .{ .url = test_url },
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// path doesn't match
|
||||
try expectCookies("global1=1; global2=2", &jar, "http://lightpanda.io/aboutus", .{
|
||||
.origin_url = test_url,
|
||||
.origin_url = .{ .url = test_url },
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// path doesn't match cookie directory
|
||||
try expectCookies("global1=1; global2=2", &jar, "http://lightpanda.io/docs", .{
|
||||
.origin_url = test_url,
|
||||
.origin_url = .{ .url = test_url },
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// exact directory match
|
||||
try expectCookies("global1=1; global2=2; path2=4", &jar, "http://lightpanda.io/docs/", .{
|
||||
.origin_url = test_url,
|
||||
.origin_url = .{ .url = test_url },
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// sub directory match
|
||||
try expectCookies("global1=1; global2=2; path2=4", &jar, "http://lightpanda.io/docs/more", .{
|
||||
.origin_url = test_url,
|
||||
.origin_url = .{ .url = test_url },
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// secure
|
||||
try expectCookies("global1=1; global2=2; secure=5", &jar, "https://lightpanda.io/", .{
|
||||
.origin_url = test_url,
|
||||
.origin_url = .{ .url = test_url },
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// navigational cross domain, secure
|
||||
try expectCookies("global1=1; global2=2; secure=5; sitenone=6; sitelax=7", &jar, "https://lightpanda.io/x/", .{
|
||||
.origin_url = "https://example.com/",
|
||||
.origin_url = .{ .url = "https://example.com/" },
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// navigational cross domain, insecure
|
||||
try expectCookies("global1=1; global2=2; sitelax=7", &jar, "http://lightpanda.io/x/", .{
|
||||
.origin_url = "https://example.com/",
|
||||
.origin_url = .{ .url = "https://example.com/" },
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// non-navigational cross domain, insecure
|
||||
try expectCookies("", &jar, "http://lightpanda.io/x/", .{
|
||||
.origin_url = "https://example.com/",
|
||||
.origin_url = .{ .url = "https://example.com/" },
|
||||
.is_http = true,
|
||||
.is_navigation = false,
|
||||
});
|
||||
|
||||
// non-navigational cross domain, secure
|
||||
try expectCookies("sitenone=6", &jar, "https://lightpanda.io/x/", .{
|
||||
.origin_url = "https://example.com/",
|
||||
.origin_url = .{ .url = "https://example.com/" },
|
||||
.is_http = true,
|
||||
.is_navigation = false,
|
||||
});
|
||||
|
||||
// non-navigational same origin
|
||||
try expectCookies("global1=1; global2=2; sitelax=7; sitestrict=8", &jar, "http://lightpanda.io/x/", .{
|
||||
.origin_url = "https://lightpanda.io/",
|
||||
.origin_url = .{ .url = "https://lightpanda.io/" },
|
||||
.is_http = true,
|
||||
.is_navigation = false,
|
||||
});
|
||||
|
||||
// exact domain match + suffix
|
||||
try expectCookies("global2=2; domain1=9", &jar, "http://test.lightpanda.io/", .{
|
||||
.origin_url = test_url,
|
||||
.origin_url = .{ .url = test_url },
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// domain suffix match + suffix
|
||||
try expectCookies("global2=2; domain1=9", &jar, "http://1.test.lightpanda.io/", .{
|
||||
.origin_url = test_url,
|
||||
.origin_url = .{ .url = test_url },
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// non-matching domain
|
||||
try expectCookies("global2=2", &jar, "http://other.lightpanda.io/", .{
|
||||
.origin_url = test_url,
|
||||
.origin_url = .{ .url = test_url },
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
const l = jar.cookies.items.len;
|
||||
try expectCookies("global1=1", &jar, test_url, .{
|
||||
.request_time = now + 100,
|
||||
.origin_url = test_url,
|
||||
.origin_url = .{ .url = test_url },
|
||||
.is_http = true,
|
||||
});
|
||||
try testing.expectEqual(l - 1, jar.cookies.items.len);
|
||||
@@ -1001,13 +1014,13 @@ test "Jar: forRequest SameSite=Strict on cross-site navigation" {
|
||||
|
||||
// Same-site navigation: cookie included.
|
||||
try expectCookies("sid=STRICT_COOKIE", &jar, "http://victim.example/transfer", .{
|
||||
.origin_url = victim_url,
|
||||
.origin_url = .{ .url = victim_url },
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// Cross-site navigation from attacker.test: cookie excluded.
|
||||
try expectCookies("", &jar, "http://victim.example/transfer", .{
|
||||
.origin_url = "http://attacker.test/strict-form",
|
||||
.origin_url = .{ .url = "http://attacker.test/strict-form" },
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
@@ -1017,6 +1030,48 @@ test "Jar: forRequest SameSite=Strict on cross-site navigation" {
|
||||
});
|
||||
}
|
||||
|
||||
test "Jar: forRequest with a null site-for-cookies" {
|
||||
const expectCookies = struct {
|
||||
fn expect(expected: []const u8, jar: *Jar, target_url: [:0]const u8, opts: Jar.LookupOpts) !void {
|
||||
var aw: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||
defer aw.deinit();
|
||||
try jar.forRequest(target_url, &aw.writer, opts);
|
||||
try testing.expectEqual(expected, aw.written());
|
||||
}
|
||||
}.expect;
|
||||
|
||||
var jar = Jar.init(testing.allocator, null);
|
||||
defer jar.deinit();
|
||||
|
||||
const now = lp.datetime.timestamp(.real);
|
||||
const victim_url: [:0]const u8 = "https://victim.example/";
|
||||
try jar.add(try Cookie.parse(testing.allocator, victim_url, "strict=1; Path=/; SameSite=Strict"), now, true);
|
||||
try jar.add(try Cookie.parse(testing.allocator, victim_url, "lax=2; Path=/; SameSite=Lax"), now, true);
|
||||
try jar.add(try Cookie.parse(testing.allocator, victim_url, "none=3; Path=/; SameSite=None; Secure"), now, true);
|
||||
|
||||
// .none is the site-for-cookies of a frame whose ancestor chain contains
|
||||
// a cross-site document. Even though the target here is the cookies' own
|
||||
// site, the request is cross-site: Strict is withheld.
|
||||
try expectCookies("lax=2; none=3", &jar, victim_url, .{
|
||||
.origin_url = .none,
|
||||
.is_http = true,
|
||||
});
|
||||
|
||||
// Sub-resources from such a frame only carry SameSite=None cookies.
|
||||
try expectCookies("none=3", &jar, victim_url, .{
|
||||
.origin_url = .none,
|
||||
.is_http = true,
|
||||
.is_navigation = false,
|
||||
});
|
||||
|
||||
// Sanity: a same-site initiator still gets everything.
|
||||
try expectCookies("strict=1; lax=2; none=3", &jar, victim_url, .{
|
||||
.origin_url = .{ .url = victim_url },
|
||||
.is_http = true,
|
||||
.is_navigation = false,
|
||||
});
|
||||
}
|
||||
|
||||
test "Cookie: parse key=value" {
|
||||
try expectError(error.Empty, null, "");
|
||||
try expectError(error.InvalidByteSequence, null, &.{ 'a', 30, '=', 'b' });
|
||||
|
||||
@@ -23,6 +23,7 @@ const js = @import("../../js/js.zig");
|
||||
const URL = @import("../../URL.zig");
|
||||
const Notification = @import("../../../Notification.zig");
|
||||
|
||||
const Frame = @import("../../Frame.zig");
|
||||
const Cookie = @import("Cookie.zig");
|
||||
const EventTarget = @import("../EventTarget.zig");
|
||||
const CookieChangeEvent = @import("../event/CookieChangeEvent.zig");
|
||||
@@ -66,7 +67,7 @@ fn onCookieChanged(ctx: *anyopaque, data: *const Notification.CookieChanged) !vo
|
||||
|
||||
// CookieStore exposes only cookies that script would see for the
|
||||
// current document — same filter as `match` (HttpOnly hidden,
|
||||
// same-site treated as first-party against the document URL).
|
||||
// SameSite judged against the global's site-for-cookies).
|
||||
const doc_url = exec.url.*;
|
||||
const target = Cookie.PreparedUri.init(doc_url);
|
||||
if (target.host.len == 0) {
|
||||
@@ -84,7 +85,8 @@ fn onCookieChanged(ctx: *anyopaque, data: *const Notification.CookieChanged) !vo
|
||||
.http_only = data.http_only,
|
||||
.same_site = data.same_site,
|
||||
};
|
||||
if (!probe.appliesTo(&target, true, true, false)) {
|
||||
const same_site = Cookie.areSameSite(exec.siteForCookies(), target.host);
|
||||
if (!probe.appliesTo(&target, same_site, false, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -377,11 +379,12 @@ fn matchCookies(
|
||||
// we're matching
|
||||
const normalized_name: ?[]const u8 = if (name) |n| std.mem.trim(u8, n, " \t") else null;
|
||||
|
||||
const same_site = Cookie.areSameSite(exec.siteForCookies(), target.host);
|
||||
var items: std.ArrayList(CookieListItem) = .empty;
|
||||
for (session.cookie_jar.cookies.items) |*cookie| {
|
||||
// CookieStore exposes only cookies that script would see for the
|
||||
// current document. HttpOnly cookies stay hidden.
|
||||
if (cookie.appliesTo(&target, true, true, false) == false) {
|
||||
if (cookie.appliesTo(&target, same_site, false, false) == false) {
|
||||
continue;
|
||||
}
|
||||
if (normalized_name) |n| {
|
||||
@@ -488,6 +491,10 @@ fn storeCookie(exec: *const Execution, init_: CookieInit, is_delete: bool) !void
|
||||
}
|
||||
}
|
||||
|
||||
if (init.sameSite != .none and Cookie.areSameSite(exec.siteForCookies(), URL.getHostname(url)) == false) {
|
||||
return error.SameSiteBlocked;
|
||||
}
|
||||
|
||||
const is_https = URL.isSecure(url);
|
||||
// Per spec, SameSite=None requires Secure. CookieStore additionally
|
||||
// marks any cookie written from an HTTPS document as Secure.
|
||||
@@ -612,3 +619,78 @@ const testing = @import("../../../testing.zig");
|
||||
test "WebApi: CookieStore" {
|
||||
try testing.htmlRunner("cookie_store.html", .{});
|
||||
}
|
||||
|
||||
test "CookieStore: cross-site frame" {
|
||||
defer testing.test_session.closeAllPages();
|
||||
const frame = try testing.createFrame();
|
||||
const exec = &frame.js.execution;
|
||||
const jar = &frame._session.cookie_jar;
|
||||
defer jar.clearRetainingCapacity();
|
||||
|
||||
// victim.example embedded by attacker.example: the ancestor chain is
|
||||
// cross-site, so the frame has no site for cookies.
|
||||
var top: Frame = undefined;
|
||||
top.parent = null;
|
||||
top.url = "https://attacker.example/";
|
||||
frame.url = "https://victim.example/inner";
|
||||
frame.parent = ⊤
|
||||
defer frame.parent = null;
|
||||
|
||||
try jar.populateFromResponse("https://victim.example/", "strict=1; SameSite=Strict");
|
||||
try jar.populateFromResponse("https://victim.example/", "lax=2; SameSite=Lax");
|
||||
try jar.populateFromResponse("https://victim.example/", "none=3; SameSite=None; Secure");
|
||||
|
||||
// getAll(): only SameSite=None is visible from a cross-site context.
|
||||
{
|
||||
const items = try matchCookies(exec, null, null, false);
|
||||
try testing.expectEqual(1, items.len);
|
||||
try testing.expectEqual("none", items[0].name.str());
|
||||
}
|
||||
|
||||
// set(): the default (Strict) and Lax are rejected, None is stored.
|
||||
// delete() is an expiring Strict set, so it is rejected too.
|
||||
try std.testing.expectError(error.SameSiteBlocked, storeCookie(exec, .{ .name = "set_strict", .value = "4" }, false));
|
||||
try std.testing.expectError(error.SameSiteBlocked, storeCookie(exec, .{ .name = "set_lax", .value = "5", .sameSite = .lax }, false));
|
||||
try storeCookie(exec, .{ .name = "set_none", .value = "6", .sameSite = .none }, false);
|
||||
try std.testing.expectError(error.SameSiteBlocked, storeCookie(exec, .{ .name = "none", .value = "", .expires = 0 }, true));
|
||||
{
|
||||
const items = try matchCookies(exec, null, null, false);
|
||||
try testing.expectEqual(2, items.len);
|
||||
try testing.expectEqual("none", items[0].name.str());
|
||||
try testing.expectEqual("set_none", items[1].name.str());
|
||||
}
|
||||
|
||||
// The same jar seen from a same-site chain: everything applies, and the
|
||||
// rejected writes really were rejected rather than hidden.
|
||||
top.url = "https://victim.example/";
|
||||
{
|
||||
const items = try matchCookies(exec, null, null, false);
|
||||
try testing.expectEqual(4, items.len);
|
||||
try testing.expectEqual("strict", items[0].name.str());
|
||||
try testing.expectEqual("lax", items[1].name.str());
|
||||
try testing.expectEqual("none", items[2].name.str());
|
||||
try testing.expectEqual("set_none", items[3].name.str());
|
||||
}
|
||||
try storeCookie(exec, .{ .name = "set_strict", .value = "4" }, false);
|
||||
try storeCookie(exec, .{ .name = "none", .value = "", .expires = 0 }, true);
|
||||
{
|
||||
// The jar swap-removes on delete, so only check membership here.
|
||||
const items = try matchCookies(exec, null, null, false);
|
||||
try testing.expectEqual(4, items.len);
|
||||
var has_set_strict = false;
|
||||
for (items) |item| {
|
||||
try testing.expectEqual(false, std.mem.eql(u8, "none", item.name.str()));
|
||||
if (std.mem.eql(u8, "set_strict", item.name.str())) has_set_strict = true;
|
||||
}
|
||||
try testing.expectEqual(true, has_set_strict);
|
||||
}
|
||||
|
||||
// Back in the cross-site context, the Strict cookie written same-site is
|
||||
// hidden again.
|
||||
top.url = "https://attacker.example/";
|
||||
{
|
||||
const items = try matchCookies(exec, null, null, false);
|
||||
try testing.expectEqual(1, items.len);
|
||||
try testing.expectEqual("set_none", items[0].name.str());
|
||||
}
|
||||
}
|
||||
+21
-15
@@ -28,7 +28,8 @@ const Driver = @import("../server/Driver.zig");
|
||||
const URL = @import("../browser/URL.zig");
|
||||
const referrer = @import("../browser/referrer.zig");
|
||||
const WebSocket = @import("../browser/webapi/net/WebSocket.zig");
|
||||
const CookieJar = @import("../browser/webapi/storage/Cookie.zig").Jar;
|
||||
const Cookie = @import("../browser/webapi/storage/Cookie.zig");
|
||||
const CookieJar = Cookie.Jar;
|
||||
|
||||
const http = @import("http.zig");
|
||||
const Network = @import("Network.zig");
|
||||
@@ -605,7 +606,12 @@ pub fn newRequest(self: *Client, req: Request, owner: ?*Owner) anyerror!*Transfe
|
||||
// These are all small, so duping them into the transfer's arena is
|
||||
// cheap and can solve some nasty UAF.
|
||||
owned.url = try arena.dupeZ(u8, req.url);
|
||||
owned.cookie_origin = try arena.dupeZ(u8, req.cookie_origin);
|
||||
if (req.cookie_origin) |cookie_origin| {
|
||||
owned.cookie_origin = switch (cookie_origin) {
|
||||
.none => .none,
|
||||
.url => |url| .{ .url = try arena.dupeZ(u8, url) },
|
||||
};
|
||||
}
|
||||
if (req.credentials) |c| {
|
||||
owned.credentials = try arena.dupeZ(u8, c);
|
||||
}
|
||||
@@ -1774,7 +1780,7 @@ pub const Request = struct {
|
||||
url: [:0]const u8,
|
||||
body: ?[]const u8 = null,
|
||||
cookie_jar: ?*CookieJar,
|
||||
cookie_origin: [:0]const u8,
|
||||
cookie_origin: ?Cookie.SiteForCookies,
|
||||
resource_type: ResourceType,
|
||||
redirect: RedirectMode = .follow,
|
||||
referrer_policy: ?referrer.Policy = null,
|
||||
@@ -3800,7 +3806,7 @@ fn testTransfer(arena: *lp.Arena) Transfer {
|
||||
.method = .GET,
|
||||
.url = "http://example.com/",
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = "",
|
||||
.cookie_origin = .none,
|
||||
.resource_type = .document,
|
||||
.notification = undefined,
|
||||
.shutdown_callback = noopShutdown,
|
||||
@@ -3978,7 +3984,7 @@ test "HttpClient: fulfillIntercepted survives a done_callback that tears down th
|
||||
.method = .GET,
|
||||
.url = "http://example.com/",
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = "",
|
||||
.cookie_origin = .none,
|
||||
.resource_type = .document,
|
||||
.notification = undefined,
|
||||
.shutdown_callback = noopShutdown,
|
||||
@@ -4063,7 +4069,7 @@ test "HttpClient: kill during done_callback does not also fire shutdown_callback
|
||||
.method = .GET,
|
||||
.url = "http://example.com/",
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = "",
|
||||
.cookie_origin = .none,
|
||||
.resource_type = .xhr,
|
||||
.notification = undefined,
|
||||
.shutdown_callback = Ctx.shutdownCallback,
|
||||
@@ -4149,7 +4155,7 @@ test "HttpClient: kill during a non-terminal callback defers shutdown_callback"
|
||||
.method = .GET,
|
||||
.url = "http://example.com/",
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = "",
|
||||
.cookie_origin = .none,
|
||||
.resource_type = .xhr,
|
||||
.notification = undefined,
|
||||
.shutdown_callback = Ctx.shutdownCallback,
|
||||
@@ -4210,7 +4216,7 @@ test "HttpClient: aborting a robots-parked transfer unlinks it from the gate" {
|
||||
.method = .GET,
|
||||
.url = "http://example.com/",
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = "",
|
||||
.cookie_origin = .none,
|
||||
.resource_type = .document,
|
||||
.notification = undefined,
|
||||
.shutdown_callback = noopShutdown,
|
||||
@@ -4278,7 +4284,7 @@ test "HttpClient: fulfillIntercepted follows a 3xx redirect" {
|
||||
.url = "http://example.com/start",
|
||||
.body = "payload",
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = "",
|
||||
.cookie_origin = .none,
|
||||
.resource_type = .document,
|
||||
.notification = undefined,
|
||||
.shutdown_callback = noopShutdown,
|
||||
@@ -4322,7 +4328,7 @@ test "HttpClient: fulfillIntercepted follows a 3xx redirect" {
|
||||
.url = "http://example.com/start",
|
||||
.body = "payload",
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = "",
|
||||
.cookie_origin = .none,
|
||||
.resource_type = .document,
|
||||
.notification = undefined,
|
||||
.shutdown_callback = noopShutdown,
|
||||
@@ -4390,7 +4396,7 @@ test "HttpClient: fulfillIntercepted delivers a 3xx without a Location as the re
|
||||
.method = .GET,
|
||||
.url = "http://example.com/",
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = "",
|
||||
.cookie_origin = .none,
|
||||
.resource_type = .document,
|
||||
.notification = undefined,
|
||||
.shutdown_callback = noopShutdown,
|
||||
@@ -4458,7 +4464,7 @@ test "HttpClient: abortParked survives an error_callback that tears down the own
|
||||
.method = .GET,
|
||||
.url = "http://example.com/",
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = "",
|
||||
.cookie_origin = .none,
|
||||
.resource_type = .document,
|
||||
.notification = undefined,
|
||||
.shutdown_callback = noopShutdown,
|
||||
@@ -4536,7 +4542,7 @@ test "HttpClient: abort survives an error_callback that tears down the owner" {
|
||||
.method = .GET,
|
||||
.url = "http://example.com/",
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = "",
|
||||
.cookie_origin = .none,
|
||||
.resource_type = .xhr,
|
||||
.notification = undefined,
|
||||
.shutdown_callback = noopShutdown,
|
||||
@@ -4573,7 +4579,7 @@ test "HttpClient: abort survives an error_callback that tears down the owner" {
|
||||
.method = .GET,
|
||||
.url = "http://example.com/",
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = "",
|
||||
.cookie_origin = .none,
|
||||
.resource_type = .xhr,
|
||||
.notification = undefined,
|
||||
.shutdown_callback = noopShutdown,
|
||||
@@ -4637,7 +4643,7 @@ test "HttpClient: throttled navigations wait for their per-host slot" {
|
||||
.method = .GET,
|
||||
.url = url,
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = "",
|
||||
.cookie_origin = .none,
|
||||
.resource_type = .document,
|
||||
.notification = undefined,
|
||||
.shutdown_callback = noopShutdown,
|
||||
|
||||
@@ -117,7 +117,7 @@ fn fetchThenResume(self: *RobotsGate, robots_url: [:0]const u8, transfer: *Trans
|
||||
.loader_id = transfer.req.loader_id,
|
||||
.notification = transfer.req.notification,
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = owned_url,
|
||||
.cookie_origin = .{ .url = owned_url },
|
||||
.ctx = robots_ctx,
|
||||
.header_callback = RobotsContext.headerCallback,
|
||||
.data_callback = RobotsContext.dataCallback,
|
||||
|
||||
@@ -109,7 +109,7 @@ fn makeTestTransfer(arena: *lp.Arena, client: *HttpClient, id: u32) !*Transfer {
|
||||
.method = .GET,
|
||||
.url = "http://example.com/",
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = "",
|
||||
.cookie_origin = .none,
|
||||
.resource_type = .document,
|
||||
.notification = undefined,
|
||||
.shutdown_callback = HttpClient.noopShutdown,
|
||||
|
||||
@@ -247,7 +247,7 @@ test "signRequest: adds headers with correct names" {
|
||||
.method = .GET,
|
||||
.url = "https://example.com/",
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = "",
|
||||
.cookie_origin = .none,
|
||||
.resource_type = .document,
|
||||
.notification = undefined,
|
||||
.shutdown_callback = @import("HttpClient.zig").noopShutdown,
|
||||
|
||||
@@ -1541,7 +1541,7 @@ test "cdp: syncRequest short-circuits after disconnect" {
|
||||
.method = .GET,
|
||||
.url = "http://127.0.0.1:9582/",
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = "",
|
||||
.cookie_origin = .none,
|
||||
.resource_type = .fetch,
|
||||
.notification = undefined,
|
||||
.shutdown_callback = HttpClient.noopShutdown,
|
||||
|
||||
@@ -1174,7 +1174,7 @@ test "cdp.Network: setBlockedURLs blocks requests with inspector reason" {
|
||||
.method = .GET,
|
||||
.url = "https://blocked.test/script.js",
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = "https://blocked.test/",
|
||||
.cookie_origin = .{ .url = "https://blocked.test/" },
|
||||
.resource_type = .script,
|
||||
.notification = bc.session.notification,
|
||||
.ctx = &error_context,
|
||||
@@ -1200,7 +1200,7 @@ test "cdp.Network: setBlockedURLs blocks requests with inspector reason" {
|
||||
.method = .GET,
|
||||
.url = "http://127.0.0.1:9582/redirect-no-fragment",
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = "http://127.0.0.1:9582/",
|
||||
.cookie_origin = .{ .url = "http://127.0.0.1:9582/" },
|
||||
.resource_type = .script,
|
||||
.notification = bc.session.notification,
|
||||
.ctx = &error_context,
|
||||
@@ -1241,7 +1241,7 @@ test "cdp.Network: POST body exposed as postData" {
|
||||
.url = "http://127.0.0.1:9582/echo_body",
|
||||
.body = body,
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = "http://127.0.0.1:9582/",
|
||||
.cookie_origin = .{ .url = "http://127.0.0.1:9582/" },
|
||||
.resource_type = .fetch,
|
||||
.notification = bc.session.notification,
|
||||
.shutdown_callback = HttpClient.noopShutdown,
|
||||
@@ -1303,7 +1303,7 @@ const EchoDriver = struct {
|
||||
.url = "http://127.0.0.1:9582/echo_body",
|
||||
.body = body,
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = "http://127.0.0.1:9582/",
|
||||
.cookie_origin = .{ .url = "http://127.0.0.1:9582/" },
|
||||
.resource_type = .fetch,
|
||||
.notification = bc.session.notification,
|
||||
.ctx = &driver,
|
||||
@@ -1506,7 +1506,7 @@ test "cdp.Network: redirect hop precedes Fetch pause and carries redirectRespons
|
||||
.method = .GET,
|
||||
.url = start_url,
|
||||
.cookie_jar = null,
|
||||
.cookie_origin = start_url,
|
||||
.cookie_origin = .{ .url = start_url },
|
||||
.resource_type = .script,
|
||||
.notification = bc.session.notification,
|
||||
.ctx = &callback_context,
|
||||
|
||||
Reference in new issue
Block a user