Issue #3170525 by mcdruid, nullkernel, simonholt83, MustangGB, Znak, axle_foley00, Fabianx, akorkot, cilefen, thalemn, Ayesh, ressa, finne: Set samesite cookie attribute for PHP sessions
parent
975e9c6040
commit
7ba88d9d4f
|
@ -3879,3 +3879,85 @@ function drupal_clear_opcode_cache($filepath) {
|
|||
@apc_delete_file($filepath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drupal's wrapper around PHP's setcookie() function.
|
||||
*
|
||||
* This allows the cookie's $value and $options to be altered.
|
||||
*
|
||||
* @param $name
|
||||
* The name of the cookie.
|
||||
* @param $value
|
||||
* The value of the cookie.
|
||||
* @param $options
|
||||
* An associative array which may have any of the keys expires, path, domain,
|
||||
* secure, httponly, samesite.
|
||||
*
|
||||
* @see setcookie()
|
||||
* @ingroup php_wrappers
|
||||
*/
|
||||
function drupal_setcookie($name, $value, $options) {
|
||||
$options = _drupal_cookie_params($options);
|
||||
if (\PHP_VERSION_ID >= 70300) {
|
||||
setcookie($name, $value, $options);
|
||||
}
|
||||
else {
|
||||
setcookie($name, $value, $options['expires'], $options['path'], $options['domain'], $options['secure'], $options['httponly']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the params for cookies. This emulates support for the SameSite
|
||||
* attribute in earlier versions of PHP, and allows the value of that attribute
|
||||
* to be overridden.
|
||||
*
|
||||
* @param $options
|
||||
* An associative array which may have any of the keys expires, path, domain,
|
||||
* secure, httponly, samesite.
|
||||
*
|
||||
* @return
|
||||
* An associative array which may have any of the keys expires, path, domain,
|
||||
* secure, httponly, and samesite.
|
||||
*/
|
||||
function _drupal_cookie_params($options) {
|
||||
$options['samesite'] = _drupal_samesite_cookie($options);
|
||||
if (\PHP_VERSION_ID < 70300) {
|
||||
// Emulate SameSite support in older PHP versions.
|
||||
if (!empty($options['samesite'])) {
|
||||
// Ensure the SameSite attribute is only added once.
|
||||
if (!preg_match('/SameSite=/i', $options['path'])) {
|
||||
$options['path'] .= '; SameSite=' . $options['samesite'];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the value for the samesite cookie attribute, in the following order
|
||||
* of precedence:
|
||||
*
|
||||
* 1) A value explicitly passed to drupal_setcookie()
|
||||
* 2) A value set in $conf['samesite_cookie_value']
|
||||
* 3) The setting from php ini
|
||||
* 4) The default of None, or FALSE (no attribute) if the cookie is not Secure
|
||||
*
|
||||
* @param $options
|
||||
* An associative array as passed to drupal_setcookie().
|
||||
* @return
|
||||
* The value for the samesite cookie attribute.
|
||||
*/
|
||||
function _drupal_samesite_cookie($options) {
|
||||
if (isset($options['samesite'])) {
|
||||
return $options['samesite'];
|
||||
}
|
||||
$override = variable_get('samesite_cookie_value', NULL);
|
||||
if ($override !== NULL) {
|
||||
return $override;
|
||||
}
|
||||
$ini_options = session_get_cookie_params();
|
||||
if (isset($ini_options['samesite'])) {
|
||||
return $ini_options['samesite'];
|
||||
}
|
||||
return empty($options['secure']) ? FALSE : 'None';
|
||||
}
|
||||
|
|
|
@ -284,6 +284,20 @@ function drupal_session_start() {
|
|||
// Save current session data before starting it, as PHP will destroy it.
|
||||
$session_data = isset($_SESSION) ? $_SESSION : NULL;
|
||||
|
||||
// Apply any overrides to the session cookie params.
|
||||
$params = $original_params = session_get_cookie_params();
|
||||
// PHP settings for samesite will be handled by _drupal_cookie_params().
|
||||
unset($params['samesite']);
|
||||
$params = _drupal_cookie_params($params);
|
||||
if ($params !== $original_params) {
|
||||
if (\PHP_VERSION_ID >= 70300) {
|
||||
session_set_cookie_params($params);
|
||||
}
|
||||
else {
|
||||
session_set_cookie_params($params['lifetime'], $params['path'], $params['domain'], $params['secure'], $params['httponly']);
|
||||
}
|
||||
}
|
||||
|
||||
session_start();
|
||||
drupal_session_started(TRUE);
|
||||
|
||||
|
@ -323,7 +337,14 @@ function drupal_session_commit() {
|
|||
$insecure_session_name = substr(session_name(), 1);
|
||||
$params = session_get_cookie_params();
|
||||
$expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
|
||||
setcookie($insecure_session_name, $_COOKIE[$insecure_session_name], $expire, $params['path'], $params['domain'], FALSE, $params['httponly']);
|
||||
$options = array(
|
||||
'expires' => $expire,
|
||||
'path' => $params['path'],
|
||||
'domain' => $params['domain'],
|
||||
'secure' => FALSE,
|
||||
'httponly' => $params['httponly'],
|
||||
);
|
||||
drupal_setcookie($insecure_session_name, $_COOKIE[$insecure_session_name], $options);
|
||||
}
|
||||
}
|
||||
// Write the session data.
|
||||
|
@ -365,7 +386,14 @@ function drupal_session_regenerate() {
|
|||
// $params['lifetime'] seconds from the current request. If it is not set,
|
||||
// it will expire when the browser is closed.
|
||||
$expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
|
||||
setcookie($insecure_session_name, $session_id, $expire, $params['path'], $params['domain'], FALSE, $params['httponly']);
|
||||
$options = array(
|
||||
'expires' => $expire,
|
||||
'path' => $params['path'],
|
||||
'domain' => $params['domain'],
|
||||
'secure' => FALSE,
|
||||
'httponly' => $params['httponly'],
|
||||
);
|
||||
drupal_setcookie($insecure_session_name, $session_id, $options);
|
||||
$_COOKIE[$insecure_session_name] = $session_id;
|
||||
}
|
||||
|
||||
|
@ -380,7 +408,14 @@ function drupal_session_regenerate() {
|
|||
if (isset($old_session_id)) {
|
||||
$params = session_get_cookie_params();
|
||||
$expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
|
||||
setcookie(session_name(), session_id(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
|
||||
$options = array(
|
||||
'expires' => $expire,
|
||||
'path' => $params['path'],
|
||||
'domain' => $params['domain'],
|
||||
'secure' => $params['secure'],
|
||||
'httponly' => $params['httponly'],
|
||||
);
|
||||
drupal_setcookie(session_name(), session_id(), $options);
|
||||
$fields = array('sid' => session_id());
|
||||
if ($is_https) {
|
||||
$fields['ssid'] = session_id();
|
||||
|
@ -488,7 +523,14 @@ function _drupal_session_delete_cookie($name, $secure = NULL) {
|
|||
if ($secure !== NULL) {
|
||||
$params['secure'] = $secure;
|
||||
}
|
||||
setcookie($name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
|
||||
$options = array(
|
||||
'expires' => REQUEST_TIME - 3600,
|
||||
'path' => $params['path'],
|
||||
'domain' => $params['domain'],
|
||||
'secure' => $params['secure'],
|
||||
'httponly' => $params['httponly'],
|
||||
);
|
||||
drupal_setcookie($name, '', $options);
|
||||
unset($_COOKIE[$name]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -244,6 +244,187 @@ class SessionTestCase extends DrupalWebTestCase {
|
|||
$this->assertResponse(403, 'An empty session ID is not allowed.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test absence of SameSite attribute on session cookies by default.
|
||||
*/
|
||||
function testNoSameSiteCookieAttributeDefault() {
|
||||
$user = $this->drupalCreateUser(array('access content'));
|
||||
$this->sessionReset($user->uid);
|
||||
if (\PHP_VERSION_ID < 70300) {
|
||||
$this->drupalLogin($user);
|
||||
}
|
||||
else {
|
||||
// PHP often defaults to an empty value for session.cookie_samesite but
|
||||
// that may vary, so we set an explicit empty value.
|
||||
// Send our own login POST so that we can pass a custom header to trigger
|
||||
// session_test.module to call ini_set('session.cookie_samesite', $value)
|
||||
$headers[] = 'X-Session-Cookie-Ini-Set: *EMPTY*';
|
||||
$edit = array(
|
||||
'name' => $user->name,
|
||||
'pass' => $user->pass_raw,
|
||||
);
|
||||
$this->drupalPost('user', $edit, t('Log in'), array(), $headers);
|
||||
}
|
||||
$this->assertFalse(preg_match('/SameSite=/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie has no SameSite attribute (default).');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test SameSite attribute = None by default on Secure session cookies.
|
||||
*/
|
||||
function testSameSiteCookieAttributeNoneSecure() {
|
||||
$user = $this->drupalCreateUser(array('access content'));
|
||||
$this->sessionReset($user->uid);
|
||||
$headers = array();
|
||||
if (\PHP_VERSION_ID >= 70300) {
|
||||
// Send our own login POST so that we can pass a custom header to trigger
|
||||
// session_test.module to call ini_set('session.cookie_samesite', $value)
|
||||
$headers[] = 'X-Session-Cookie-Ini-Set: None';
|
||||
}
|
||||
// Test HTTPS session handling by altering the form action to submit the
|
||||
// login form through https.php, which creates a mock HTTPS request.
|
||||
$this->drupalGet('user');
|
||||
$form = $this->xpath('//form[@id="user-login"]');
|
||||
$form[0]['action'] = $this->httpsUrl('user');
|
||||
$edit = array('name' => $user->name, 'pass' => $user->pass_raw);
|
||||
$this->drupalPost(NULL, $edit, t('Log in'), array(), $headers);
|
||||
$this->assertTrue(preg_match('/SameSite=None/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie is set as SameSite=None.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test SameSite attribute = None on session cookies.
|
||||
*/
|
||||
function testSameSiteCookieAttributeNone() {
|
||||
variable_set('samesite_cookie_value', 'None');
|
||||
$user = $this->drupalCreateUser(array('access content'));
|
||||
$this->sessionReset($user->uid);
|
||||
$this->drupalLogin($user);
|
||||
$this->assertTrue(preg_match('/SameSite=None/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie is set as SameSite=None.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test SameSite attribute = Lax on session cookies.
|
||||
*/
|
||||
function testSameSiteCookieAttributeLax() {
|
||||
variable_set('samesite_cookie_value', 'Lax');
|
||||
$user = $this->drupalCreateUser(array('access content'));
|
||||
$this->sessionReset($user->uid);
|
||||
$this->drupalLogin($user);
|
||||
$this->assertTrue(preg_match('/SameSite=Lax/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie is set as SameSite=Lax.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test SameSite attribute = Strict on session cookies.
|
||||
*/
|
||||
function testSameSiteCookieAttributeStrict() {
|
||||
variable_set('samesite_cookie_value', 'Strict');
|
||||
$user = $this->drupalCreateUser(array('access content'));
|
||||
$this->sessionReset($user->uid);
|
||||
$this->drupalLogin($user);
|
||||
$this->assertTrue(preg_match('/SameSite=Strict/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie is set as SameSite=Strict.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test disabling the samesite attribute on session cookies via $conf
|
||||
*/
|
||||
function testSameSiteCookieAttributeDisabledViaConf() {
|
||||
$user = $this->drupalCreateUser(array('access content'));
|
||||
$this->sessionReset($user->uid);
|
||||
variable_set('samesite_cookie_value', FALSE);
|
||||
if (\PHP_VERSION_ID < 70300) {
|
||||
// There is no session.cookie_samesite in earlier PHP versions.
|
||||
$this->drupalLogin($user);
|
||||
}
|
||||
else {
|
||||
// Send our own login POST so that we can pass a custom header to trigger
|
||||
// session_test.module to call ini_set('session.cookie_samesite', $value)
|
||||
$headers[] = 'X-Session-Cookie-Ini-Set: Lax';
|
||||
$edit = array(
|
||||
'name' => $user->name,
|
||||
'pass' => $user->pass_raw,
|
||||
);
|
||||
$this->drupalPost('user', $edit, t('Log in'), array(), $headers);
|
||||
}
|
||||
$this->assertFalse(preg_match('/SameSite=/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie has no SameSite attribute (conf).');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test disabling the samesite attribute on session cookies via php ini
|
||||
*/
|
||||
function testSameSiteCookieAttributeDisabledViaPhpIni() {
|
||||
if (\PHP_VERSION_ID < 70300) {
|
||||
// There is no session.cookie_samesite in earlier PHP versions.
|
||||
$this->pass('This test is only for PHP 7.3 and later.');
|
||||
return;
|
||||
}
|
||||
$user = $this->drupalCreateUser(array('access content'));
|
||||
// Send our own login POST so that we can pass a custom header to trigger
|
||||
// session_test.module to call ini_set('session.cookie_samesite', $value)
|
||||
$headers[] = 'X-Session-Cookie-Ini-Set: *EMPTY*';
|
||||
$edit = array(
|
||||
'name' => $user->name,
|
||||
'pass' => $user->pass_raw,
|
||||
);
|
||||
$this->drupalPost('user', $edit, t('Log in'), array(), $headers);
|
||||
$this->assertFalse(preg_match('/SameSite=/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie has no SameSite attribute (ini).');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that a PHP setting for session.cookie_samesite is not overridden by
|
||||
* the default value in Drupal, without a samesite_cookie_value variable.
|
||||
*/
|
||||
function testSamesiteCookiePhpSettingLax() {
|
||||
if (\PHP_VERSION_ID < 70300) {
|
||||
// There is no session.cookie_samesite in earlier PHP versions.
|
||||
$this->pass('This test is only for PHP 7.3 and later.');
|
||||
return;
|
||||
}
|
||||
$user = $this->drupalCreateUser(array('access content'));
|
||||
// Send our own login POST so that we can pass a custom header to trigger
|
||||
// session_test.module to call ini_set('session.cookie_samesite', $value)
|
||||
$headers[] = 'X-Session-Cookie-Ini-Set: Lax';
|
||||
$edit = array(
|
||||
'name' => $user->name,
|
||||
'pass' => $user->pass_raw,
|
||||
);
|
||||
$this->drupalPost('user', $edit, t('Log in'), array(), $headers);
|
||||
$this->assertTrue(preg_match('/SameSite=Lax/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie is set as SameSite=Lax.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test overriding the PHP setting for session.cookie_samesite with the
|
||||
* samesite_cookie_value variable.
|
||||
*/
|
||||
function testSamesiteCookieOverrideLaxToStrict() {
|
||||
if (\PHP_VERSION_ID < 70300) {
|
||||
// There is no session.cookie_samesite in earlier PHP versions.
|
||||
$this->pass('This test is only for PHP 7.3 and later.');
|
||||
return;
|
||||
}
|
||||
variable_set('samesite_cookie_value', 'Strict');
|
||||
$user = $this->drupalCreateUser(array('access content'));
|
||||
// Send our own login POST so that we can pass a custom header to trigger
|
||||
// session_test.module to call ini_set('session.cookie_samesite', $value)
|
||||
$headers[] = 'X-Session-Cookie-Ini-Set: Lax';
|
||||
$edit = array(
|
||||
'name' => $user->name,
|
||||
'pass' => $user->pass_raw,
|
||||
);
|
||||
$this->drupalPost('user', $edit, t('Log in'), array(), $headers);
|
||||
$this->assertTrue(preg_match('/SameSite=Strict/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie is set as SameSite=Strict.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test SameSite attribute = Lax on set-cookie header on logout.
|
||||
*/
|
||||
function testSamesiteCookieLogoutLax() {
|
||||
variable_set('samesite_cookie_value', 'Lax');
|
||||
$user = $this->drupalCreateUser(array('access content'));
|
||||
$this->sessionReset($user->uid);
|
||||
$this->drupalLogin($user);
|
||||
$this->drupalGet('user/logout');
|
||||
$this->assertTrue(preg_match('/SameSite=Lax/i', $this->drupalGetHeader('Set-Cookie', TRUE)), 'Session cookie deletion includes SameSite=Lax.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the cookie file so that it refers to the specified user.
|
||||
*
|
||||
|
@ -285,6 +466,20 @@ class SessionTestCase extends DrupalWebTestCase {
|
|||
$this->assertIdentical($this->drupalGetHeader('X-Session-Empty'), '0', 'Session was not empty.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a URL for submitting a mock HTTPS request to HTTP test environments.
|
||||
*
|
||||
* @param $url
|
||||
* A Drupal path such as 'user'.
|
||||
*
|
||||
* @return
|
||||
* An absolute URL.
|
||||
*/
|
||||
protected function httpsUrl($url) {
|
||||
global $base_url;
|
||||
return $base_url . '/modules/simpletest/tests/https.php?q=' . $url;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -64,6 +64,19 @@ function session_test_menu() {
|
|||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* It's very unusual to do anything outside of a function / hook, but in this
|
||||
* case we want to simulate a given session.cookie_samesite setting in php.ini
|
||||
* or via ini_set() in settings.php. This would almost never be a good idea
|
||||
* outside of a test scenario.
|
||||
*/
|
||||
if (isset($_SERVER['HTTP_X_SESSION_COOKIE_INI_SET'])) {
|
||||
if (in_array($_SERVER['HTTP_X_SESSION_COOKIE_INI_SET'], array('None', 'Lax', 'Strict', '*EMPTY*'))) {
|
||||
$value = ($_SERVER['HTTP_X_SESSION_COOKIE_INI_SET'] == '*EMPTY*') ? '' : $_SERVER['HTTP_X_SESSION_COOKIE_INI_SET'];
|
||||
ini_set('session.cookie_samesite', $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_boot().
|
||||
*/
|
||||
|
|
|
@ -725,3 +725,23 @@ $conf['field_sql_storage_skip_writing_unchanged_fields'] = TRUE;
|
|||
* @see drupal_mail()
|
||||
*/
|
||||
$conf['mail_display_name_site_name'] = TRUE;
|
||||
|
||||
/**
|
||||
* SameSite cookie attribute.
|
||||
*
|
||||
* This variable can be used to set a value for the SameSite cookie attribute.
|
||||
*
|
||||
* Versions of PHP before 7.3 have no native support for the SameSite attribute
|
||||
* so it is emulated.
|
||||
*
|
||||
* The session.cookie-samesite setting in PHP 7.3 and later will be overridden
|
||||
* by this variable for Drupal session cookies, and any other cookies managed
|
||||
* with drupal_setcookie().
|
||||
*
|
||||
* Setting this variable to FALSE disables the SameSite attribute on cookies.
|
||||
*
|
||||
* @see drupal_setcookie()
|
||||
* @see drupal_session_start()
|
||||
* @see https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-samesite
|
||||
*/
|
||||
#$conf['samesite_cookie_value'] = 'None';
|
||||
|
|
Loading…
Reference in New Issue