Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/cookie same site #420

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions application/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@
*/
// Cookie::$secure = isset($_SERVER['HTTPS']) AND $_SERVER['HTTPS'] == 'on' ? TRUE : FALSE;

/**
* SameSite attribute of Set-Cookie HTTP response header allows you to declare if your
* cookie should be restricted to first-party or same-site context
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
*/
// Cookie::$samesite = Kohana_Cookie_Samesite::STRICT;

/**
* Set the routes. Each route must have a minimum of a name, a URI and a set of
* defaults for the URI.
Expand Down
88 changes: 53 additions & 35 deletions system/classes/Kohana/Cookie.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,24 @@ class Kohana_Cookie {
*/
public static $httponly = FALSE;

/**
* Gets the value of a signed cookie. Cookies without signatures will not
* be returned. If the cookie signature is present, but invalid, the cookie
* will be deleted.
*
* // Get the "theme" cookie, or use "blue" if the cookie does not exist
* $theme = Cookie::get('theme', 'blue');
*
* @param string $key cookie name
* @param mixed $default default value to return
* @return string
*/
/**
* @var string Transmit cookies with third party requests
*/
public static $samesite = Kohana_Cookie_Samesite::LAX;

/**
* Gets the value of a signed cookie. Cookies without signatures will not
* be returned. If the cookie signature is present, but invalid, the cookie
* will be deleted.
*
* // Get the "theme" cookie, or use "blue" if the cookie does not exist
* $theme = Cookie::get('theme', 'blue');
*
* @param string $key cookie name
* @param mixed $default default value to return
* @return string
* @throws Kohana_Exception
*/
public static function get($key, $default = NULL)
{
if ( ! isset($_COOKIE[$key]))
Expand Down Expand Up @@ -84,24 +90,25 @@ public static function get($key, $default = NULL)
return $default;
}

/**
* Sets a signed cookie. Note that all cookie values must be strings and no
* automatic serialization will be performed!
*
* [!!] By default, Cookie::$expiration is 0 - if you skip/pass NULL for the optional
* lifetime argument your cookies will expire immediately unless you have separately
* configured Cookie::$expiration.
*
*
* // Set the "theme" cookie
* Cookie::set('theme', 'red');
*
* @param string $name name of cookie
* @param string $value value of cookie
* @param integer $lifetime lifetime in seconds
* @return boolean
* @uses Cookie::salt
*/
/**
* Sets a signed cookie. Note that all cookie values must be strings and no
* automatic serialization will be performed!
*
* [!!] By default, Cookie::$expiration is 0 - if you skip/pass NULL for the optional
* lifetime argument your cookies will expire immediately unless you have separately
* configured Cookie::$expiration.
*
*
* // Set the "theme" cookie
* Cookie::set('theme', 'red');
*
* @param string $name name of cookie
* @param string $value value of cookie
* @param integer $lifetime lifetime in seconds
* @return boolean
* @throws Kohana_Exception
* @uses Cookie::salt
*/
public static function set($name, $value, $lifetime = NULL)
{
if ($lifetime === NULL)
Expand All @@ -119,7 +126,7 @@ public static function set($name, $value, $lifetime = NULL)
// Add the salt to the cookie value
$value = Cookie::salt($name, $value).'~'.$value;

return static::_setcookie($name, $value, $lifetime, Cookie::$path, Cookie::$domain, Cookie::$secure, Cookie::$httponly);
return static::_setcookie($name, $value, $lifetime, Cookie::$path, Cookie::$domain, Cookie::$secure, Cookie::$httponly, Cookie::$samesite);
}

/**
Expand All @@ -136,7 +143,7 @@ public static function delete($name)
unset($_COOKIE[$name]);

// Nullify the cookie and make it expire
return static::_setcookie($name, NULL, -86400, Cookie::$path, Cookie::$domain, Cookie::$secure, Cookie::$httponly);
return static::_setcookie($name, NULL, -86400, Cookie::$path, Cookie::$domain, Cookie::$secure, Cookie::$httponly, Cookie::$samesite);
}

/**
Expand Down Expand Up @@ -170,7 +177,7 @@ public static function salt($name, $value)
*
* @param string $name
* @param string $value
* @param integer $expire
* @param integer $expires
* @param string $path
* @param string $domain
* @param boolean $secure
Expand All @@ -179,9 +186,20 @@ public static function salt($name, $value)
* @return bool
* @see setcookie
*/
protected static function _setcookie($name, $value, $expire, $path, $domain, $secure, $httponly)
protected static function _setcookie($name, $value, $expires, $path, $domain, $secure, $httponly, $samesite)
{
return setcookie($name, $value, $expire, $path, $domain, $secure, $httponly);
return setcookie(
$name,
$value,
[
Kohana_Cookie_Properties::EXPIRES => $expires,
Kohana_Cookie_Properties::PATH => $path,
Kohana_Cookie_Properties::DOMAIN => $domain,
Kohana_Cookie_Properties::SECURE => $secure,
Kohana_Cookie_Properties::HTTP_ONLY => $httponly,
Kohana_Cookie_Properties::SAME_SITE => $samesite,
]
);
}

