From 5e0694091fa82004c77b64c51d2369713c164d4b Mon Sep 17 00:00:00 2001 From: rawleyfowler Date: Wed, 28 Aug 2024 11:45:25 -0500 Subject: [PATCH] Add partitioned cookies --- lib/Mojo/Cookie/Response.pm | 17 ++++++++++++++--- lib/Mojolicious/Sessions.pm | 25 ++++++++++++++++++------- t/mojo/cookie.t | 15 ++++++++++++++- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/lib/Mojo/Cookie/Response.pm b/lib/Mojo/Cookie/Response.pm index ea1419c8f6..b35fdd96f5 100644 --- a/lib/Mojo/Cookie/Response.pm +++ b/lib/Mojo/Cookie/Response.pm @@ -4,9 +4,9 @@ use Mojo::Base 'Mojo::Cookie'; use Mojo::Date; use Mojo::Util qw(quote split_cookie_header); -has [qw(domain expires host_only httponly max_age path samesite secure)]; +has [qw(domain expires host_only httponly max_age partitioned path samesite secure)]; -my %ATTRS = map { $_ => 1 } qw(domain expires httponly max-age path samesite secure); +my %ATTRS = map { $_ => 1 } qw(domain expires httponly max-age partitioned path samesite secure); sub parse { my ($self, $str) = @_; @@ -21,7 +21,7 @@ sub parse { next unless $ATTRS{my $attr = lc $name}; $value =~ s/^\.// if $attr eq 'domain' && defined $value; $value = Mojo::Date->new($value // '')->epoch if $attr eq 'expires'; - $value = 1 if $attr eq 'secure' || $attr eq 'httponly'; + $value = 1 if $attr eq 'secure' || $attr eq 'httponly' || $attr eq 'partitioned'; $cookies[-1]{$attr eq 'max-age' ? 'max_age' : $attr} = $value; } } @@ -47,6 +47,9 @@ sub to_string { # "path" if (my $path = $self->path) { $cookie .= "; path=$path" } + # "Partitioned" + $cookie .= "; Partitioned" if $self->partitioned; + # "secure" $cookie .= "; secure" if $self->secure; @@ -123,6 +126,14 @@ HttpOnly flag, which can prevent client-side scripts from accessing this cookie. Max age for cookie. +=head2 partitioned + + my $partitioned = $cookie->partitioned; + $cookie = $cookie->parititoned(1); + +Partitioned flag, this is to be used in accordance to the L. + =head2 path my $path = $cookie->path; diff --git a/lib/Mojolicious/Sessions.pm b/lib/Mojolicious/Sessions.pm index 989ad5faaa..46cbca3fde 100644 --- a/lib/Mojolicious/Sessions.pm +++ b/lib/Mojolicious/Sessions.pm @@ -4,7 +4,7 @@ use Mojo::Base -base; use Mojo::JSON; use Mojo::Util qw(b64_decode b64_encode); -has [qw(cookie_domain secure)]; +has [qw(cookie_domain partitioned secure)]; has cookie_name => 'mojolicious'; has cookie_path => '/'; has default_expiration => 3600; @@ -51,12 +51,13 @@ sub store { my $value = b64_encode $self->serialize->($session), ''; $value =~ y/=/-/; my $options = { - domain => $self->cookie_domain, - expires => $session->{expires}, - httponly => 1, - path => $self->cookie_path, - samesite => $self->samesite, - secure => $self->secure + domain => $self->cookie_domain, + expires => $session->{expires}, + httponly => 1, + partitioned => $self->partitioned, + path => $self->cookie_path, + samesite => $self->samesite, + secure => $self->secure, }; $c->signed_cookie($self->cookie_name, $value, $options); } @@ -143,6 +144,16 @@ A callback used to deserialize sessions, defaults to L. $sessions->deserialize(sub ($bytes) { return {} }); +=head2 partitioned + + my $bool = $sessions->partitioned; + $sessions = $sessions->partitioned($bool); + +Set the partitioned flag on all session cookies, this is used in accordance to the L. + +Partitioned cookies are held within a separate cookie jar per top-level site. + =head2 samesite my $samesite = $sessions->samesite; diff --git a/t/mojo/cookie.t b/t/mojo/cookie.t index 2a61029d7b..c8f1f88ce3 100644 --- a/t/mojo/cookie.t +++ b/t/mojo/cookie.t @@ -168,13 +168,14 @@ subtest 'Full response cookie as string' => sub { $cookie->value('ba r'); $cookie->domain('example.com'); $cookie->path('/test'); + $cookie->partitioned(1); $cookie->max_age(60); $cookie->expires(1218092879); $cookie->secure(1); $cookie->httponly(1); $cookie->samesite('Lax'); is $cookie->to_string, '0="ba r"; expires=Thu, 07 Aug 2008 07:07:59 GMT; domain=example.com;' - . ' path=/test; secure; HttpOnly; SameSite=Lax; Max-Age=60', 'right format'; + . ' path=/test; Partitioned; secure; HttpOnly; SameSite=Lax; Max-Age=60', 'right format'; }; subtest 'Empty response cookie' => sub { @@ -216,6 +217,18 @@ subtest 'Parse response cookie (RFC 6265)' => sub { is $cookies->[1], undef, 'no more cookies'; }; +subtest 'Partitioned cookie (RFC 6265 CHIPS)' => sub { + my $cookies + = Mojo::Cookie::Response->parse( + 'foo="bar"; Domain=example.com; Partitioned; Path=/test; Max-Age=60; Expires=Thu, 07 Aug 2008 07:07:59 GMT; Secure;' + ); + is $cookies->[0]->partitioned, 1, 'partitioned set?'; + + $cookies = Mojo::Cookie::Response->parse( + 'foo="bar"; Domain=example.com; Path=/test; Max-Age=60; Expires=Thu, 07 Aug 2008 07:07:59 GMT; Secure;'); + is $cookies->[0]->partitioned, undef, 'partitioned not set?'; +}; + subtest 'Parse response cookie with invalid flag (RFC 6265)' => sub { my $cookies = Mojo::Cookie::Response->parse( 'foo="ba r"; Domain=.example.com; Path=/test; Max-Age=60;' . ' Expires=Thu, 07 Aug 2008 07:07:59 GMT; InSecure;');