Encode additional data into the session cookie
authorTim Düsterhus <duesterhus@woltlab.com>
Wed, 16 Dec 2020 11:41:05 +0000 (12:41 +0100)
committerTim Düsterhus <duesterhus@woltlab.com>
Wed, 6 Jan 2021 11:20:08 +0000 (12:20 +0100)
The cookie now stores:
- sessionID
- userID
- an compact representation of the last update that repeats every 64 days.

wcfsetup/install/files/lib/system/session/SessionHandler.class.php

index 1efbfdc8fbde66053400538de3ad640d92d88fa0..5d26472f9f70d2f52c4db47f62da37af814c11ef 100644 (file)
@@ -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();