/**
Expand Down
70 changes: 70 additions & 0 deletions system/classes/Kohana/Cookie/Properties.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

/**
* Class Properties
*
* @package Kohana
* @category Cookie
* @author Kohana Team
* @copyright (c) Kohana Team
* @license https://koseven.ga/LICENSE.md
*/
final class Kohana_Cookie_Properties
{
/**
* Name of the cookie
*/
const NAME = 'name';

/**
* Value of the cookie
*/
const VALUE = 'value';

/**
* The time the cookie expires. This is a Unix timestamp so is in number of seconds since the epoch.
* In other words, you'll most likely set this with the time() function plus the number of seconds before
* you want it to expire. Or you might use mktime(). time()+60*60*24*30 will set the cookie to expire in 30 days.
* If set to 0, or omitted, the cookie will expire at the end of the session (when the browser closes).
*/
const EXPIRES = 'expires';

/**
* The path on the server in which the cookie will be available on.
* If set to '/', the cookie will be available within the entire domain. If set to '/foo/',
* the cookie will only be available within the /foo/ directory and all sub-directories such as /foo/bar/ of
* domain. The default value is the current directory that the cookie is being set in.
*/
const PATH = 'path';

/**
* The (sub)domain that the cookie is available to. Setting this to a subdomain (such as 'www.example.com')
* will make the cookie available to that subdomain and all other sub-domains of it (i.e. w2.www.example.com).
* To make the cookie available to the whole domain (including all subdomains of it), simply set the value to
* the domain name ('example.com', in this case).
*/
const DOMAIN = 'domain';

/**
* Indicates that the cookie should only be transmitted over a secure HTTPS connection from the client.
* When set to TRUE, the cookie will only be set if a secure connection exists. On the server-side,
* it's on the programmer to send this kind of cookie only on secure connection
* (e.g. with respect to $_SERVER["HTTPS"]).
*/
const SECURE = 'secure';

/**
* When TRUE the cookie will be made accessible only through the HTTP protocol.
* This means that the cookie won't be accessible by scripting languages, such as JavaScript.
* It has been suggested that this setting can effectively help to reduce identity theft through XSS attacks
* (although it is not supported by all browsers), but that claim is often disputed.
* Added in PHP 5.2.0. TRUE or FALSE
*/
const HTTP_ONLY = 'httponly';

/**
* The SameSite attribute of the Set-Cookie HTTP response header allows you to declare if your cookie
* should be restricted to a first-party or same-site context.
*/
const SAME_SITE = 'samesite';
}
33 changes: 33 additions & 0 deletions system/classes/Kohana/Cookie/Samesite.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/**
* Class Samesite
*
* @package Kohana
* @category Cookie
* @author Kohana Team
* @copyright (c) Kohana Team
* @license https://koseven.ga/LICENSE.md
*/
final class Kohana_Cookie_Samesite
{
/**
* Cookies are allowed to be sent with top-level navigations and will be sent along with GET request
* initiated by third party website. This is the default value in modern browsers.
*/
const LAX = 'Lax';

/**
* Cookies will only be sent in a first-party context and not be sent along with requests initiated
* by third party websites.
*/
const STRICT = 'Strict';

/**
* None used to be the default value, but recent browser versions made Lax the default value to have reasonably
* robust defense against some classes of cross-site request forgery (CSRF) attacks.
*
* None requires the Secure attribute in latest browser versions.
*/
const NONE = 'None';
}
93 changes: 50 additions & 43 deletions system/tests/kohana/CookieTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,17 @@ public function test_set_creates_cookie_with_configured_cookie_options()
'Cookie::$domain' => 'my.domain',
'Cookie::$secure' => TRUE,
'Cookie::$httponly' => FALSE,
'Cookie::$samesite' => Kohana_Cookie_Samesite::LAX
]);

Kohana_CookieTest_TestableCookie::set('cookie', 'value');

$this->assertSetCookieWith([
'path' => '/path',
'domain' => 'my.domain',
'secure' => TRUE,
'httponly' => FALSE
Kohana_Cookie_Properties::PATH => '/path',
Kohana_Cookie_Properties::DOMAIN => 'my.domain',
Kohana_Cookie_Properties::SECURE => TRUE,
Kohana_Cookie_Properties::HTTP_ONLY => FALSE,
Kohana_Cookie_Properties::SAME_SITE => Kohana_Cookie_Samesite::LAX
]);
}

