From e4029985776e62a62aafd7b429298370cbf86409 Mon Sep 17 00:00:00 2001 From: sjorsdonkers <72333389+sjorsdonkers@users.noreply.github.com> Date: Mon, 16 Jun 2025 11:54:53 +0200 Subject: [PATCH] JS may not set/get HttpOnly cookies --- src/browser/html/document.zig | 8 +++++++- src/browser/page.zig | 3 ++- src/browser/storage/cookie.zig | 33 ++++++++++++++++++++++++++++----- src/browser/xhr/xhr.zig | 1 + src/cdp/domains/storage.zig | 7 +++---- 5 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/browser/html/document.zig b/src/browser/html/document.zig index 68483a56..ed406ea5 100644 --- a/src/browser/html/document.zig +++ b/src/browser/html/document.zig @@ -81,7 +81,7 @@ pub const HTMLDocument = struct { pub fn get_cookie(_: *parser.DocumentHTML, page: *Page) ![]const u8 { var buf: std.ArrayListUnmanaged(u8) = .{}; - try page.cookie_jar.forRequest(&page.url.uri, buf.writer(page.arena), .{ .navigation = true }); + try page.cookie_jar.forRequest(&page.url.uri, buf.writer(page.arena), .{ .navigation = true, .is_http = false }); return buf.items; } @@ -90,6 +90,10 @@ pub const HTMLDocument = struct { // outlives the page's arena. const c = try Cookie.parse(page.cookie_jar.allocator, &page.url.uri, cookie_str); errdefer c.deinit(); + if (c.http_only) { + c.deinit(); + return ""; // HttpOnly cookies cannot be set from JS + } try page.cookie_jar.add(c, std.time.timestamp()); return cookie_str; } @@ -333,6 +337,8 @@ test "Browser.HTML.Document" { .{ "document.cookie = 'name=Oeschger; SameSite=None; Secure'", "name=Oeschger; SameSite=None; Secure" }, .{ "document.cookie = 'favorite_food=tripe; SameSite=None; Secure'", "favorite_food=tripe; SameSite=None; Secure" }, .{ "document.cookie", "name=Oeschger; favorite_food=tripe" }, + .{ "document.cookie = 'IgnoreMy=Ghost; HttpOnly'", null }, // "" should be returned, but the framework overrules it atm + .{ "document.cookie", "name=Oeschger; favorite_food=tripe" }, }, .{}); try runner.testCases(&.{ diff --git a/src/browser/page.zig b/src/browser/page.zig index 368a3c5a..8786f229 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -217,7 +217,7 @@ pub const Page = struct { { // block exists to limit the lifetime of the request, which holds // onto a connection - var request = try self.newHTTPRequest(opts.method, &self.url, .{ .navigation = true }); + var request = try self.newHTTPRequest(opts.method, &self.url, .{ .navigation = true, .is_http = true }); defer request.deinit(); request.body = opts.body; @@ -513,6 +513,7 @@ pub const Page = struct { var request = try self.newHTTPRequest(.GET, &url, .{ .origin_uri = &origin_url.uri, .navigation = false, + .is_http = true, }); defer request.deinit(); diff --git a/src/browser/storage/cookie.zig b/src/browser/storage/cookie.zig index 1abd37e1..eb978826 100644 --- a/src/browser/storage/cookie.zig +++ b/src/browser/storage/cookie.zig @@ -12,6 +12,7 @@ pub const LookupOpts = struct { request_time: ?i64 = null, origin_uri: ?*const Uri = null, navigation: bool = true, + is_http: bool, }; pub const Jar = struct { @@ -91,7 +92,7 @@ pub const Jar = struct { var first = true; for (self.cookies.items) |*cookie| { - if (!cookie.appliesTo(&target, same_site, opts.navigation)) continue; + if (!cookie.appliesTo(&target, same_site, opts.navigation, opts.is_http)) continue; // we have a match! if (first) { @@ -411,7 +412,12 @@ pub const Cookie = struct { return .{ name, value, rest }; } - pub fn appliesTo(self: *const Cookie, url: *const PreparedUri, same_site: bool, navigation: bool) bool { + pub fn appliesTo(self: *const Cookie, url: *const PreparedUri, same_site: bool, navigation: bool, is_http: bool) bool { + if (self.http_only and is_http == false) { + // http only cookies can be accessed from Javascript + return false; + } + if (url.secure == false and self.secure) { // secure cookie can only be sent over HTTPs return false; @@ -581,7 +587,7 @@ test "Jar: forRequest" { { // test with no cookies - try expectCookies("", &jar, test_uri, .{}); + try expectCookies("", &jar, test_uri, .{ .is_http = true }); } try jar.add(try Cookie.parse(testing.allocator, &test_uri, "global1=1"), now); @@ -595,97 +601,114 @@ test "Jar: forRequest" { try jar.add(try Cookie.parse(testing.allocator, &test_uri_2, "domain1=9;domain=test.lightpanda.io"), now); // nothing fancy here - try expectCookies("global1=1; global2=2", &jar, test_uri, .{}); - try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .origin_uri = &test_uri, .navigation = false }); + try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .is_http = true }); + try expectCookies("global1=1; global2=2", &jar, test_uri, .{ .origin_uri = &test_uri, .navigation = false, .is_http = true }); // We have a cookie where Domain=lightpanda.io // This should _not_ match xyxlightpanda.io try expectCookies("", &jar, try std.Uri.parse("http://anothersitelightpanda.io/"), .{ .origin_uri = &test_uri, + .is_http = true, }); // matching path without trailing / try expectCookies("global1=1; global2=2; path1=3", &jar, try std.Uri.parse("http://lightpanda.io/about"), .{ .origin_uri = &test_uri, + .is_http = true, }); // incomplete prefix path try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/abou"), .{ .origin_uri = &test_uri, + .is_http = true, }); // path doesn't match try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/aboutus"), .{ .origin_uri = &test_uri, + .is_http = true, }); // path doesn't match cookie directory try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/docs"), .{ .origin_uri = &test_uri, + .is_http = true, }); // exact directory match try expectCookies("global1=1; global2=2; path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/"), .{ .origin_uri = &test_uri, + .is_http = true, }); // sub directory match try expectCookies("global1=1; global2=2; path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/more"), .{ .origin_uri = &test_uri, + .is_http = true, }); // secure try expectCookies("global1=1; global2=2; secure=5", &jar, try std.Uri.parse("https://lightpanda.io/"), .{ .origin_uri = &test_uri, + .is_http = true, }); // navigational cross domain, secure try expectCookies("global1=1; global2=2; secure=5; sitenone=6; sitelax=7", &jar, try std.Uri.parse("https://lightpanda.io/x/"), .{ .origin_uri = &(try std.Uri.parse("https://example.com/")), + .is_http = true, }); // navigational cross domain, insecure try expectCookies("global1=1; global2=2; sitelax=7", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{ .origin_uri = &(try std.Uri.parse("https://example.com/")), + .is_http = true, }); // non-navigational cross domain, insecure try expectCookies("", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{ .origin_uri = &(try std.Uri.parse("https://example.com/")), .navigation = false, + .is_http = true, }); // non-navigational cross domain, secure try expectCookies("sitenone=6", &jar, try std.Uri.parse("https://lightpanda.io/x/"), .{ .origin_uri = &(try std.Uri.parse("https://example.com/")), .navigation = false, + .is_http = true, }); // non-navigational same origin try expectCookies("global1=1; global2=2; sitelax=7; sitestrict=8", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{ .origin_uri = &(try std.Uri.parse("https://lightpanda.io/")), .navigation = false, + .is_http = true, }); // exact domain match + suffix try expectCookies("global2=2; domain1=9", &jar, try std.Uri.parse("http://test.lightpanda.io/"), .{ .origin_uri = &test_uri, + .is_http = true, }); // domain suffix match + suffix try expectCookies("global2=2; domain1=9", &jar, try std.Uri.parse("http://1.test.lightpanda.io/"), .{ .origin_uri = &test_uri, + .is_http = true, }); // non-matching domain try expectCookies("global2=2", &jar, try std.Uri.parse("http://other.lightpanda.io/"), .{ .origin_uri = &test_uri, + .is_http = true, }); const l = jar.cookies.items.len; try expectCookies("global1=1", &jar, test_uri, .{ .request_time = now + 100, .origin_uri = &test_uri, + .is_http = true, }); try testing.expectEqual(l - 1, jar.cookies.items.len); diff --git a/src/browser/xhr/xhr.zig b/src/browser/xhr/xhr.zig index 37f87bee..e7b11630 100644 --- a/src/browser/xhr/xhr.zig +++ b/src/browser/xhr/xhr.zig @@ -475,6 +475,7 @@ pub const XMLHttpRequest = struct { try self.cookie_jar.forRequest(&self.url.?.uri, arr.writer(self.arena), .{ .navigation = false, .origin_uri = &self.origin_url.uri, + .is_http = true, }); if (arr.items.len > 0) { diff --git a/src/cdp/domains/storage.zig b/src/cdp/domains/storage.zig index cc26437b..c2fb0d13 100644 --- a/src/cdp/domains/storage.zig +++ b/src/cdp/domains/storage.zig @@ -133,8 +133,6 @@ pub fn setCdpCookie(cookie_jar: *CookieJar, param: CdpCookie) !void { if (param.priority != .Medium or param.sameParty != null or param.sourceScheme != null or param.partitionKey != null) { return error.NotYetImplementedParams; } - if (param.name.len == 0) return error.InvalidParams; - if (param.value.len == 0) return error.InvalidParams; var arena = std.heap.ArenaAllocator.init(cookie_jar.allocator); errdefer arena.deinit(); @@ -183,7 +181,7 @@ pub const CookieWriter = struct { if (self.urls) |urls| { for (self.cookies) |*cookie| { for (urls) |*url| { - if (cookie.appliesTo(url, true, true)) { // TBD same_site, should we compare to the pages url? + if (cookie.appliesTo(url, true, true, true)) { // TBD same_site, should we compare to the pages url? try writeCookie(cookie, w); break; } @@ -223,7 +221,8 @@ pub fn writeCookie(cookie: *const Cookie, w: anytype) !void { try w.objectField("secure"); try w.write(cookie.secure); - // TODO session + try w.objectField("session"); + try w.write(cookie.expires == null); try w.objectField("sameSite"); switch (cookie.same_site) {