JS may not set/get HttpOnly cookies

This commit is contained in:
sjorsdonkers
2025-06-16 11:54:53 +02:00
committed by Sjors
parent 073f75efa3
commit e402998577
5 changed files with 41 additions and 11 deletions

View File

@@ -81,7 +81,7 @@ pub const HTMLDocument = struct {
pub fn get_cookie(_: *parser.DocumentHTML, page: *Page) ![]const u8 { pub fn get_cookie(_: *parser.DocumentHTML, page: *Page) ![]const u8 {
var buf: std.ArrayListUnmanaged(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; return buf.items;
} }
@@ -90,6 +90,10 @@ pub const HTMLDocument = struct {
// outlives the page's arena. // outlives the page's arena.
const c = try Cookie.parse(page.cookie_jar.allocator, &page.url.uri, cookie_str); const c = try Cookie.parse(page.cookie_jar.allocator, &page.url.uri, cookie_str);
errdefer c.deinit(); 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()); try page.cookie_jar.add(c, std.time.timestamp());
return cookie_str; 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 = '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 = 'favorite_food=tripe; SameSite=None; Secure'", "favorite_food=tripe; SameSite=None; Secure" },
.{ "document.cookie", "name=Oeschger; favorite_food=tripe" }, .{ "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(&.{ try runner.testCases(&.{

View File

@@ -217,7 +217,7 @@ pub const Page = struct {
{ {
// block exists to limit the lifetime of the request, which holds // block exists to limit the lifetime of the request, which holds
// onto a connection // 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(); defer request.deinit();
request.body = opts.body; request.body = opts.body;
@@ -513,6 +513,7 @@ pub const Page = struct {
var request = try self.newHTTPRequest(.GET, &url, .{ var request = try self.newHTTPRequest(.GET, &url, .{
.origin_uri = &origin_url.uri, .origin_uri = &origin_url.uri,
.navigation = false, .navigation = false,
.is_http = true,
}); });
defer request.deinit(); defer request.deinit();

View File

@@ -12,6 +12,7 @@ pub const LookupOpts = struct {
request_time: ?i64 = null, request_time: ?i64 = null,
origin_uri: ?*const Uri = null, origin_uri: ?*const Uri = null,
navigation: bool = true, navigation: bool = true,
is_http: bool,
}; };
pub const Jar = struct { pub const Jar = struct {
@@ -91,7 +92,7 @@ pub const Jar = struct {
var first = true; var first = true;
for (self.cookies.items) |*cookie| { 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! // we have a match!
if (first) { if (first) {
@@ -411,7 +412,12 @@ pub const Cookie = struct {
return .{ name, value, rest }; 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) { if (url.secure == false and self.secure) {
// secure cookie can only be sent over HTTPs // secure cookie can only be sent over HTTPs
return false; return false;
@@ -581,7 +587,7 @@ test "Jar: forRequest" {
{ {
// test with no cookies // 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); 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); try jar.add(try Cookie.parse(testing.allocator, &test_uri_2, "domain1=9;domain=test.lightpanda.io"), now);
// nothing fancy here // nothing fancy here
try expectCookies("global1=1; global2=2", &jar, test_uri, .{}); 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 }); 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 // We have a cookie where Domain=lightpanda.io
// This should _not_ match xyxlightpanda.io // This should _not_ match xyxlightpanda.io
try expectCookies("", &jar, try std.Uri.parse("http://anothersitelightpanda.io/"), .{ try expectCookies("", &jar, try std.Uri.parse("http://anothersitelightpanda.io/"), .{
.origin_uri = &test_uri, .origin_uri = &test_uri,
.is_http = true,
}); });
// matching path without trailing / // matching path without trailing /
try expectCookies("global1=1; global2=2; path1=3", &jar, try std.Uri.parse("http://lightpanda.io/about"), .{ try expectCookies("global1=1; global2=2; path1=3", &jar, try std.Uri.parse("http://lightpanda.io/about"), .{
.origin_uri = &test_uri, .origin_uri = &test_uri,
.is_http = true,
}); });
// incomplete prefix path // incomplete prefix path
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/abou"), .{ try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/abou"), .{
.origin_uri = &test_uri, .origin_uri = &test_uri,
.is_http = true,
}); });
// path doesn't match // path doesn't match
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/aboutus"), .{ try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/aboutus"), .{
.origin_uri = &test_uri, .origin_uri = &test_uri,
.is_http = true,
}); });
// path doesn't match cookie directory // path doesn't match cookie directory
try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/docs"), .{ try expectCookies("global1=1; global2=2", &jar, try std.Uri.parse("http://lightpanda.io/docs"), .{
.origin_uri = &test_uri, .origin_uri = &test_uri,
.is_http = true,
}); });
// exact directory match // exact directory match
try expectCookies("global1=1; global2=2; path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/"), .{ try expectCookies("global1=1; global2=2; path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/"), .{
.origin_uri = &test_uri, .origin_uri = &test_uri,
.is_http = true,
}); });
// sub directory match // sub directory match
try expectCookies("global1=1; global2=2; path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/more"), .{ try expectCookies("global1=1; global2=2; path2=4", &jar, try std.Uri.parse("http://lightpanda.io/docs/more"), .{
.origin_uri = &test_uri, .origin_uri = &test_uri,
.is_http = true,
}); });
// secure // secure
try expectCookies("global1=1; global2=2; secure=5", &jar, try std.Uri.parse("https://lightpanda.io/"), .{ try expectCookies("global1=1; global2=2; secure=5", &jar, try std.Uri.parse("https://lightpanda.io/"), .{
.origin_uri = &test_uri, .origin_uri = &test_uri,
.is_http = true,
}); });
// navigational cross domain, secure // 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/"), .{ 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/")), .origin_uri = &(try std.Uri.parse("https://example.com/")),
.is_http = true,
}); });
// navigational cross domain, insecure // navigational cross domain, insecure
try expectCookies("global1=1; global2=2; sitelax=7", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{ 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/")), .origin_uri = &(try std.Uri.parse("https://example.com/")),
.is_http = true,
}); });
// non-navigational cross domain, insecure // non-navigational cross domain, insecure
try expectCookies("", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{ try expectCookies("", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{
.origin_uri = &(try std.Uri.parse("https://example.com/")), .origin_uri = &(try std.Uri.parse("https://example.com/")),
.navigation = false, .navigation = false,
.is_http = true,
}); });
// non-navigational cross domain, secure // non-navigational cross domain, secure
try expectCookies("sitenone=6", &jar, try std.Uri.parse("https://lightpanda.io/x/"), .{ try expectCookies("sitenone=6", &jar, try std.Uri.parse("https://lightpanda.io/x/"), .{
.origin_uri = &(try std.Uri.parse("https://example.com/")), .origin_uri = &(try std.Uri.parse("https://example.com/")),
.navigation = false, .navigation = false,
.is_http = true,
}); });
// non-navigational same origin // non-navigational same origin
try expectCookies("global1=1; global2=2; sitelax=7; sitestrict=8", &jar, try std.Uri.parse("http://lightpanda.io/x/"), .{ 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/")), .origin_uri = &(try std.Uri.parse("https://lightpanda.io/")),
.navigation = false, .navigation = false,
.is_http = true,
}); });
// exact domain match + suffix // exact domain match + suffix
try expectCookies("global2=2; domain1=9", &jar, try std.Uri.parse("http://test.lightpanda.io/"), .{ try expectCookies("global2=2; domain1=9", &jar, try std.Uri.parse("http://test.lightpanda.io/"), .{
.origin_uri = &test_uri, .origin_uri = &test_uri,
.is_http = true,
}); });
// domain suffix match + suffix // domain suffix match + suffix
try expectCookies("global2=2; domain1=9", &jar, try std.Uri.parse("http://1.test.lightpanda.io/"), .{ try expectCookies("global2=2; domain1=9", &jar, try std.Uri.parse("http://1.test.lightpanda.io/"), .{
.origin_uri = &test_uri, .origin_uri = &test_uri,
.is_http = true,
}); });
// non-matching domain // non-matching domain
try expectCookies("global2=2", &jar, try std.Uri.parse("http://other.lightpanda.io/"), .{ try expectCookies("global2=2", &jar, try std.Uri.parse("http://other.lightpanda.io/"), .{
.origin_uri = &test_uri, .origin_uri = &test_uri,
.is_http = true,
}); });
const l = jar.cookies.items.len; const l = jar.cookies.items.len;
try expectCookies("global1=1", &jar, test_uri, .{ try expectCookies("global1=1", &jar, test_uri, .{
.request_time = now + 100, .request_time = now + 100,
.origin_uri = &test_uri, .origin_uri = &test_uri,
.is_http = true,
}); });
try testing.expectEqual(l - 1, jar.cookies.items.len); try testing.expectEqual(l - 1, jar.cookies.items.len);