Expand All @@ -74,18 +76,19 @@ public function provider_set_calculates_expiry_from_lifetime()
];
}

/**
* @param int $expiration
* @param int $expect_expiry
*
* @dataProvider provider_set_calculates_expiry_from_lifetime
* @covers Cookie::set
*/
/**
* @param int $expiration
* @param int $expect_expiry
*
* @dataProvider provider_set_calculates_expiry_from_lifetime
* @covers Cookie::set
* @throws Kohana_Exception
*/
public function test_set_calculates_expiry_from_lifetime($expiration, $expect_expiry)
{
$this->setEnvironment(['Cookie::$expiration' => self::COOKIE_EXPIRATION]);
Kohana_CookieTest_TestableCookie::set('foo', 'bar', $expiration);
$this->assertSetCookieWith(['expire' => $expect_expiry]);
$this->assertSetCookieWith([Kohana_Cookie_Properties::EXPIRES => $expect_expiry]);
}

/**
Expand Down Expand Up @@ -121,15 +124,16 @@ public function provider_get_returns_default_without_deleting_if_cookie_unsigned
];
}

/**
* Verifies that unsigned cookies are not available to the kohana application, but are not affected for other
* consumers.
*
* @param string $unsigned_value
*
* @dataProvider provider_get_returns_default_without_deleting_if_cookie_unsigned
* @covers Cookie::get
*/
/**
* Verifies that unsigned cookies are not available to the kohana application, but are not affected for other
* consumers.
*
* @param string $unsigned_value
*
* @dataProvider provider_get_returns_default_without_deleting_if_cookie_unsigned
* @covers Cookie::get
* @throws Kohana_Exception
*/
public function test_get_returns_default_without_deleting_if_cookie_unsigned($unsigned_value)
{
$_COOKIE['cookie'] = $unsigned_value;
Expand Down Expand Up @@ -208,13 +212,14 @@ public function provider_salt_creates_different_hash_for_different_data()
];
}

/**
* @param array $first_args
* @param array $changed_args
*
* @dataProvider provider_salt_creates_different_hash_for_different_data
* @covers Cookie::salt
*/
/**
* @param array $first_args
* @param array $changed_args
*
* @dataProvider provider_salt_creates_different_hash_for_different_data
* @covers Cookie::salt
* @throws Kohana_Exception
*/
public function test_salt_creates_different_hash_for_different_data($first_args, $changed_args)
{
$second_args = array_merge($first_args, $changed_args);
Expand Down Expand Up @@ -243,13 +248,14 @@ protected function assertDeletedCookie($name)
$this->assertArrayNotHasKey($name, $_COOKIE);
// To delete the client-side cookie, Cookie::delete should send a new cookie with value NULL and expiry in the past
$this->assertSetCookieWith([
'name' => $name,
'value' => NULL,
'expire' => -86400,
'path' => Cookie::$path,
'domain' => Cookie::$domain,
'secure' => Cookie::$secure,
'httponly' => Cookie::$httponly
Kohana_Cookie_Properties::NAME => $name,
Kohana_Cookie_Properties::VALUE => NULL,
Kohana_Cookie_Properties::EXPIRES => -86400,
Kohana_Cookie_Properties::PATH => Cookie::$path,
Kohana_Cookie_Properties::DOMAIN => Cookie::$domain,
Kohana_Cookie_Properties::SECURE => Cookie::$secure,
Kohana_Cookie_Properties::HTTP_ONLY => Cookie::$httponly,
Kohana_Cookie_Properties::SAME_SITE => Cookie::$samesite,
]);
}

Expand Down Expand Up @@ -299,16 +305,17 @@ class Kohana_CookieTest_TestableCookie extends Cookie {
/**
* {@inheritdoc}
*/
protected static function _setcookie($name, $value, $expire, $path, $domain, $secure, $httponly)
protected static function _setcookie($name, $value, $expires, $path, $domain, $secure, $httponly, $samesite)
{
self::$_mock_cookies_set[] = [
'name' => $name,
'value' => $value,
'expire' => $expire,
'path' => $path,
'domain' => $domain,
'secure' => $secure,
'httponly' => $httponly
Kohana_Cookie_Properties::NAME => $name,
Kohana_Cookie_Properties::VALUE => $value,
Kohana_Cookie_Properties::EXPIRES => $expires,
Kohana_Cookie_Properties::PATH => $path,
Kohana_Cookie_Properties::DOMAIN => $domain,
Kohana_Cookie_Properties::SECURE => $secure,
Kohana_Cookie_Properties::HTTP_ONLY => $httponly,
Kohana_Cookie_Properties::SAME_SITE => $samesite
];

return TRUE;
Expand Down