diff --git a/typo3/sysext/install/Classes/Service/Session/FileSessionHandler.php b/typo3/sysext/install/Classes/Service/Session/FileSessionHandler.php
new file mode 100644
index 0000000000000000000000000000000000000000..17156bf52503e4971f118a15179026b4fddb8140
--- /dev/null
+++ b/typo3/sysext/install/Classes/Service/Session/FileSessionHandler.php
@@ -0,0 +1,293 @@
+<?php
+declare(strict_types=1);
+
+namespace TYPO3\CMS\Install\Service\Session;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Service\Exception;
+
+/**
+ * PHP session handling with "secure" session files (hashed session id)
+ * see http://www.php.net/manual/en/function.session-set-save-handler.php
+ */
+class FileSessionHandler implements \SessionHandlerInterface
+{
+    /**
+     * The path to our var/session/ folder (where we can write our sessions). Set in the
+     * constructor.
+     * Path where to store our session files in var/session/.
+     *
+     * @var string
+     */
+    private $sessionPath = 'session/';
+
+    /**
+     * time (minutes) to expire an unused session
+     *
+     * @var int
+     */
+    private $expirationTimeInMinutes = 60;
+
+    public function __construct(string $sessionPath, int $expirationTimeInMinutes)
+    {
+        $this->sessionPath = rtrim($sessionPath, '/') . '/';
+        $this->expirationTimeInMinutes = $expirationTimeInMinutes;
+        // Start our PHP session early so that hasSession() works
+        session_save_path($this->getSessionSavePath());
+    }
+
+    /**
+     * Returns the path where to store our session files
+     *
+     * @throws \TYPO3\CMS\Install\Exception
+     * @return string Session save path
+     */
+    private function getSessionSavePath()
+    {
+        if (empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'])) {
+            throw new \TYPO3\CMS\Install\Exception(
+                'No encryption key set to secure session',
+                1371243449
+            );
+        }
+        $sessionSavePath = $this->sessionPath . GeneralUtility::hmac('session:' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']);
+        $this->ensureSessionSavePathExists($sessionSavePath);
+        return $sessionSavePath;
+    }
+
+    /**
+     * Returns the file where to store our session data
+     *
+     * @param string $id
+     * @return string A filename
+     */
+    private function getSessionFile($id)
+    {
+        $sessionSavePath = $this->getSessionSavePath();
+        return $sessionSavePath . '/hash_' . $this->getSessionHash($id);
+    }
+
+    /**
+     * Open function. See @session_set_save_handler
+     *
+     * @param string $savePath
+     * @param string $sessionName
+     * @return bool
+     */
+    public function open($savePath, $sessionName)
+    {
+        return true;
+    }
+
+    /**
+     * Close function. See @session_set_save_handler
+     *
+     * @return bool
+     */
+    public function close()
+    {
+        return true;
+    }
+
+    /**
+     * Read session data. See @session_set_save_handler
+     *
+     * @param string $id The session id
+     * @return string
+     */
+    public function read($id)
+    {
+        $sessionFile = $this->getSessionFile($id);
+        $content = '';
+        if (file_exists($sessionFile)) {
+            if ($fd = fopen($sessionFile, 'rb')) {
+                $lockres = flock($fd, LOCK_SH);
+                if ($lockres) {
+                    $length = filesize($sessionFile);
+                    if ($length > 0) {
+                        $content = fread($fd, $length);
+                    }
+                    flock($fd, LOCK_UN);
+                }
+                fclose($fd);
+            }
+        }
+        // Do a "test write" of the session file after opening it. The real session data is written in
+        // __destruct() and we can not create a sane error message there anymore, so this test should fail
+        // before if final session file can not be written due to permission problems.
+        $this->write($id, $content);
+        return $content;
+    }
+
+    /**
+     * Write session data. See @session_set_save_handler
+     *
+     * @param string $id The session id
+     * @param string $sessionData The data to be stored
+     * @throws Exception
+     * @return bool
+     */
+    public function write($id, $sessionData)
+    {
+        $sessionFile = $this->getSessionFile($id);
+        $result = false;
+        $changePermissions = !@is_file($sessionFile);
+        if ($fd = fopen($sessionFile, 'cb')) {
+            if (flock($fd, LOCK_EX)) {
+                ftruncate($fd, 0);
+                $res = fwrite($fd, $sessionData);
+                if ($res !== false) {
+                    fflush($fd);
+                    $result = true;
+                }
+                flock($fd, LOCK_UN);
+            }
+            fclose($fd);
+            // Change the permissions only if the file has just been created
+            if ($changePermissions) {
+                GeneralUtility::fixPermissions($sessionFile);
+            }
+        }
+        if (!$result) {
+            throw new Exception(
+                'Session file not writable. Please check permission on ' .
+                $this->sessionPath . ' and its subdirectories.',
+                1424355157
+            );
+        }
+        return $result;
+    }
+
+    /**
+     * Destroys one session. See @session_set_save_handler
+     *
+     * @param string $id The session id
+     * @return bool
+     */
+    public function destroy($id): bool
+    {
+        $sessionFile = $this->getSessionFile($id);
+        return @unlink($sessionFile);
+    }
+
+    /**
+     * Garbage collect session info. See @session_set_save_handler
+     *
+     * @param int $maxLifeTime The setting of session.gc_maxlifetime
+     * @return bool
+     */
+    public function gc($maxLifeTime)
+    {
+        $sessionSavePath = $this->getSessionSavePath();
+        $files = glob($sessionSavePath . '/hash_*');
+        if (!is_array($files)) {
+            return true;
+        }
+        foreach ($files as $filename) {
+            if (filemtime($filename) + $this->expirationTimeInMinutes * 60 < time()) {
+                @unlink($filename);
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Writes the session data at the end, to overcome a PHP APC bug.
+     *
+     * Writes the session data in a proper context that is not affected by the APC bug:
+     * http://pecl.php.net/bugs/bug.php?id=16721.
+     *
+     * This behaviour was introduced in #17511, where self::write() made use of GeneralUtility
+     * which due to the APC bug throws a "Fatal error: Class 'GeneralUtility' not found"
+     * (and the session data is not saved). Calling session_write_close() at this point
+     * seems to be the most easy solution, according to PHP author.
+     */
+    public function __destruct()
+    {
+        session_write_close();
+    }
+
+    /**
+     * Returns the session ID of the running session.
+     *
+     * @return string the session ID
+     */
+    public function getSessionId()
+    {
+        return session_id();
+    }
+
+    /**
+     * Returns a session hash, which can only be calculated by the server.
+     * Used to store our session files without exposing the session ID.
+     *
+     * @param string $sessionId An alternative session ID. Defaults to our current session ID
+     * @throws \TYPO3\CMS\Install\Exception
+     * @return string the session hash
+     */
+    private function getSessionHash($sessionId = '')
+    {
+        if (empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'])) {
+            throw new \TYPO3\CMS\Install\Exception(
+                'No encryption key set to secure session',
+                1371243450
+            );
+        }
+        if (!$sessionId) {
+            $sessionId = $this->getSessionId();
+        }
+        return md5($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] . '|' . $sessionId);
+    }
+
+    /**
+     * Create directories for the session save path
+     * and throw an exception if that fails.
+     *
+     * @param string $sessionSavePath The absolute path to the session files
+     * @throws \TYPO3\CMS\Install\Exception
+     */
+    private function ensureSessionSavePathExists($sessionSavePath)
+    {
+        if (!is_dir($sessionSavePath)) {
+            try {
+                GeneralUtility::mkdir_deep($sessionSavePath);
+            } catch (\RuntimeException $exception) {
+                throw new \TYPO3\CMS\Install\Exception(
+                    'Could not create session folder in ' . $this->sessionPath . '. Make sure it is writeable!',
+                    1294587484
+                );
+            }
+            $htaccessContent = '
+# Apache < 2.3
+<IfModule !mod_authz_core.c>
+	Order allow,deny
+	Deny from all
+	Satisfy All
+</IfModule>
+
+# Apache ≥ 2.3
+<IfModule mod_authz_core.c>
+	Require all denied
+</IfModule>
+			';
+            GeneralUtility::writeFile($sessionSavePath . '/.htaccess', $htaccessContent);
+            $indexContent = '<!DOCTYPE html>';
+            $indexContent .= '<html><head><title></title><meta http-equiv=Refresh Content="0; Url=../../"/>';
+            $indexContent .= '</head></html>';
+            GeneralUtility::writeFile($sessionSavePath . '/index.html', $indexContent);
+        }
+    }
+}
diff --git a/typo3/sysext/install/Classes/Service/SessionService.php b/typo3/sysext/install/Classes/Service/SessionService.php
index b7351b54621e6a7117f522c873528116eb702763..5dce38ecbeffed7bd86b4163c670919f1f8662bf 100644
--- a/typo3/sysext/install/Classes/Service/SessionService.php
+++ b/typo3/sysext/install/Classes/Service/SessionService.php
@@ -21,6 +21,7 @@ use TYPO3\CMS\Core\Http\CookieHeaderTrait;
 use TYPO3\CMS\Core\Messaging\FlashMessage;
 use TYPO3\CMS\Core\SingletonInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Install\Service\Session\FileSessionHandler;
 
 /**
  * Secure session handling for the install tool.
@@ -30,22 +31,6 @@ class SessionService implements SingletonInterface
 {
     use CookieHeaderTrait;
 
-    /**
-     * The path to our var/ folder (where we can write our sessions). Set in the
-     * constructor.
-     *
-     * @var string
-     */
-    private $basePath;
-
-    /**
-     * Path where to store our session files in var/session/. %s will be
-     * non-guessable.
-     *
-     * @var string
-     */
-    private $sessionPath = 'session/%s';
-
     /**
      * the cookie to store the session ID of the install tool
      *
@@ -74,12 +59,13 @@ class SessionService implements SingletonInterface
      */
     public function __construct()
     {
-        $this->basePath = Environment::getVarPath() . '/';
-        // Start our PHP session early so that hasSession() works
-        $sessionSavePath = $this->getSessionSavePath();
         // Register our "save" session handler
-        session_set_save_handler([$this, 'open'], [$this, 'close'], [$this, 'read'], [$this, 'write'], [$this, 'destroy'], [$this, 'gc']);
-        session_save_path($sessionSavePath);
+        $sessionHandler = GeneralUtility::makeInstance(
+            FileSessionHandler::class,
+            Environment::getVarPath() . '/session',
+            $this->expireTimeInMinutes
+        );
+        session_set_save_handler($sessionHandler);
         session_name($this->cookieName);
         ini_set('session.cookie_httponly', true);
         if ($this->hasSameSiteCookieSupport()) {
@@ -107,67 +93,6 @@ class SessionService implements SingletonInterface
         }
     }
 
-    /**
-     * Returns the path where to store our session files
-     *
-     * @throws \TYPO3\CMS\Install\Exception
-     * @return string Session save path
-     */
-    private function getSessionSavePath()
-    {
-        if (empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'])) {
-            throw new \TYPO3\CMS\Install\Exception(
-                'No encryption key set to secure session',
-                1371243449
-            );
-        }
-        $sessionSavePath = sprintf(
-            $this->basePath . $this->sessionPath,
-            GeneralUtility::hmac('session:' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'])
-        );
-        $this->ensureSessionSavePathExists($sessionSavePath);
-        return $sessionSavePath;
-    }
-
-    /**
-     * Create directories for the session save path
-     * and throw an exception if that fails.
-     *
-     * @param string $sessionSavePath The absolute path to the session files
-     * @throws \TYPO3\CMS\Install\Exception
-     */
-    private function ensureSessionSavePathExists($sessionSavePath)
-    {
-        if (!is_dir($sessionSavePath)) {
-            try {
-                GeneralUtility::mkdir_deep($sessionSavePath);
-            } catch (\RuntimeException $exception) {
-                throw new \TYPO3\CMS\Install\Exception(
-                    'Could not create session folder in ' . Environment::getVarPath() . '. Make sure it is writeable!',
-                    1294587484
-                );
-            }
-            $htaccessContent = '
-# Apache < 2.3
-<IfModule !mod_authz_core.c>
-	Order allow,deny
-	Deny from all
-	Satisfy All
-</IfModule>
-
-# Apache ≥ 2.3
-<IfModule mod_authz_core.c>
-	Require all denied
-</IfModule>
-			';
-            GeneralUtility::writeFile($sessionSavePath . '/.htaccess', $htaccessContent);
-            $indexContent = '<!DOCTYPE html>';
-            $indexContent .= '<html><head><title></title><meta http-equiv=Refresh Content="0; Url=../../"/>';
-            $indexContent .= '</head></html>';
-            GeneralUtility::writeFile($sessionSavePath . '/index.html', $indexContent);
-        }
-    }
-
     /**
      * Starts a new session
      *
@@ -221,38 +146,6 @@ class SessionService implements SingletonInterface
         return $_SESSION['active'] === true;
     }
 
-    /**
-     * Returns the session ID of the running session.
-     *
-     * @return string the session ID
-     */
-    public function getSessionId()
-    {
-        return session_id();
-    }
-
-    /**
-     * Returns a session hash, which can only be calculated by the server.
-     * Used to store our session files without exposing the session ID.
-     *
-     * @param string $sessionId An alternative session ID. Defaults to our current session ID
-     * @throws \TYPO3\CMS\Install\Exception
-     * @return string the session hash
-     */
-    private function getSessionHash($sessionId = '')
-    {
-        if (empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'])) {
-            throw new \TYPO3\CMS\Install\Exception(
-                'No encryption key set to secure session',
-                1371243450
-            );
-        }
-        if (!$sessionId) {
-            $sessionId = $this->getSessionId();
-        }
-        return md5($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] . '|' . $sessionId);
-    }
-
     /**
      * Marks this session as an "authorized" one (login successful).
      * Should only be called if:
@@ -382,164 +275,6 @@ class SessionService implements SingletonInterface
         return $messages;
     }
 
-    /*************************
-     *
-     * PHP session handling with "secure" session files (hashed session id)
-     * see http://www.php.net/manual/en/function.session-set-save-handler.php
-     *
-     *************************/
-    /**
-     * Returns the file where to store our session data
-     *
-     * @param string $id
-     * @return string A filename
-     */
-    private function getSessionFile($id)
-    {
-        $sessionSavePath = $this->getSessionSavePath();
-        return $sessionSavePath . '/hash_' . $this->getSessionHash($id);
-    }
-
-    /**
-     * Open function. See @session_set_save_handler
-     *
-     * @param string $savePath
-     * @param string $sessionName
-     * @return bool
-     */
-    public function open($savePath, $sessionName)
-    {
-        return true;
-    }
-
-    /**
-     * Close function. See @session_set_save_handler
-     *
-     * @return bool
-     */
-    public function close()
-    {
-        return true;
-    }
-
-    /**
-     * Read session data. See @session_set_save_handler
-     *
-     * @param string $id The session id
-     * @return string
-     */
-    public function read($id)
-    {
-        $sessionFile = $this->getSessionFile($id);
-        $content = '';
-        if (file_exists($sessionFile)) {
-            if ($fd = fopen($sessionFile, 'rb')) {
-                $lockres = flock($fd, LOCK_SH);
-                if ($lockres) {
-                    $length = filesize($sessionFile);
-                    if ($length > 0) {
-                        $content = fread($fd, $length);
-                    }
-                    flock($fd, LOCK_UN);
-                }
-                fclose($fd);
-            }
-        }
-        // Do a "test write" of the session file after opening it. The real session data is written in
-        // __destruct() and we can not create a sane error message there anymore, so this test should fail
-        // before if final session file can not be written due to permission problems.
-        $this->write($id, $content);
-        return $content;
-    }
-
-    /**
-     * Write session data. See @session_set_save_handler
-     *
-     * @param string $id The session id
-     * @param string $sessionData The data to be stored
-     * @throws Exception
-     * @return bool
-     */
-    public function write($id, $sessionData)
-    {
-        $sessionFile = $this->getSessionFile($id);
-        $result = false;
-        $changePermissions = !@is_file($sessionFile);
-        if ($fd = fopen($sessionFile, 'cb')) {
-            if (flock($fd, LOCK_EX)) {
-                ftruncate($fd, 0);
-                $res = fwrite($fd, $sessionData);
-                if ($res !== false) {
-                    fflush($fd);
-                    $result = true;
-                }
-                flock($fd, LOCK_UN);
-            }
-            fclose($fd);
-            // Change the permissions only if the file has just been created
-            if ($changePermissions) {
-                GeneralUtility::fixPermissions($sessionFile);
-            }
-        }
-        if (!$result) {
-            throw new Exception(
-                'Session file not writable. Please check permission on ' .
-                Environment::getVarPath() . '/session and its subdirectories.',
-                1424355157
-            );
-        }
-        return $result;
-    }
-
-    /**
-     * Destroys one session. See @session_set_save_handler
-     *
-     * @param string $id The session id
-     * @return string
-     */
-    public function destroy($id)
-    {
-        $sessionFile = $this->getSessionFile($id);
-        return @unlink($sessionFile);
-    }
-
-    /**
-     * Garbage collect session info. See @session_set_save_handler
-     *
-     * @param int $maxLifeTime The setting of session.gc_maxlifetime
-     * @return bool
-     */
-    public function gc($maxLifeTime)
-    {
-        $sessionSavePath = $this->getSessionSavePath();
-        $files = glob($sessionSavePath . '/hash_*');
-        if (!is_array($files)) {
-            return true;
-        }
-        foreach ($files as $filename) {
-            if (filemtime($filename) + $this->expireTimeInMinutes * 60 < time()) {
-                @unlink($filename);
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Writes the session data at the end, to overcome a PHP APC bug.
-     *
-     * Writes the session data in a proper context that is not affected by the APC bug:
-     * http://pecl.php.net/bugs/bug.php?id=16721.
-     *
-     * This behaviour was introduced in #17511, where self::write() made use of GeneralUtility
-     * which due to the APC bug throws a "Fatal error: Class 'GeneralUtility' not found"
-     * (and the session data is not saved). Calling session_write_close() at this point
-     * seems to be the most easy solution, according to PHP author.
-     */
-    public function __destruct()
-    {
-        session_write_close();
-    }
-
     /**
      * Check if php session.auto_start is enabled
      *