View File

@@ -475,6 +475,7 @@ pub const XMLHttpRequest = struct {
try self.cookie_jar.forRequest(&self.url.?.uri, arr.writer(self.arena), .{ try self.cookie_jar.forRequest(&self.url.?.uri, arr.writer(self.arena), .{
.navigation = false, .navigation = false,
.origin_uri = &self.origin_url.uri, .origin_uri = &self.origin_url.uri,
.is_http = true,
}); });
if (arr.items.len > 0) { if (arr.items.len > 0) {

View File

@@ -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) { if (param.priority != .Medium or param.sameParty != null or param.sourceScheme != null or param.partitionKey != null) {
return error.NotYetImplementedParams; 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); var arena = std.heap.ArenaAllocator.init(cookie_jar.allocator);
errdefer arena.deinit(); errdefer arena.deinit();
@@ -183,7 +181,7 @@ pub const CookieWriter = struct {
if (self.urls) |urls| { if (self.urls) |urls| {
for (self.cookies) |*cookie| { for (self.cookies) |*cookie| {
for (urls) |*url| { 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); try writeCookie(cookie, w);
break; break;
} }
@@ -223,7 +221,8 @@ pub fn writeCookie(cookie: *const Cookie, w: anytype) !void {
try w.objectField("secure"); try w.objectField("secure");
try w.write(cookie.secure); try w.write(cookie.secure);
// TODO session try w.objectField("session");
try w.write(cookie.expires == null);
try w.objectField("sameSite"); try w.objectField("sameSite");
switch (cookie.same_site) { switch (cookie.same_site) {