Skip to content

Commit fd205a1

Browse files
ENH Add samesite attribute to cookies.
Co-authored-by: pine3ree <[email protected]>
1 parent 6f27dad commit fd205a1

File tree

7 files changed

+173
-5
lines changed

7 files changed

+173
-5
lines changed

docs/en/02_Developer_Guides/09_Security/04_Secure_Coding.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -762,11 +762,15 @@ disable this behaviour using `CanonicalURLMiddleware::singleton()->setForceBasic
762762
configuration in YAML.
763763

764764
We also want to ensure cookies are not shared between secure and non-secure sessions, so we must tell Silverstripe CMS to
765-
use a [secure session](https://docs.silverstripe.org/en/3/developer_guides/cookies_and_sessions/sessions/#secure-session-cookie).
765+
use a [secure session](https://docs.silverstripe.org/en/3/developer_guides/cookies_and_sessions/sessions/#secure-session-cookie).
766+
It is also a good idea to set the `samesite` attribute for the session cookie to `Strict` unless you have a specific use case for
767+
sharing the session cookie across domains.
768+
766769
To do this, you may set the `cookie_secure` parameter to `true` in your `config.yml` for `Session`
767770

768771
```yml
769772
SilverStripe\Control\Session:
773+
cookies_samesite: 'Strict'
770774
cookie_secure: true
771775
```
772776
@@ -784,6 +788,12 @@ SilverStripe\Core\Injector\Injector:
784788
TokenCookieSecure: true
785789
```
786790
791+
[info]
792+
There is not currently an easy way to pass a `securesite` attribute value for setting this cookie - but you can set the
793+
default value for the attribute for all cookies, or use an `Extension` to apply it for this specific cookie. See
794+
[the main cookies documentation](/developer_guides/cookies_and_sessions/cookies#samesite-attribute) for more information.
795+
[/info]
796+
787797
For other cookies set by your application we should also ensure the users are provided with secure cookies by setting
788798
the "Secure" and "HTTPOnly" flags. These flags prevent them from being stolen by an attacker through javascript.
789799

docs/en/02_Developer_Guides/18_Cookies_And_Sessions/01_Cookies.md

+42
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ icon: cookie-bite
55
---
66

77
# Cookies
8+
9+
Note that cookies can have security implications - before setting your own cookies, make sure to read through the
10+
[secure coding](/developer_guides/security/secure_coding#secure-sessions-cookies-and-tls-https) documentation.
11+
812
## Accessing and Manipulating Cookies
913

1014
Cookies are a mechanism for storing data in the remote browser and thus tracking or identifying return users.
@@ -52,6 +56,44 @@ Cookie::force_expiry($name, $path = null, $domain = null);
5256
// Cookie::force_expiry('MyApplicationPreference')
5357
```
5458

59+
### Samesite attribute
60+
61+
The `samesite` attribute is set on all cookies with a default value of `Lax`. You can change the default value by setting the `default_samesite` value on the
62+
[Cookie](api:SilverStripe\Control\Cookie) class:
63+
64+
```yml
65+
SilverStripe\Control\Cookie:
66+
default_samesite: 'Strict'
67+
```
68+
69+
If you need to set the `samesite` attribute for a specific cookie, you can implement the `updateSameSite()` method on an `Extension` subclass and apply that to
70+
[CookieJar](api:SilverStripe\Control\CookieJar) - though note that this extension method will stop working in 5.0 in favour of a new parameter for passing the
71+
`samesite` attribute value directly.
72+
73+
```yml
74+
SilverStripe\Control\CookieJar:
75+
extensions:
76+
- App\Extension\CookieJarExtension
77+
```
78+
79+
```php
80+
<?php
81+
82+
use SilverStripe\Core\Extension;
83+
84+
namespace App\Extension;
85+
86+
class CookieJarExtension extends Extension
87+
{
88+
public function updateSameSite(string $cookieName, string $sameSite): void
89+
{
90+
if ($cookieName === 'my-cookie') {
91+
$sameSite = 'Lax';
92+
}
93+
}
94+
}
95+
```
96+
5597
## Cookie_Backend
5698

5799
The [Cookie](api:SilverStripe\Control\Cookie) class manipulates and sets cookies using a [Cookie_Backend](api:SilverStripe\Control\Cookie_Backend). The backend is in charge of the logic

docs/en/02_Developer_Guides/18_Cookies_And_Sessions/02_Sessions.md

+15-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,19 @@ including form and page comment information. None of this is vital but `clear_al
101101
$session->clearAll();
102102
```
103103

104-
## Secure Session Cookie
104+
## Cookies
105+
106+
### Samesite attribute
107+
108+
The session cookie is handled slightly differently than most cookies on the site, which provides the opportunity to handle the samesite attribute separately from other cookies.
109+
It will respect the default value, but you can also set a `samesite` attribute that differs from the default:
110+
111+
```yml
112+
SilverStripe\Control\Session:
113+
cookies_samesite: 'Strict'
114+
```
115+
116+
### Secure Session Cookie
105117
106118
In certain circumstances, you may want to use a different `session_name` cookie when using the `https` protocol for security purposes. To do this, you may set the `cookie_secure` parameter to `true` on your `config.yml`
107119

@@ -113,6 +125,8 @@ SilverStripe\Control\Session:
113125

114126
This uses the session_name `SECSESSID` for `https` connections instead of the default `PHPSESSID`. Doing so adds an extra layer of security to your session cookie since you no longer share `http` and `https` sessions.
115127

128+
Note that if you set `cookies_samesite` to `None` (which is _strongly_ discouraged), the `cookie_secure` value will _always_ be `true`.
129+
116130
## Relaxing checks around user agent strings
117131

118132
Out of the box, Silverstripe CMS will invalidate a user's session if the `User-Agent` header changes. This provides some supplemental protection against session high-jacking attacks.

docs/en/04_Changelogs/4.12.0.md

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
title: 4.12.0 (unreleased)
3+
---
4+
5+
# 4.12.0 (unreleased)
6+
7+
## Overview
8+
9+
- [Regression test and Security audit](#audit)
10+
- [Features and enhancements](#features-and-enhancements)
11+
- [Samesite attribute on cookies](#cookies-samesite)
12+
- [Other features](#other-features)
13+
- [Bugfixes](#bugfixes)
14+
15+
## Regression test and Security audit{#audit}
16+
17+
This release has been comprehensively regression tested and passed to a third party for a security-focused audit.
18+
19+
While it is still advised that you perform your own due diligence when upgrading your project, this work is performed to ensure a safe and secure upgrade with each recipe release.
20+
21+
## Features and enhancements {#features-and-enhancements}
22+
23+
### Samesite attribute on cookies {#cookies-samesite}
24+
25+
The `samesite` attribute is now set on all cookies. To avoid backward compatability issues, the `Lax` value has been set by default, but we recommend reviewing the requirements of your project and setting an appropriate value.
26+
27+
The default value can be set for all cookies in yaml configuration like so:
28+
29+
```yml
30+
SilverStripe\Control\Cookie:
31+
default_samesite: 'Strict'
32+
```
33+
34+
If you need to set the `samesite` attribute for a specific cookie, you can do that too. Check out the [cookies documentation](/developer_guides/cookies_and_sessions/cookies#samesite-attribute) for more information.
35+
36+
The session cookie is handled separately. It will respect the default value above, but you can also configure it with its own value like so:
37+
38+
```yml
39+
SilverStripe\Control\Session:
40+
cookies_samesite: 'Strict'
41+
```
42+
43+
Note that if you set the `samesite` attribute to `None`, the `secure` is automatically set to `true` as required by the specification.
44+
45+
For more information about the `samesite` attribute check out https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
46+
47+
### Other new features {#other-features}
48+
49+
## Bugfixes {#bugfixes}
50+
51+
This release includes a number of bug fixes to improve a broad range of areas. Check the change logs for full details of these fixes split by module. Thank you to the community members that helped contribute these fixes as part of the release!
52+
53+
<!--- Changes below this line will be automatically regenerated -->
54+
55+
<!--- Changes above this line will be automatically regenerated -->

src/Control/Cookie.php

+6
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ class Cookie
1919
*/
2020
private static $report_errors = true;
2121

22+
/**
23+
* Must be "Strict", "Lax", or "None"
24+
* @config
25+
*/
26+
private static string $default_samesite = 'Lax';
27+
2228
/**
2329
* Fetch the current instance of the cookie backend.
2430
*

src/Control/CookieJar.php

+27-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use SilverStripe\ORM\FieldType\DBDatetime;
66
use LogicException;
7+
use SilverStripe\Core\Extensible;
78

89
/**
910
* A default backend for the setting and getting of cookies
@@ -18,6 +19,7 @@
1819
*/
1920
class CookieJar implements Cookie_Backend
2021
{
22+
use Extensible;
2123

2224
/**
2325
* Hold the cookies that were existing at time of instantiation (ie: The ones
@@ -168,9 +170,17 @@ protected function outputCookie(
168170
$secure = false,
169171
$httpOnly = true
170172
) {
173+
$sameSite = $this->getSameSite($name);
171174
// if headers aren't sent, we can set the cookie
172175
if (!headers_sent($file, $line)) {
173-
return setcookie($name ?? '', $value ?? '', $expiry ?? 0, $path ?? '', $domain ?? '', $secure ?? false, $httpOnly ?? false);
176+
return setcookie($name ?? '', $value ?? '', [
177+
'expires' => $expiry ?? 0,
178+
'path' => $path ?? '',
179+
'domain' => $domain ?? '',
180+
'secure' => ($sameSite === 'None') ? true : (bool)$secure,
181+
'httponly' => $httpOnly ?? false,
182+
'samesite' => $sameSite,
183+
]);
174184
}
175185

176186
if (Cookie::config()->uninherited('report_errors')) {
@@ -180,4 +190,20 @@ protected function outputCookie(
180190
}
181191
return false;
182192
}
193+
194+
/**
195+
* Get the correct samesite value - if this is a session cookie it may be different to the default value.
196+
*
197+
* @deprecated 5.0 The relevant methods will include a `$sameSite` parameter instead.
198+
*/
199+
private function getSameSite(string $name): string
200+
{
201+
if ($name === session_name()) {
202+
return Session::config()->get('cookies_samesite') ?: Cookie::config()->get('default_samesite');
203+
}
204+
$sameSite = Cookie::config()->get('default_samesite');
205+
// This extension hook will also be removed in 5.0
206+
$this->extend('updateSameSite', $name, $sameSite);
207+
return $sameSite;
208+
}
183209
}

src/Control/Session.php

+17-2
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,13 @@ class Session
135135
*/
136136
private static $cookie_name_secure = 'SECSESSID';
137137

138+
/**
139+
* Must be "Strict", "Lax", "None", or empty.
140+
* If empty, the default value will be applied.
141+
* @config
142+
*/
143+
private static string $cookies_samesite = '';
144+
138145
/**
139146
* Name of session cache limiter to use.
140147
* Defaults to '' to disable cache limiter entirely.
@@ -288,7 +295,6 @@ public function start(HTTPRequest $request)
288295
$path = Director::baseURL();
289296
}
290297
$domain = $this->config()->get('cookie_domain');
291-
$secure = Director::is_https($request) && $this->config()->get('cookie_secure');
292298
$session_path = $this->config()->get('session_store_path');
293299
$timeout = $this->config()->get('timeout');
294300

@@ -307,7 +313,16 @@ public function start(HTTPRequest $request)
307313
$data = [];
308314
if (!session_id() && (!headers_sent() || $this->requestContainsSessionId($request))) {
309315
if (!headers_sent()) {
310-
session_set_cookie_params($timeout ?: 0, $path, $domain ?: null, $secure, true);
316+
$sameSite = static::config()->get('cookies_samesite') ?: Cookie::config()->get('default_samesite');
317+
$secure = ($sameSite === 'None') ? true : Director::is_https($request) && $this->config()->get('cookie_secure');
318+
session_set_cookie_params([
319+
'lifetime' => $timeout ?: 0,
320+
'path' => $path,
321+
'domain' => $domain ?: null,
322+
'secure' => $secure,
323+
'httponly' => true,
324+
'samesite' => $sameSite,
325+
]);
311326

312327
$limiter = $this->config()->get('sessionCacheLimiter');
313328
if (isset($limiter)) {

0 commit comments

Comments
 (0)