Move and improve cookie handling so that it might catch #537

This commit is contained in:
the-djmaze 2023-01-31 10:27:23 +01:00
parent fa3d4727f9
commit 977dc1e5e4
10 changed files with 180 additions and 183 deletions

View file

@ -748,9 +748,9 @@ class Actions
$aResult['ContactsSync'] = $aData;
}
$sToken = Utils::GetCookie(self::AUTH_MAILTO_TOKEN_KEY);
$sToken = \SnappyMail\Cookies::get(self::AUTH_MAILTO_TOKEN_KEY);
if (null !== $sToken) {
Utils::ClearCookie(self::AUTH_MAILTO_TOKEN_KEY);
\SnappyMail\Cookies::clear(self::AUTH_MAILTO_TOKEN_KEY);
$mMailToData = Utils::DecodeKeyValuesQ($sToken);
if (!empty($mMailToData['MailTo']) && 'MailTo' === $mMailToData['MailTo'] && !empty($mMailToData['To'])) {

View file

@ -181,7 +181,7 @@ trait Accounts
$bReload = false;
$oAccount = $this->getAccountFromToken();
if ($oAccount instanceof AdditionalAccount && $oAccount->Email() === $sEmailToDelete) {
Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY);
\SnappyMail\Cookies::clear(self::AUTH_ADDITIONAL_TOKEN_KEY);
$bReload = true;
}

View file

@ -29,13 +29,13 @@ trait Admin
protected function getAdminAuthKey() : string
{
$cookie = Utils::GetCookie(static::$AUTH_ADMIN_TOKEN_KEY);
$cookie = \SnappyMail\Cookies::get(static::$AUTH_ADMIN_TOKEN_KEY);
if ($cookie) {
$aAdminHash = Utils::DecodeKeyValuesQ($cookie);
if (!empty($aAdminHash[1]) && 'token' === $aAdminHash[0]) {
return $aAdminHash[1];
}
Utils::ClearCookie(static::$AUTH_ADMIN_TOKEN_KEY);
\SnappyMail\Cookies::clear(static::$AUTH_ADMIN_TOKEN_KEY);
}
return '';
}

View file

@ -9,6 +9,7 @@ use RainLoop\Model\MainAccount;
use RainLoop\Model\AdditionalAccount;
use RainLoop\Providers\Storage\Enumerations\StorageType;
use RainLoop\Exceptions\ClientException;
use SnappyMail\Cookies;
trait UserAuth
{
@ -156,12 +157,12 @@ trait UserAuth
private static function SetAccountCookie(string $sName, ?Account $oAccount)
{
if ($oAccount) {
Utils::SetCookie(
Cookies::set(
$sName,
\MailSo\Base\Utils::UrlSafeBase64Encode(\SnappyMail\Crypt::EncryptToJSON($oAccount))
);
} else {
Utils::ClearCookie($sName);
Cookies::clear($sName);
}
}
@ -209,7 +210,7 @@ trait UserAuth
$this->getMainAccountFromToken($bThrowExceptionOnFalse);
if (false === $this->oAdditionalAuthAccount && isset($_COOKIE[self::AUTH_ADDITIONAL_TOKEN_KEY])) {
$aData = Utils::GetSecureCookie(self::AUTH_ADDITIONAL_TOKEN_KEY);
$aData = Cookies::getSecure(self::AUTH_ADDITIONAL_TOKEN_KEY);
if ($aData) {
$this->oAdditionalAuthAccount = AdditionalAccount::NewInstanceFromTokenArray(
$this,
@ -219,7 +220,7 @@ trait UserAuth
}
if (!$this->oAdditionalAuthAccount) {
$this->oAdditionalAuthAccount = null;
Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY);
Cookies::clear(self::AUTH_ADDITIONAL_TOKEN_KEY);
}
}
@ -234,14 +235,14 @@ trait UserAuth
if (false === $this->oMainAuthAccount) try {
$this->oMainAuthAccount = null;
if (isset($_COOKIE[self::AUTH_SPEC_LOGOUT_TOKEN_KEY])) {
Utils::ClearCookie(self::AUTH_SPEC_LOGOUT_TOKEN_KEY);
Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY);
// Utils::ClearCookie(self::AUTH_SPEC_TOKEN_KEY);
// Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY);
Utils::ClearCookie(Utils::SESSION_TOKEN);
Cookies::clear(self::AUTH_SPEC_LOGOUT_TOKEN_KEY);
Cookies::clear(self::AUTH_SIGN_ME_TOKEN_KEY);
// Cookies::clear(self::AUTH_SPEC_TOKEN_KEY);
// Cookies::clear(self::AUTH_ADDITIONAL_TOKEN_KEY);
Cookies::clear(Utils::SESSION_TOKEN);
}
$aData = Utils::GetSecureCookie(self::AUTH_SPEC_TOKEN_KEY);
$aData = Cookies::getSecure(self::AUTH_SPEC_TOKEN_KEY);
if ($aData) {
/**
* Server side control/kickout of logged in sessions
@ -274,7 +275,7 @@ trait UserAuth
}
}
if (!$this->oMainAuthAccount) {
Utils::ClearCookie(Utils::SESSION_TOKEN);
Cookies::clear(Utils::SESSION_TOKEN);
// \MailSo\Base\Http::StatusHeader(401);
$this->Logout(true);
// $sAdditionalMessage = $this->StaticI18N('SESSION_GONE');
@ -310,7 +311,7 @@ trait UserAuth
$this->oAdditionalAuthAccount = false;
$this->oMainAuthAccount = $oAccount;
if (!isset($_COOKIE['smctoken'])) {
\RainLoop\Utils::SetCookie('smctoken', \base64_encode(\random_bytes(16)), 0, false);
Cookies::set('smctoken', \base64_encode(\random_bytes(16)), 0, false);
}
static::SetAccountCookie(self::AUTH_SPEC_TOKEN_KEY, $oAccount);
}
@ -327,7 +328,7 @@ trait UserAuth
private static function GetSignMeToken(): ?array
{
$sSignMeToken = Utils::GetCookie(self::AUTH_SIGN_ME_TOKEN_KEY);
$sSignMeToken = Cookies::get(self::AUTH_SIGN_ME_TOKEN_KEY);
if ($sSignMeToken) {
\SnappyMail\Log::notice(self::AUTH_SIGN_ME_TOKEN_KEY, 'decrypt');
$aResult = \SnappyMail\Crypt::DecryptUrlSafe($sSignMeToken);
@ -336,7 +337,7 @@ trait UserAuth
}
\SnappyMail\Log::notice(self::AUTH_SIGN_ME_TOKEN_KEY, 'invalid');
// Don't clear due to smctoken cookie missing at initialization and login checkbox
// Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY);
// Cookies::clear(self::AUTH_SIGN_ME_TOKEN_KEY);
}
return null;
}
@ -346,7 +347,7 @@ trait UserAuth
$this->ClearSignMeData();
$uuid = \SnappyMail\UUID::generate();
$data = \SnappyMail\Crypt::Encrypt($oAccount);
Utils::SetCookie(
Cookies::set(
self::AUTH_SIGN_ME_TOKEN_KEY,
\SnappyMail\Crypt::EncryptUrlSafe([
'e' => $oAccount->Email(),
@ -405,7 +406,7 @@ trait UserAuth
if ($aTokenData) {
$this->StorageProvider()->Clear($aTokenData['e'], StorageType::SIGN_ME, $aTokenData['u']);
}
Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY);
Cookies::clear(self::AUTH_SIGN_ME_TOKEN_KEY);
}
/**
@ -415,14 +416,14 @@ trait UserAuth
public function SetAuthLogoutToken(): void
{
\header('X-RainLoop-Action: Logout');
Utils::SetCookie(self::AUTH_SPEC_LOGOUT_TOKEN_KEY, \md5($_SERVER['REQUEST_TIME_FLOAT']));
Cookies::set(self::AUTH_SPEC_LOGOUT_TOKEN_KEY, \md5($_SERVER['REQUEST_TIME_FLOAT']));
}
public function GetSpecLogoutCustomMgsWithDeletion(): string
{
$sResult = Utils::GetCookie(self::AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY) ?: '';
$sResult = Cookies::get(self::AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY) ?: '';
if (\strlen($sResult)) {
Utils::ClearCookie(self::AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY);
Cookies::clear(self::AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY);
}
return $sResult;
@ -430,14 +431,14 @@ trait UserAuth
public function SetSpecLogoutCustomMgsWithDeletion(string $sMessage): void
{
Utils::SetCookie(self::AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY, $sMessage);
Cookies::set(self::AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY, $sMessage);
}
public function Logout(bool $bMain) : void
{
// Utils::ClearCookie(Utils::SESSION_TOKEN);
Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY);
$bMain && Utils::ClearCookie(self::AUTH_SPEC_TOKEN_KEY);
// Cookies::clear(Utils::SESSION_TOKEN);
Cookies::clear(self::AUTH_ADDITIONAL_TOKEN_KEY);
$bMain && Cookies::clear(self::AUTH_SPEC_TOKEN_KEY);
// TODO: kill SignMe data to prevent automatic login?
}

View file

@ -133,7 +133,7 @@ class ActionsAdmin extends Actions
if ($sAdminKey) {
$this->Cacher(null, true)->Delete(KeyPathHelper::SessionAdminKey($sAdminKey));
}
Utils::ClearCookie(static::$AUTH_ADMIN_TOKEN_KEY);
\SnappyMail\Cookies::clear(static::$AUTH_ADMIN_TOKEN_KEY);
return $this->TrueResponse();
}
@ -331,7 +331,7 @@ class ActionsAdmin extends Actions
if (!$sToken) {
throw new \RuntimeException('Failed to encode admin token');
}
Utils::SetCookie(static::$AUTH_ADMIN_TOKEN_KEY, $sToken);
\SnappyMail\Cookies::set(static::$AUTH_ADMIN_TOKEN_KEY, $sToken);
return $sToken;
}

View file

@ -5,16 +5,6 @@ namespace RainLoop;
abstract class Api
{
public static function Handle() : bool
{
static $bOne = false;
if (!$bOne) {
static::SetupDefaultConfig();
$bOne = true;
}
return $bOne;
}
public static function Actions() : Actions
{
static $oActions = null;
@ -40,6 +30,7 @@ abstract class Api
// \ini_set('display_errors', '1');
\ini_set('log_errors', '1');
}
\MailSo\Config::$BoundaryPrefix = \trim($oConfig->Get('labs', 'boundary_prefix', ''));
}
return $oConfig;
}
@ -82,17 +73,6 @@ abstract class Api
return $oLogger;
}
protected static function SetupDefaultConfig() : void
{
\MailSo\Config::$BoundaryPrefix = \trim(static::Config()->Get('labs', 'boundary_prefix', ''));
Utils::$CookieDefaultPath = static::Config()->Get('labs', 'cookie_default_path', '');
Utils::$CookieSameSite = static::Config()->Get('security', 'cookie_samesite', 'Strict');
Utils::$CookieSecure = isset($_SERVER['HTTPS'])
|| 'None' == Utils::$CookieSameSite
|| !!static::Config()->Get('labs', 'cookie_default_secure', false);
}
public static function Version() : string
{
return APP_VERSION;

View file

@ -545,7 +545,7 @@ class ServiceActions
$this->oHttp->ServerNoCache();
$sTo = \trim($_GET['to'] ?? '');
if (!empty($sTo) && \preg_match('/^mailto:/i', $sTo)) {
Utils::SetCookie(
\SnappyMail\Cookies::set(
Actions::AUTH_MAILTO_TOKEN_KEY,
Utils::EncodeKeyValuesQ(array(
'Time' => \microtime(true),

View file

@ -4,12 +4,6 @@ namespace RainLoop;
class Utils
{
static $CookieDefaultPath = '';
static $CookieSecure = null;
static $CookieSameSite = 'Strict';
const
/**
* 30 days cookie
@ -60,14 +54,14 @@ class Utils
public static function GetSessionToken(bool $generate = true) : ?string
{
$sToken = static::GetCookie(self::SESSION_TOKEN);
$sToken = \SnappyMail\Cookies::get(self::SESSION_TOKEN);
if (!$sToken) {
if (!$generate) {
return null;
}
\SnappyMail\Log::debug('TOKENS', 'New SESSION_TOKEN');
$sToken = \MailSo\Base\Utils::Sha1Rand(APP_SALT);
static::SetCookie(self::SESSION_TOKEN, $sToken);
\SnappyMail\Cookies::set(self::SESSION_TOKEN, $sToken);
}
return \sha1('Session'.APP_SALT.$sToken.'Token'.APP_SALT);
}
@ -79,10 +73,10 @@ class Utils
if ($oAccount) {
return $oAccount->Hash();
}
$sToken = static::GetCookie(self::CONNECTION_TOKEN);
$sToken = \SnappyMail\Cookies::get(self::CONNECTION_TOKEN);
if (!$sToken) {
$sToken = \MailSo\Base\Utils::Sha1Rand(APP_SALT);
static::SetCookie(self::CONNECTION_TOKEN, $sToken, \time() + 3600 * 24 * 30);
\SnappyMail\Cookies::set(self::CONNECTION_TOKEN, $sToken, \time() + 3600 * 24 * 30);
}
return \sha1('Connection'.APP_SALT.$sToken.'Token'.APP_SALT);
}
@ -94,9 +88,9 @@ class Utils
public static function UpdateConnectionToken() : void
{
$sToken = static::GetCookie(self::CONNECTION_TOKEN);
$sToken = \SnappyMail\Cookies::get(self::CONNECTION_TOKEN);
if ($sToken) {
static::SetCookie(self::CONNECTION_TOKEN, $sToken, \time() + 3600 * 24 * 30);
\SnappyMail\Cookies::set(self::CONNECTION_TOKEN, $sToken, \time() + 3600 * 24 * 30);
}
}
@ -110,118 +104,6 @@ class Utils
));
}
/**
* @param mixed $mDefault = null
*/
public static function GetCookie(string $sName) : ?string
{
if (isset($_COOKIE[$sName])) {
$aParts = [];
foreach (\array_keys($_COOKIE) as $sCookieName) {
if (\strtok($sCookieName, '~') === $sName) {
$aParts[$sCookieName] = $_COOKIE[$sCookieName];
}
}
\ksort($aParts);
return \implode('', $aParts);
}
return null;
}
public static function GetSecureCookie(string $sName)
{
return isset($_COOKIE[$sName])
? \SnappyMail\Crypt::DecryptFromJSON(\MailSo\Base\Utils::UrlSafeBase64Decode(static::GetCookie($sName)))
: null;
}
private static function _SetCookie(string $sName, string $sValue, int $iExpire, bool $httponly = true)
{
$sPath = static::$CookieDefaultPath;
$sPath = $sPath && \strlen($sPath) ? $sPath : '/';
/*
if (\strlen($sValue) > 4000 - \strlen($sPath . $sName)) {
throw new \Exception("Cookie '{$sName}' value too long");
}
*/
if (\strlen($sValue)) {
$_COOKIE[$sName] = $sValue;
} else {
if (!isset($_COOKIE[$sName])) {
return;
}
unset($_COOKIE[$sName]);
$iExpire = \time() - 3600 * 24 * 30;
}
// Cookie "$sName" has been rejected because it is already expired.
// Happens when \setcookie() sends multiple with the same name (and one is deleted)
// So when previously set, we must delete all 'Set-Cookie' headers and start over
$cookies = [];
$cookie_remove = false;
foreach (\headers_list() as $header) {
if (\preg_match("/Set-Cookie:([^=]+)=/i", $header, $match)) {
if (\trim($match[1]) == $sName) {
$cookie_remove = true;
} else {
$cookies[] = $header;
}
}
}
if ($cookie_remove) {
\header_remove('Set-Cookie');
foreach ($cookies as $cookie) {
\header($cookie);
}
}
\setcookie($sName, $sValue, array(
'expires' => $iExpire,
'path' => $sPath,
// 'domain' => null,
'secure' => static::$CookieSecure,
'httponly' => $httponly,
'samesite' => static::$CookieSameSite
));
}
/**
* Firefox: Cookie "$sName" has been rejected because it is already expired.
* \header_remove("set-cookie: {$sName}");
*/
public static function SetCookie(string $sName, string $sValue, int $iExpire = 0, bool $httponly = true)
{
$sPath = static::$CookieDefaultPath;
$sPath = $sPath && \strlen($sPath) ? $sPath : '/';
// https://github.com/the-djmaze/snappymail/issues/451
// The 4K browser limit is for the entire cookie, including name, value, expiry date etc.
$iMaxSize = 4000 - \strlen($sPath . $sName);
/*
if ($iMaxSize < \strlen($sValue)) {
throw new \Exception("Cookie '{$sName}' value too long");
}
*/
// Set the new 4K split cookie
foreach (\str_split($sValue, $iMaxSize) as $i => $sPart) {
$sCookieName = $i ? "{$sName}~{$i}" : $sName;
\SnappyMail\Log::debug('COOKIE', "set {$sCookieName}");
static::_SetCookie($sCookieName, $sPart, $iExpire);
}
// Delete unused old 4K split cookie parts
foreach (\array_keys($_COOKIE) as $sCookieName) {
$aSplit = \explode('~', $sCookieName);
if (isset($aSplit[1]) && $aSplit[0] == $sName && $aSplit[1] > $i) {
\SnappyMail\Log::debug('COOKIE', "unset {$sCookieName}");
static::_SetCookie($sCookieName, '', 0);
}
}
}
public static function ClearCookie(string $sName)
{
static::_SetCookie($sName, '', 0);
}
public static function UrlEncode(string $sV, bool $bEncode = false) : string
{
return $bEncode ? \urlencode($sV) : $sV;

View file

@ -0,0 +1,138 @@
<?php
namespace SnappyMail;
class Cookies
{
static $DefaultPath = '';
static $Secure = null;
static $SameSite = 'Strict';
private static function init() : bool
{
static $bOne = false;
if (!$bOne) {
$oConfig = \RainLoop\Api::Config();
static::$DefaultPath = $oConfig->Get('labs', 'cookie_default_path', '');
static::$SameSite = $oConfig->Get('security', 'cookie_samesite', 'Strict');
static::$Secure = isset($_SERVER['HTTPS'])
|| 'None' == static::$SameSite
|| !!$oConfig->Get('labs', 'cookie_default_secure', false);
$bOne = true;
}
return $bOne;
}
public static function get(string $sName) : ?string
{
if (isset($_COOKIE[$sName])) {
$aParts = [];
foreach (\array_keys($_COOKIE) as $sCookieName) {
if (\strtok($sCookieName, '~') === $sName) {
$aParts[$sCookieName] = $_COOKIE[$sCookieName];
}
}
\ksort($aParts);
return \implode('', $aParts);
}
return null;
}
public static function getSecure(string $sName)
{
return isset($_COOKIE[$sName])
? Crypt::DecryptFromJSON(\MailSo\Base\Utils::UrlSafeBase64Decode(static::get($sName)))
: null;
}
private static function _set(string $sName, string $sValue, int $iExpire, bool $httponly = true) : bool
{
$sPath = static::$DefaultPath;
$sPath = $sPath && \strlen($sPath) ? $sPath : '/';
/*
if (\strlen($sValue) > 4000 - \strlen($sPath . $sName)) {
throw new \Exception("Cookie '{$sName}' value too long");
}
*/
if (\strlen($sValue)) {
$_COOKIE[$sName] = $sValue;
} else {
if (!isset($_COOKIE[$sName])) {
return true;
}
unset($_COOKIE[$sName]);
$iExpire = \time() - 3600 * 24 * 30;
}
// Cookie "$sName" has been rejected because it is already expired.
// Happens when \setcookie() sends multiple with the same name (and one is deleted)
// So when previously set, we must delete all 'Set-Cookie' headers and start over
$cookies = [];
$cookie_remove = false;
foreach (\headers_list() as $header) {
if (\preg_match("/Set-Cookie:([^=]+)=/i", $header, $match)) {
if (\trim($match[1]) == $sName) {
$cookie_remove = true;
} else {
$cookies[] = $header;
}
}
}
if ($cookie_remove) {
\header_remove('Set-Cookie');
foreach ($cookies as $cookie) {
\header($cookie);
}
}
return \setcookie($sName, $sValue, array(
'expires' => $iExpire,
'path' => $sPath,
// 'domain' => null,
'secure' => static::$Secure,
'httponly' => $httponly,
'samesite' => static::$SameSite
));
}
/**
* Firefox: Cookie "$sName" has been rejected because it is already expired.
* \header_remove("set-cookie: {$sName}");
*/
public static function set(string $sName, string $sValue, int $iExpire = 0, bool $httponly = true) : void
{
static::init();
$sPath = static::$DefaultPath;
$sPath = $sPath && \strlen($sPath) ? $sPath : '/';
// https://github.com/the-djmaze/snappymail/issues/451
// The 4K browser limit is for the entire cookie, including name, value, expiry date etc.
$iMaxSize = 4000 - \strlen($sPath . $sName);
/*
if ($iMaxSize < \strlen($sValue)) {
throw new \Exception("Cookie '{$sName}' value too long");
}
*/
// Set the new 4K split cookie
foreach (\str_split($sValue, $iMaxSize) as $i => $sPart) {
$sCookieName = $i ? "{$sName}~{$i}" : $sName;
Log::debug('COOKIE', "set {$sCookieName}");
static::_set($sCookieName, $sPart, $iExpire);
}
// Delete unused old 4K split cookie parts
foreach (\array_keys($_COOKIE) as $sCookieName) {
$aSplit = \explode('~', $sCookieName);
if (isset($aSplit[1]) && $aSplit[0] == $sName && $aSplit[1] > $i) {
Log::debug('COOKIE', "unset {$sCookieName}");
static::_set($sCookieName, '', 0);
}
}
}
public static function clear(string $sName) : void
{
static::init();
static::_set($sName, '', 0);
}
}

View file

@ -165,11 +165,7 @@ if (!empty($_ENV['CPANEL']) && !is_dir(APP_PLUGINS_PATH.'login-remote')) {
require __DIR__ . '/cpanel.php';
}
if (class_exists('RainLoop\\Api')) {
RainLoop\Api::Handle();
// NextCloud/OwnCloud?
if (empty($_ENV['SNAPPYMAIL_INCLUDE_AS_API'])) {
RainLoop\Service::Handle();
exit(0);
}
if (class_exists('RainLoop\\Api') && empty($_ENV['SNAPPYMAIL_INCLUDE_AS_API'])) {
RainLoop\Service::Handle();
exit;
}