From 3b07fad7445f10555cc367eadedb9543565e4943 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Tim=20D=C3=BCsterhus?= Date: Wed, 16 Dec 2020 12:41:05 +0100 Subject: [PATCH] Encode additional data into the session cookie The cookie now stores: - sessionID - userID - an compact representation of the last update that repeats every 64 days. --- .../system/session/SessionHandler.class.php | 113 +++++++++++++++--- 1 file changed, 96 insertions(+), 17 deletions(-) diff --git a/wcfsetup/install/files/lib/system/session/SessionHandler.class.php b/wcfsetup/install/files/lib/system/session/SessionHandler.class.php index 1efbfdc8fb..5d26472f9f 100644 --- a/wcfsetup/install/files/lib/system/session/SessionHandler.class.php +++ b/wcfsetup/install/files/lib/system/session/SessionHandler.class.php @@ -201,38 +201,117 @@ final class SessionHandler extends SingletonFactory { public function setHasValidCookie($hasValidCookie) { } /** - * Returns the session ID stored in the session cookie or `null`. + * Parses the session cookie value, returning an array with the stored fields. + * + * The return array is guaranteed to have a `sessionId` key. + */ + private function parseCookie(string $value): array { + $length = \mb_strlen($value, '8bit'); + if ($length < 1) { + throw new \InvalidArgumentException(\sprintf( + 'Expected at least 1 Byte, %d given.', + $length + )); + } + + $version = \unpack('Cversion', $value)['version']; + if (!in_array($version, [1], true)) { + throw new \InvalidArgumentException(\sprintf( + 'Unknown version %d', + $version + )); + } + + if ($version === 1) { + if ($length !== 26) { + throw new \InvalidArgumentException(\sprintf( + 'Expected exactly 26 Bytes, %d given.', + $length + )); + } + $data = \unpack('Cversion/A20sessionId/Ctimestep/NuserId', $value); + $data['sessionId'] = Hex::encode($data['sessionId']); + + return $data; + } + + throw new \LogicException('Unreachable'); + } + + /** + * Extracts the data from the cookie identified by the `$isACP` parameter. + * If the `$isACP` parameter is `null` the current environment is assumed. + * + * @see SessionHandler::parseCookie() */ - private function getSessionIdFromCookie(): ?string { - $cookieName = COOKIE_PREFIX.($this->isACP ? 'acp' : 'user')."_session"; + private function getParsedCookieData(?bool $isACP = null): ?array { + if ($isACP === null) { + $isACP = $this->isACP; + } + + $cookieName = COOKIE_PREFIX.($isACP ? 'acp' : 'user')."_session"; if (!empty($_COOKIE[$cookieName])) { if (!PACKAGE_ID) { - return $_COOKIE[$cookieName]; + return [ + 'sessionId' => $_COOKIE[$cookieName], + ]; } - $compressedSessionId = CryptoUtil::getValueFromSignedString($_COOKIE[$cookieName]); + $cookieData = CryptoUtil::getValueFromSignedString($_COOKIE[$cookieName]); // Check whether the sessionId was correctly signed. - if (!$compressedSessionId) { + if (!$cookieData) { return null; } - return Hex::encode($compressedSessionId); + try { + return $this->parseCookie($cookieData); + } + catch (\InvalidArgumentException $e) { + return null; + } + } + + return null; + } + + /** + * Returns the session ID stored in the session cookie or `null`. + */ + private function getSessionIdFromCookie(): ?string { + $cookieData = $this->getParsedCookieData(); + + if ($cookieData) { + return $cookieData['sessionId']; } return null; } /** - * Returns the signed session ID for use in a cookie. + * Returns the current time step. The time step changes + * every 6 hours. */ - private function getSessionIdForCookie(string $sessionID): string { + private function getCookieTimestep(): int { + return floor(TIME_NOW / (6 * 3600)) & 0xFF; + } + + /** + * Returns the signed session data for use in a cookie. + */ + private function getCookieValue(): string { if (!PACKAGE_ID) { - return $sessionID; + return $this->sessionID; } - return CryptoUtil::createSignedString(Hex::decode($sessionID)); + return CryptoUtil::createSignedString(\pack( + 'CA20CN', + 1, + Hex::decode($this->sessionID), + $this->getCookieTimestep(), + $this->user->userID ?: 0 + )); } /** @@ -498,7 +577,7 @@ final class SessionHandler extends SingletonFactory { if ($this->user->userID && !$this->isACP) { HeaderUtil::setCookie( ($this->isACP ? 'acp' : 'user') . '_session', - $this->getSessionIdForCookie($this->sessionID), + $this->getCookieValue(), TIME_NOW + 86400 * 14 ); } @@ -555,15 +634,15 @@ final class SessionHandler extends SingletonFactory { \serialize([]), ]); - HeaderUtil::setCookie( - ($this->isACP ? 'acp' : 'user')."_session", - $this->getSessionIdForCookie($this->sessionID) - ); - $this->variables = []; $this->user = new User(null); $this->firstVisit = true; + HeaderUtil::setCookie( + ($this->isACP ? 'acp' : 'user')."_session", + $this->getCookieValue() + ); + // Maintain legacy session table for users online list. if (!$this->isACP) { $this->createLegacySession(); -- 2.20.1