1
0
mirror of https://github.com/processwire/processwire.git synced 2025-08-09 08:17:12 +02:00

Add PR #187 - add cookie SameSite support and settings (session and other cookies)

This commit is contained in:
pine3ree
2021-05-10 14:46:37 -04:00
committed by Ryan Cramer
parent ddce5e5cda
commit d29ed3eb96
6 changed files with 154 additions and 13 deletions

View File

@@ -420,6 +420,25 @@ $config->sessionCookieSecure = 1;
*/ */
$config->sessionCookieDomain = null; $config->sessionCookieDomain = null;
/**
* Cookie “SameSite” value for sessions - “Lax” (default) or “Strict”
*
* - `Lax`: The session cookie will be sent along with the GET requests initiated by third party website.
* This ensures an existing session on this site is maintained when clicking to it from another site.
*
* - `Strict`: The session cookie will not be sent along with requests initiated by third party websites.
* If user already has a login session on this site, it wont be recognized when clicking from another
* site to this one.
*
* The default/recommended value is `Lax`.
*
* @var string
* @since 3.0.178
* @see https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-samesite
*
*/
$config->sessionCookieSameSite = 'Lax';
/** /**
* Number of session history entries to record. * Number of session history entries to record.
* *
@@ -1020,6 +1039,7 @@ $config->wireInputLazy = false;
* #property int age Max age of cookies in seconds or 0 to expire with session (3600=1hr, 86400=1day, 604800=1week, 2592000=30days, etc.) * #property int age Max age of cookies in seconds or 0 to expire with session (3600=1hr, 86400=1day, 604800=1week, 2592000=30days, etc.)
* #property string|null Cookie path or null for PW installations root URL (default=null). * #property string|null Cookie path or null for PW installations root URL (default=null).
* #property string|null|bool domain Cookie domain: null for current hostname, true for all subdomains of current domain, domain.com for domain and all subdomains, www.domain.com for www subdomain. * #property string|null|bool domain Cookie domain: null for current hostname, true for all subdomains of current domain, domain.com for domain and all subdomains, www.domain.com for www subdomain.
* #property string samesite When set to “Lax” cookies are preserved on GET requests to this site originated from external links. May also be 'Strict' or 'None' ('secure' option required for 'None'). 3.0.178+
* #property bool|null secure Transmit cookies only over secure HTTPS connection? (true, false, or null to auto-detect, using true option for cookies set when HTTPS is active). * #property bool|null secure Transmit cookies only over secure HTTPS connection? (true, false, or null to auto-detect, using true option for cookies set when HTTPS is active).
* #property bool httponly When true, cookie is http/server-side and not visible to JS code in most browsers. * #property bool httponly When true, cookie is http/server-side and not visible to JS code in most browsers.
* *
@@ -1032,6 +1052,7 @@ $config->cookieOptions = array(
'path' => null, // Cookie path/URL or null for PW installations root URL (default=null). 'path' => null, // Cookie path/URL or null for PW installations root URL (default=null).
'domain' => null, // Cookie domain: null for current hostname, true for all subdomains of current domain, domain.com for domain and all subdomains, www.domain.com for www subdomain. 'domain' => null, // Cookie domain: null for current hostname, true for all subdomains of current domain, domain.com for domain and all subdomains, www.domain.com for www subdomain.
'secure' => null, // Transmit cookies only over secure HTTPS connection? (true, false, or null to auto-detect, substituting true for cookies set when HTTPS is active). 'secure' => null, // Transmit cookies only over secure HTTPS connection? (true, false, or null to auto-detect, substituting true for cookies set when HTTPS is active).
'samesite' => 'Lax', // When set to “Lax” cookies are preserved on GET requests to this site originated from external links. May also be 'Strict' or 'None' ('secure' option required for 'None').
'httponly' => false, // When true, cookie is http/server-side only and not visible to client-side JS code. 'httponly' => false, // When true, cookie is http/server-side only and not visible to client-side JS code.
'fallback' => true, // If set cookie fails (perhaps due to output already sent), attempt to set at beginning of next request? (default=true) 'fallback' => true, // If set cookie fails (perhaps due to output already sent), attempt to set at beginning of next request? (default=true)
); );

View File

@@ -62,6 +62,7 @@
* @property string $sessionNameSecure Session name when on HTTPS. Used when the sessionCookieSecure option is enabled (default). When blank (default), it will assume sessionName + 's'. #pw-group-session * @property string $sessionNameSecure Session name when on HTTPS. Used when the sessionCookieSecure option is enabled (default). When blank (default), it will assume sessionName + 's'. #pw-group-session
* @property bool|int $sessionCookieSecure Use secure cookies when on HTTPS? When enabled, separate sessions will be maintained for HTTP vs. HTTPS. Good for security but tradeoff is login session may be lost when switching (default=1 or true). #pw-group-session * @property bool|int $sessionCookieSecure Use secure cookies when on HTTPS? When enabled, separate sessions will be maintained for HTTP vs. HTTPS. Good for security but tradeoff is login session may be lost when switching (default=1 or true). #pw-group-session
* @property null|string $sessionCookieDomain Domain to use for sessions, which enables a session to work across subdomains, or NULL to disable (default/recommended). #pw-group-session * @property null|string $sessionCookieDomain Domain to use for sessions, which enables a session to work across subdomains, or NULL to disable (default/recommended). #pw-group-session
* @property string $sessionCookieSameSite Cookie “SameSite” value for sessions - “Lax” (default) or “Strict”. (3.0.178+) #pw-group-session
* @property bool|callable $sessionAllow Are sessions allowed? Typically boolean true, unless provided a callable function that returns boolean. See /wire/config.php for an example. #pw-group-session * @property bool|callable $sessionAllow Are sessions allowed? Typically boolean true, unless provided a callable function that returns boolean. See /wire/config.php for an example. #pw-group-session
* @property int $sessionExpireSeconds How many seconds of inactivity before session expires? #pw-group-session * @property int $sessionExpireSeconds How many seconds of inactivity before session expires? #pw-group-session
* @property bool $sessionChallenge Should login sessions have a challenge key? (for extra security, recommended) #pw-group-session * @property bool $sessionChallenge Should login sessions have a challenge key? (for extra security, recommended) #pw-group-session

View File

@@ -305,7 +305,18 @@ class Session extends Wire implements \IteratorAggregate {
} }
} }
@session_start(); $options = array();
$cookieSameSite = $this->sessionCookieSameSite();
if(PHP_VERSION_ID < 70300) {
$cookiePath = ini_get('session.cookie_path');
if(empty($cookiePath)) $cookiePath = '/';
$options['cookie_path'] = "$cookiePath; SameSite=$cookieSameSite";
} else {
$options['cookie_samesite'] = $cookieSameSite;
}
@session_start($options);
if(!empty($this->data)) { if(!empty($this->data)) {
foreach($this->data as $key => $value) $this->set($key, $value); foreach($this->data as $key => $value) $this->set($key, $value);
@@ -937,8 +948,16 @@ class Session extends Wire implements \IteratorAggregate {
$this->set('_user', 'challenge', $challenge); $this->set('_user', 'challenge', $challenge);
$secure = $this->config->sessionCookieSecure ? (bool) $this->config->https : false; $secure = $this->config->sessionCookieSecure ? (bool) $this->config->https : false;
// set challenge cookie to last 30 days (should be longer than any session would feasibly last) // set challenge cookie to last 30 days (should be longer than any session would feasibly last)
setcookie(session_name() . self::challengeSuffix, $challenge, time()+60*60*24*30, '/', $this->setCookie(
$this->config->sessionCookieDomain, $secure, true); session_name() . self::challengeSuffix,
$challenge,
time() + 60*60*24*30,
'/',
$this->config->sessionCookieDomain,
$secure,
true,
$this->config->sessionCookieSameSite
);
} }
if($this->config->sessionFingerprint) { if($this->config->sessionFingerprint) {
@@ -947,7 +966,7 @@ class Session extends Wire implements \IteratorAggregate {
} }
$this->wire('user', $user); $this->wire('user', $user);
$this->get('CSRF')->resetAll(); $this->CSRF()->resetAll();
$this->loginSuccess($user); $this->loginSuccess($user);
$fail = false; $fail = false;
@@ -1119,22 +1138,81 @@ class Session extends Wire implements \IteratorAggregate {
return $this; return $this;
} }
/**
* Add a SetCookie response header
*
* @param string $name
* @param string|null|false $value
* @param int $expires
* @param string $path
* @param string|null $domain
* @param bool $secure
* @param bool $httponly
* @param string $samesite One of 'Strict', 'Lax', 'None'
* @return bool
* @since 3.0.178
*
*/
protected function setCookie($name, $value, $expires = 0, $path = '/', $domain = null, $secure = false, $httponly = false, $samesite = 'Lax') {
if(empty($path)) $path = '/';
$samesite = $this->sessionCookieSameSite($samesite);
if($samesite === 'None') $secure = true;
if(PHP_VERSION_ID < 70300) {
return setcookie($name, $value, $expires, "$path; SameSite=$samesite", $domain, $secure, $httponly);
}
// PHP 7.3+ supports $options array
return setcookie($name, $value, array(
'expires' => $expires,
'path' => $path,
'domain' => $domain,
'secure' => $secure,
'httponly' => $httponly,
'samesite' => $samesite,
));
}
/** /**
* Remove all cookies used by the session * Remove all cookies used by the session
* *
*/ */
protected function removeCookies() { protected function removeCookies() {
$sessionName = session_name(); $sessionName = session_name();
$challengeName = $sessionName . self::challengeSuffix;
$time = time() - 42000; $time = time() - 42000;
$domain = $this->config->sessionCookieDomain;
$secure = $this->config->sessionCookieSecure ? (bool) $this->config->https : false; $secure = $this->config->sessionCookieSecure ? (bool) $this->config->https : false;
$samesite = $this->sessionCookieSameSite();
if(isset($_COOKIE[$sessionName])) { if(isset($_COOKIE[$sessionName])) {
setcookie($sessionName, '', $time, '/', $this->config->sessionCookieDomain, $secure, true); $this->setCookie($sessionName, '', $time, '/', $domain, $secure, true, $samesite);
} }
if(isset($_COOKIE[$sessionName . self::challengeSuffix])) {
setcookie($sessionName . self::challengeSuffix, '', $time, '/', $this->config->sessionCookieDomain, $secure, true); if(isset($_COOKIE[$challengeName])) {
$this->setCookie($challengeName, '', $time, '/', $domain, $secure, true, $samesite);
} }
} }
/**
* Get 'SameSite' value for session cookie
*
* @param string|null $value
* @return string
* @since 3.0.178
*
*/
protected function sessionCookieSameSite($value = null) {
$samesite = $value === null ? $this->config->sessionCookieSameSite : $value;
$samesite = empty($samesite) ? 'Lax' : ucfirst(strtolower($samesite));
if(!in_array($samesite, array('Strict', 'Lax', 'None'), true)) $samesite = 'Lax';
return $samesite;
}
/** /**
* Get the names of all cookies managed by Session * Get the names of all cookies managed by Session
* *

View File

@@ -21,7 +21,7 @@
* @property array|string[] $urlSegments Retrieve all URL segments (array). This requires url segments are enabled on the template of the requested page. You can turn it on or off under the url tab when editing a template. #pw-group-URL-segments * @property array|string[] $urlSegments Retrieve all URL segments (array). This requires url segments are enabled on the template of the requested page. You can turn it on or off under the url tab when editing a template. #pw-group-URL-segments
* @property WireInputData $post POST variables * @property WireInputData $post POST variables
* @property WireInputData $get GET variables * @property WireInputData $get GET variables
* @property WireInputData $cookie COOKIE variables * @property WireInputDataCookie $cookie COOKIE variables
* @property WireInputData $whitelist Whitelisted variables * @property WireInputData $whitelist Whitelisted variables
* @property int $pageNum Current page number (where 1 is first) #pw-group-URLs * @property int $pageNum Current page number (where 1 is first) #pw-group-URLs
* @property string $urlSegmentsStr String of current URL segments, separated by slashes, i.e. a/b/c #pw-internal * @property string $urlSegmentsStr String of current URL segments, separated by slashes, i.e. a/b/c #pw-internal

View File

@@ -56,6 +56,11 @@
* // Specify true, false, or null to auto-detect (uses true for cookies set when HTTPS). * // Specify true, false, or null to auto-detect (uses true for cookies set when HTTPS).
* 'secure' => null, * 'secure' => null,
* *
* // Cookie SameSite value: When set to “Lax” cookies are preserved on GET requests to this site
* // originated from external links. May also be “Strict” or “None”. The 'secure' option is
* // required for “None”. Default value is “Lax”. Available in PW 3.0.178+.
* 'samesite' => 'Lax',
*
* // Make cookies accessible by HTTP (ProcessWire/PHP) only? * // Make cookies accessible by HTTP (ProcessWire/PHP) only?
* // When true, cookie is http/server-side only and not visible to client-side JS code. * // When true, cookie is http/server-side only and not visible to client-side JS code.
* 'httponly' => false, * 'httponly' => false,
@@ -95,6 +100,7 @@ class WireInputDataCookie extends WireInputData {
'domain' => null, 'domain' => null,
'secure' => null, 'secure' => null,
'httponly' => false, 'httponly' => false,
'samesite' => 'Lax',
'fallback' => true, 'fallback' => true,
); );
@@ -340,6 +346,7 @@ class WireInputDataCookie extends WireInputData {
* - `path` (string|null): Cookie path/URL or null for PW installations root URL. (default=null) * - `path` (string|null): Cookie path/URL or null for PW installations root URL. (default=null)
* - `secure` (bool|null): Transmit cookies only over secure HTTPS connection? Specify true or false, or use null to auto-detect, * - `secure` (bool|null): Transmit cookies only over secure HTTPS connection? Specify true or false, or use null to auto-detect,
* which uses true for cookies set when HTTPS is detected. (default=null) * which uses true for cookies set when HTTPS is detected. (default=null)
* - `samesite` (string): SameSite value, one of 'Lax' (default), 'Strict' or 'None'. (default='Lax') 3.0.178+
* - `httponly` (bool): When true, cookie is visible to PHP/ProcessWire only and not visible to client-side JS code. (default=false) * - `httponly` (bool): When true, cookie is visible to PHP/ProcessWire only and not visible to client-side JS code. (default=false)
* - `fallback` (bool): If set cookie fails (perhaps due to output already sent), attempt to set at beginning of next request? (default=true) * - `fallback` (bool): If set cookie fails (perhaps due to output already sent), attempt to set at beginning of next request? (default=true)
* - `domain` (string|bool|null): Cookie domain, specify one of the following: `null` or blank string for current hostname [default], * - `domain` (string|bool|null): Cookie domain, specify one of the following: `null` or blank string for current hostname [default],
@@ -351,8 +358,7 @@ class WireInputDataCookie extends WireInputData {
*/ */
public function setCookie($key, $value, array $options) { public function setCookie($key, $value, array $options) {
/** @var Config $config */ $config = $this->wire()->config;
$config = $this->wire('config');
$options = array_merge($this->defaultOptions, $config->cookieOptions, $this->options, $options); $options = array_merge($this->defaultOptions, $config->cookieOptions, $this->options, $options);
$path = $options['path'] === null || $options['path'] === true ? $config->urls->root : $options['path']; $path = $options['path'] === null || $options['path'] === true ? $config->urls->root : $options['path'];
@@ -361,6 +367,13 @@ class WireInputDataCookie extends WireInputData {
$domain = $options['domain']; $domain = $options['domain'];
$remove = $value === null; $remove = $value === null;
$expires = null; $expires = null;
$samesite = $options['samesite'] ? ucfirst(strtolower($options['samesite'])) : 'Lax';
if($samesite === 'None') {
$secure = true;
} else if(!in_array($samesite, array('Lax', 'Strict', 'None'), true)) {
$samesite = 'Lax';
}
if(!empty($options['expire'])) { if(!empty($options['expire'])) {
if(is_string($options['expire']) && !ctype_digit($options['expire'])) { if(is_string($options['expire']) && !ctype_digit($options['expire'])) {
@@ -395,11 +408,22 @@ class WireInputDataCookie extends WireInputData {
if($remove) list($value, $expires) = array('', 1); if($remove) list($value, $expires) = array('', 1);
// set the cookie // set the cookie
$result = setcookie($key, $value, $expires, $path, $domain, $secure, $httponly); if(PHP_VERSION_ID < 70300) {
$result = setcookie($key, $value, $expires, "$path; SameSite=$samesite", $domain, $secure, $httponly);
} else {
$result = setcookie($key, $value, array(
'expires' => $expires,
'path' => $path,
'domain' => $domain,
'secure' => $secure,
'httponly' => $httponly,
'samesite' => $samesite,
));
}
if($result === false && $options['fallback']) { if($result === false && $options['fallback']) {
// output must have already started, set at construct on next request // output must have already started, set at construct on next request
$this->wire('session')->setFor($this, $key, $value); $this->wire()->session->setFor($this, $key, $value);
} }
if($remove) { if($remove) {

View File

@@ -179,7 +179,24 @@ class SessionHandlerDB extends WireSessionHandler implements Module, Configurabl
$query = $database->prepare("DELETE FROM `$table` WHERE id=:id"); $query = $database->prepare("DELETE FROM `$table` WHERE id=:id");
$query->execute(array(":id" => $id)); $query->execute(array(":id" => $id));
$secure = $config->sessionCookieSecure ? (bool) $config->https : false; $secure = $config->sessionCookieSecure ? (bool) $config->https : false;
setcookie(session_name(), '', time()-42000, '/', $config->sessionCookieDomain, $secure, true); $expires = time() - 42000;
$samesite = $config->sessionCookieSameSite ? ucfirst(strtolower($config->sessionCookieSameSite)) : 'Lax';
if($samesite === 'None') $secure = true;
if(PHP_VERSION_ID < 70300) {
setcookie(session_name(), '', $expires, "/; SameSite=$samesite", $config->sessionCookieDomain, $secure, true);
} else {
setcookie(session_name(), '', array(
'expires' => $expires,
'path' => '/',
'domain' => $config->sessionCookieDomain,
'secure' => $secure,
'httponly' => true,
'samesite' => $samesite
));
}
return true; return true;
} }