CVE-2023-24814: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: TYPO3 is a free and open source Content Management Framework released under the GNU General Public License. In affected versions the TYPO3 core component `GeneralUtility::getIndpEnv()` uses the unfiltered server environment variable `PATH_INFO`, which allows attackers to inject malicious content. In combination with the TypoScript setting `config.absRefPrefix=auto`, attackers can inject malicious HTML code to pages that have not been rendered and cached, yet. As a result, injected values would be cached and delivered to other website visitors (persisted cross-site scripting). Individual code which relies on the resolved value of `GeneralUtility::getIndpEnv('SCRIPT_NAME')` and corresponding usages (as shown below) are vulnerable as well. Additional investigations confirmed that at least Apache web server deployments using CGI (FPM, FCGI/FastCGI, and similar) are affected. However, there still might be the risk that other scenarios like nginx, IIS, or Apache/mod_php are vulnerable. The usage of server environment variable `PATH_INFO` has been removed from corresponding processings in `GeneralUtility::getIndpEnv()`. Besides that, the public property `TypoScriptFrontendController::$absRefPrefix` is encoded for both being used as a URI component and for being used as a prefix in an HTML context. This mitigates the cross-site scripting vulnerability. Users are advised to update to TYPO3 versions 8.7.51 ELTS, 9.5.40 ELTS, 10.4.35 LTS, 11.5.23 LTS and 12.2.0 which fix this problem. For users who are unable to patch in a timely manner the TypoScript setting `config.absRefPrefix` should at least be set to a static path value, instead of using auto - e.g. `config.absRefPrefix=/`. This workaround **does not fix all aspects of the vulnerability**, and is just considered to be an intermediate mitigation to the most prominent manifestation.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 6.1
Published: 2023-02-07T19:15Z
Repository: https://github.com/TYPO3/typo3
Language: PHP
Files changed: typo3/sysext/core/Classes/Core/SystemEnvironmentBuilder.php, typo3/sysext/core/Classes/Http/NormalizedParams.php, typo3/sysext/core/Classes/Utility/GeneralUtility.php, typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationEnvironment.php, typo3/sysext/core/Tests/Unit/Http/NormalizedParamsTest.php, typo3/sysext/core/Tests/Unit/Log/Processor/WebProcessorTest.php, typo3/sysext/felogin/Tests/Unit/Validation/RedirectUrlValidatorTest.php, typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
Commit date: 2023-02-07 10:25:10 +0100
Commit message: [SECURITY] Prevent XSS due to wrong PATH_INFO evaluation

As already started in #88304 (but only for NormalizedParams)
and later reverted in #89312 (because of cgi-bin problems),
PATH_INFO is no longer considered as a preferable SCRIPT_NAME
alternative. All known server configurations set SCRIPT_NAME
these days to a proper value when cgi.fix_pathinfo is set.

The fallback to PATH_INFO has been introduced with
the initial revision of TYPO3 and isn't needed at all nowadays,
it's actually wrong, as a REQUEST_URI like /index.php/foo/bar
would incorrectly be interpreted as $scriptName == "/foo/bar",
which let's all calculations on $scriptName fail and
even leads to XSS where values derived from $scriptName are
printed without being escaped.

Also any ORIG_SCRIPT_NAME evaluation is dropped, as this variable
contains the SCRIPT_NAME that was set by the webserver configuration
before PHP applied cgi.fix_pathinfo. Using ORIG_SCRIPT_NAME
effectively meant bypassing PHP's pathinfo fix. It usually contains
the cgi-wrapper paths, which is why PATH_INFO was used to overrule
wrong ORIG_SCRIPT_NAME values.

GeneralUtility::getIndpEnv('PATH_INFO') is adapted to trust the
servers PATH_INFO information, now that we no longer allow
servers to send SCRIPT_NAME as PATH_INFO (we enforce
cgi.fix_pathinfo=1 for CGI installations).

The normalized SCRIPT_NAME is now adapted to be encoded as a URL
path by default, as all TYPO3 usages expect this to be an URL path.
Note that $_SERVER['SCRIPT_NAME'] refers to the servers file
system path, not the URL encoded value.

This SCRIPT_NAME sanitization actually enables:

a) TYPO3 to be run in a subfolder that contains characters
that need URL encoding
e.g. `/test:site/` – url encoded that'd be `/test3Asite/`.

b) prevention of XSS in case third party extensions
missed to escape any URL that is derived from SCRIPT_NAME
(while making sure that properly escaped
output is not double escaped)

Resolves: #99651
Related: #88304
Related: #89312
Releases: main, 11.5, 10.4
Change-Id: Ief95253d764665db5182a15ce8ffd02ea02ee61e
Security-Bulletin: TYPO3-CORE-SA-2023-001
Security-References: CVE-2023-24814
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/77739
Tested-by: Oliver Hader <oliver.hader@typo3.org>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>

Diff: diff --git a/typo3/sysext/core/Classes/Core/SystemEnvironmentBuilder.php b/typo3/sysext/core/Classes/Core/SystemEnvironmentBuilder.php
index a612c5be79dd..3d74ba88d9c5 100644
--- a/typo3/sysext/core/Classes/Core/SystemEnvironmentBuilder.php
+++ b/typo3/sysext/core/Classes/Core/SystemEnvironmentBuilder.php
@@ -253,27 +253,21 @@ class SystemEnvironmentBuilder
         }
         return self::getPathThisScriptNonCli();
     }
 
     /**
-     * Calculate path to entry script if not in cli mode.
-     *
-     * Depending on the environment, the script path is found in different $_SERVER variables.
+     * Return path to entry script if not in cli mode.
      *
      * @return string Absolute path to entry script
      */
     protected static function getPathThisScriptNonCli()
     {
-        $isCgi = Environment::isRunningOnCgiServer();
-        if ($isCgi && Environment::usesCgiFixPathInfo()) {
-            return $_SERVER['SCRIPT_FILENAME'];
-        }
-        $cgiPath = $_SERVER['ORIG_PATH_TRANSLATED'] ?? $_SERVER['PATH_TRANSLATED'] ?? '';
-        if ($cgiPath && $isCgi) {
-            return $cgiPath;
+        if (Environment::isRunningOnCgiServer() && !Environment::usesCgiFixPathInfo()) {
+            throw new \Exception('TYPO3 does only support being used with cgi.fix_pathinfo=1 on CGI server APIs.', 1675108421);
         }
-        return $_SERVER['ORIG_SCRIPT_FILENAME'] ?? $_SERVER['SCRIPT_FILENAME'];
+
+        return $_SERVER['SCRIPT_FILENAME'];
     }
 
     /**
      * Calculate path to entry script if in cli mode.
      *
diff --git a/typo3/sysext/core/Classes/Http/NormalizedParams.php b/typo3/sysext/core/Classes/Http/NormalizedParams.php
index 7dc87b183850..2f9c7ab0b1ca 100644
--- a/typo3/sysext/core/Classes/Http/NormalizedParams.php
+++ b/typo3/sysext/core/Classes/Http/NormalizedParams.php
@@ -309,16 +309,17 @@ class NormalizedParams
         $httpHost = $this->httpHost = self::determineHttpHost($serverParams, $configuration, $isBehindReverseProxy);
         $isHttps = $this->isHttps = self::determineHttps($serverParams, $configuration);
         $requestHost = $this->requestHost = ($isHttps ? 'https://' : 'http://') . $httpHost;
         $requestHostOnly = $this->requestHostOnly = self::determineRequestHostOnly($httpHost);
         $this->requestPort = self::determineRequestPort($httpHost, $requestHostOnly);
-        $scriptName = $this->scriptName = self::determineScriptName(
+        $scriptNameOnFileSystem = self::determineScriptName(
             $serverParams,
             $configuration,
             $isHttps,
             $isBehindReverseProxy
         );
+        $scriptName = $this->scriptName = self::encodeFileSystemPathComponentForUrlPath($scriptNameOnFileSystem);
         $requestUri = $this->requestUri = self::determineRequestUri(
             $serverParams,
             $configuration,
             $isHttps,
             $scriptName,
@@ -327,11 +328,11 @@ class NormalizedParams
         $requestUrl = $this->requestUrl = $requestHost . $requestUri;
         $this->requestScript = $requestHost . $scriptName;
         $requestDir = $this->requestDir = $requestHost . GeneralUtility::dirname($scriptName) . '/';
         $this->remoteAddress = self::determineRemoteAddress($serverParams, $configuration, $isBehindReverseProxy);
         $scriptFilename = $this->scriptFilename = $pathThisScript;
-        $this->documentRoot = self::determineDocumentRoot($scriptName, $scriptFilename);
+        $this->documentRoot = self::determineDocumentRoot($scriptNameOnFileSystem, $scriptFilename);
         $siteUrl = $this->siteUrl = self::determineSiteUrl($requestDir, $pathThisScript, $pathSite . '/');
         $this->sitePath = self::determineSitePath($requestHost, $siteUrl);
         $this->siteScript = self::determineSiteScript($requestUrl, $siteUrl);
 
         // @deprecated Below variables can be fully deprecated as soon as core does not use them anymore
@@ -342,10 +343,15 @@ class NormalizedParams
         $this->httpAcceptLanguage = $serverParams['HTTP_ACCEPT_LANGUAGE'] ?? '';
         $this->remoteHost = $serverParams['REMOTE_HOST'] ?? '';
         $this->queryString = $serverParams['QUERY_STRING'] ?? '';
     }
 
+    private static function encodeFileSystemPathComponentForUrlPath(string $path): string
+    {
+        return implode('/', array_map('rawurlencode', explode('/', $path)));
+    }
+
     /**
      * @return string Sanitized HTTP_HOST value host[:port]
      */
     public function getHttpHost(): string
     {
@@ -630,21 +636,11 @@ class NormalizedParams
         array $serverParams,
         array $configuration,
         bool $isHttps,
         bool $isBehindReverseProxy
     ): string {
-        // see https://forge.typo3.org/issues/89312
-        // When using a CGI wrapper to dispatch the PHP process `ORIG_SCRIPT_NAME`
-        // contains the name of the wrapper script (which is most probably outside
-        // the TYPO3's project root) and leads to invalid prefixes, e.g. resolving
-        // the `siteUrl` incorrectly as `http://ip10.local/fcgi/` instead of
-        // actual `http://ip10.local/`
-        $possiblePathInfo = ($serverParams['ORIG_PATH_INFO'] ?? '') ?: ($serverParams['PATH_INFO'] ?? '');
-        $possibleScriptName = ($serverParams['ORIG_SCRIPT_NAME'] ?? '') ?: ($serverParams['SCRIPT_NAME'] ?? '');
-        $scriptName = Environment::isRunningOnCgiServer() && $possiblePathInfo
-            ? $possiblePathInfo
-            : $possibleScriptName;
+        $scriptName = $serverParams['SCRIPT_NAME'] ?? '';
         if ($isBehindReverseProxy) {
             // Add a prefix if TYPO3 is behind a proxy: ext-domain.com => int-server.com/prefix
             if ($isHttps && !empty($configuration['reverseProxyPrefixSSL'])) {
                 $scriptName = $configuration['reverseProxyPrefixSSL'] . $scriptName;
             } elseif (!empty($configuration['reverseProxyPrefix'])) {
@@ -776,23 +772,23 @@ class NormalizedParams
     }
 
     /**
      * Calculate absolute path to web document root
      *
-     * @param string $scriptName Entry script path of URI, without domain and without query parameters, with leading /
+     * @param string $scriptNameOnFileSystem Entry script path of URI on file system, without domain and without query parameters, with leading /
      * @param string $scriptFilename Absolute path to entry script on server filesystem
      * @return string Path to document root with trailing slash
      */
-    protected static function determineDocumentRoot(string $scriptName, string $scriptFilename): string
+    protected static function determineDocumentRoot(string $scriptNameOnFileSystem, string $scriptFilename): string
     {
         // Get the web root (it is not the root of the TYPO3 installation)
         // Some CGI-versions (LA13CGI) and mod-rewrite rules on MODULE versions will deliver a 'wrong'
         // DOCUMENT_ROOT (according to our description). Further various aliases/mod_rewrite rules can
         // disturb this as well. Therefore the DOCUMENT_ROOT is always calculated as the SCRIPT_FILENAME
         // minus the end part shared with SCRIPT_NAME.
         $webDocRoot = '';
-        $scriptNameArray = explode('/', strrev($scriptName));
+        $scriptNameArray = explode('/', strrev($scriptNameOnFileSystem));
         $scriptFilenameArray = explode('/', strrev($scriptFilename));
         $path = [];
         foreach ($scriptNameArray as $segmentNumber => $segment) {
             if ((string)$scriptFilenameArray[$segmentNumber] === (string)$segment) {
                 $path[] = $segment;
diff --git a/typo3/sysext/core/Classes/Utility/GeneralUtility.php b/typo3/sysext/core/Classes/Utility/GeneralUtility.php
index 223982686190..2ba705fb4e03 100644
--- a/typo3/sysext/core/Classes/Utility/GeneralUtility.php
+++ b/typo3/sysext/core/Classes/Utility/GeneralUtility.php
@@ -2308,22 +2308,20 @@ class GeneralUtility
         - ALSO TRY the script from the ROOT of a site (like 'http://www.mytest.com/' and not 'http://www.mytest.com/test/' !!)
          */
         $retVal = '';
         switch ((string)$getEnvName) {
             case 'SCRIPT_NAME':
-                $retVal = Environment::isRunningOnCgiServer()
-                    && (($_SERVER['ORIG_PATH_INFO'] ?? false) ?: ($_SERVER['PATH_INFO'] ?? false))
-                        ? (($_SERVER['ORIG_PATH_INFO'] ?? '') ?: ($_SERVER['PATH_INFO'] ?? ''))
-                        : (($_SERVER['ORIG_SCRIPT_NAME'] ?? '') ?: ($_SERVER['SCRIPT_NAME'] ?? ''));
+                $retVal = $_SERVER['SCRIPT_NAME'] ?? '';
                 // Add a prefix if TYPO3 is behind a proxy: ext-domain.com => int-server.com/prefix
                 if (self::cmpIP($_SERVER['REMOTE_ADDR'] ?? '', $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP'] ?? '')) {
                     if (self::getIndpEnv('TYPO3_SSL') && $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL']) {
                         $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefixSSL'] . $retVal;
                     } elseif ($GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix']) {
                         $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix'] . $retVal;
                     }
                 }
+                $retVal = self::encodeFileSystemPathComponentForUrlPath($retVal);
                 break;
             case 'SCRIPT_FILENAME':
                 $retVal = Environment::getCurrentScript();
                 break;
             case 'REQUEST_URI':
@@ -2348,21 +2346,11 @@ class GeneralUtility
                         $retVal = $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyPrefix'] . $retVal;
                     }
                 }
                 break;
             case 'PATH_INFO':
-                // $_SERVER['PATH_INFO'] != $_SERVER['SCRIPT_NAME'] is necessary because some servers (Windows/CGI)
-                // are seen to set PATH_INFO equal to script_name
-                // Further, there must be at least one '/' in the path - else the PATH_INFO value does not make sense.
-                // IF 'PATH_INFO' never works for our purpose in TYPO3 with CGI-servers,
-                // then 'PHP_SAPI=='cgi'' might be a better check.
-                // Right now strcmp($_SERVER['PATH_INFO'], GeneralUtility::getIndpEnv('SCRIPT_NAME')) will always
-                // return FALSE for CGI-versions, but that is only as long as SCRIPT_NAME is set equal to PATH_INFO
-                // because of PHP_SAPI=='cgi' (see above)
-                if (!Environment::isRunningOnCgiServer()) {
-                    $retVal = $_SERVER['PATH_INFO'] ?? '';
-                }
+                $retVal = $_SERVER['PATH_INFO'] ?? '';
                 break;
             case 'TYPO3_REV_PROXY':
                 $retVal = self::cmpIP($_SERVER['REMOTE_ADDR'] ?? '', $GLOBALS['TYPO3_CONF_VARS']['SYS']['reverseProxyIP']);
                 break;
             case 'REMOTE_ADDR':
@@ -2431,11 +2419,14 @@ class GeneralUtility
                 // Get the web root (it is not the root of the TYPO3 installation)
                 // The absolute path of the script can be calculated with TYPO3_DOCUMENT_ROOT + SCRIPT_FILENAME
                 // Some CGI-versions (LA13CGI) and mod-rewrite rules on MODULE versions will deliver a 'wrong' DOCUMENT_ROOT (according to our description). Further various aliases/mod_rewrite rules can disturb this as well.
                 // Therefore the DOCUMENT_ROOT is now always calculated as the SCRIPT_FILENAME minus the end part shared with SCRIPT_NAME.
                 $SFN = self::getIndpEnv('SCRIPT_FILENAME');
-                $SN_A = explode('/', strrev(self::getIndpEnv('SCRIPT_NAME')));
+                // Use rawurldecode to reverse the result of self::encodeFileSystemPathComponentForUrlPath()
+                // which has been applied to getIndpEnv(SCRIPT_NAME) for web URI usage.
+                // We compare with a file system path (SCRIPT_FILENAME) in here and therefore need to undo the encoding.
+                $SN_A = array_map('rawurldecode', explode('/', strrev(self::getIndpEnv('SCRIPT_NAME'))));
                 $SFN_A = explode('/', strrev($SFN));
                 $acc = [];
                 foreach ($SN_A as $kk => $vv) {
                     if ((string)$SFN_A[$kk] === (string)$vv) {
                         $acc[] = $vv;
@@ -2556,10 +2547,15 @@ class GeneralUtility
         // https://secure.php.net/manual/en/reserved.variables.server.php
         // "Set to a non-empty value if the script was queried through the HTTPS protocol."
         return !empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off';
     }
 
+    protected static function encodeFileSystemPathComponentForUrlPath(string $path): string
+    {
+        return implode('/', array_map('rawurlencode', explode('/', $path)));
+    }
+
     /*************************
      *
      * TYPO3 SPECIFIC FUNCTIONS
      *
      *************************/
diff --git a/typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationEnvironment.php b/typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationEnvironment.php
index 1e904468cb97..4f1cc1013b21 100644
--- a/typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationEnvironment.php
+++ b/typo3/sysext/core/Tests/Acceptance/Support/Extension/ApplicationEnvironment.php
@@ -143,11 +143,10 @@ final class ApplicationEnvironment extends BackendEnvironment
             'SERVER_ADDR' => '127.0.0.1',
             'REMOTE_ADDR' => '127.0.0.1',
             'SCRIPT_NAME' => '/typo3/index.php',
             'PHP_SELF' => '/typo3/index.php',
             'SCRIPT_FILENAME' => $docRoot . '/index.php',
-            'PATH_TRANSLATED' => $docRoot . '/index.php',
             'QUERY_STRING' => $requestUrlParts['query'] ?? '',
             'REQUEST_URI' => $requestUrlParts['path'] . (isset($requestUrlParts['query']) ? '?' . $requestUrlParts['query'] : ''),
             'REQUEST_METHOD' => $method,
         ];
         // Define HTTPS and server port
diff --git a/typo3/sysext/core/Tests/Unit/Http/NormalizedParamsTest.php b/typo3/sysext/core/Tests/Unit/Http/NormalizedParamsTest.php
index 3c171e0c5d63..5f8e38206726 100644
--- a/typo3/sysext/core/Tests/Unit/Http/NormalizedParamsTest.php
+++ b/typo3/sysext/core/Tests/Unit/Http/NormalizedParamsTest.php
@@ -358,81 +358,23 @@ class NormalizedParamsTest extends UnitTestCase
                     'HTTP_HOST' => 'www.domain.com',
                 ],
                 [],
                 '',
             ],
-            'use ORIG_SCRIPT_NAME if ORIG_PATH_INFO is set but empty' => [
-                [
-                    'ORIG_PATH_INFO' => '',
-                    'PATH_INFO' => '',
-                    'ORIG_SCRIPT_NAME' => '/orig/script/name.php',
-                    'SCRIPT_NAME' => '/script/name.php',
-                ],
-                [],
-                '/orig/script/name.php',
-            ],
-            'use ORIG_SCRIPT_NAME if PATH_INFO is set but empty' => [
-                [
-                    'PATH_INFO' => '',
-                    'ORIG_SCRIPT_NAME' => '/orig/script/name.php',
-                    'SCRIPT_NAME' => '/script/name.php',
-                ],
-                [],
-                '/orig/script/name.php',
-            ],
-            'use SCRIPT_NAME if ORIG_PATH_INFO is set but empty' => [
-                [
-                    'ORIG_PATH_INFO' => '',
-                    'PATH_INFO' => '',
-                    'ORIG_SCRIPT_NAME' => '',
-                    'SCRIPT_NAME' => '/script/name.php',
-                ],
-                [],
-                '/script/name.php',
-            ],
-            'use SCRIPT_NAME if PATH_INFO is set but empty' => [
-                [
-                    'PATH_INFO' => '',
-                    'ORIG_SCRIPT_NAME' => '',
-                    'SCRIPT_NAME' => '/script/name.php',
-                ],
-                [],
-                '/script/name.php',
-            ],
-            'use SCRIPT_NAME if ORIG_PATH_INFO is set' => [
-                [
-                    'ORIG_PATH_INFO' => '/foo/bar',
-                    'PATH_INFO' => '',
-                    'ORIG_SCRIPT_NAME' => '',
-                    'SCRIPT_NAME' => '/script/name.php',
-                ],
-                [],
-                '/script/name.php',
-            ],
-            'use SCRIPT_NAME if PATH_INFO is set' => [
+            'use SCRIPT_NAME' => [
                 [
-                    'PATH_INFO' => '/foo/bar',
-                    'ORIG_SCRIPT_NAME' => '',
                     'SCRIPT_NAME' => '/script/name.php',
                 ],
                 [],
                 '/script/name.php',
             ],
-            'use ORIG_SCRIPT_NAME' => [
-                [
-                    'ORIG_SCRIPT_NAME' => '/orig/script/name.php',
-                    'SCRIPT_NAME' => '/script/name.php',
-                ],
-                [],
-                '/orig/script/name.php',
-            ],
-            'use SCRIPT_NAME' => [
+            'apply URL encoding to SCRIPT_NAME' => [
                 [
-                    'SCRIPT_NAME' => '/script/name.php',
+                    'SCRIPT_NAME' => '/test:site/script/name.php',
                 ],
                 [],
-                '/script/name.php',
+                '/test%3Asite/script/name.php',
             ],
             'add proxy ssl prefix' => [
                 [
                     'REMOTE_ADDR' => '123.123.123.123',
                     'HTTPS' => 'on',
@@ -495,10 +437,18 @@ class NormalizedParamsTest extends UnitTestCase
                     'SCRIPT_NAME' => '/typo3/index.php',
                 ],
                 [],
                 '/typo3/index.php?parameter=foo/bar&id=42',
             ],
+            'use query string and script name in special subdirectory if REQUEST_URI is not set' => [
+                [
+                    'QUERY_STRING' => 'parameter=foo/bar&id=42',
+                    'SCRIPT_NAME' => '/sub:dir/typo3/index.php',
+                ],
+                [],
+                '/sub%3Adir/typo3/index.php?parameter=foo/bar&id=42',
+            ],
             'prefix with proxy prefix with ssl if using REQUEST_URI' => [
                 [
                     'HTTP_HOST' => 'www.domain.com',
                     'REMOTE_ADDR' => '123.123.123.123',
                     'HTTPS' => 'on',
@@ -903,11 +853,10 @@ class NormalizedParamsTest extends UnitTestCase
     public function getSiteUrlReturnsExpectedUrl(): void
     {
         $serverParams = [
             'SCRIPT_NAME' => '/typo3/index.php',
             'HTTP_HOST' => 'www.domain.com',
-            'PATH_INFO' => '/typo3/index.php',
         ];
         $pathThisScript = '/var/www/myInstance/Web/typo3/index.php';
         $pathSite = '/var/www/myInstance/Web';
         $expected = 'http://www.domain.com/';
         $serverRequestParameters = new NormalizedParams($serverParams, [], $pathThisScript, $pathSite);
@@ -976,20 +925,22 @@ class NormalizedParamsTest extends UnitTestCase
     public function getSiteScriptReturnsExpectedPathDataProvider(): array
     {
         return [
             'not in a sub directory' => [
                 [
-                    'SCRIPT_NAME' => '/typo3/index.php?id=42&foo=bar',
+                    'SCRIPT_NAME' => '/typo3/index.php',
+                    'REQUEST_URI' => '/typo3/index.php?id=42&foo=bar',
                     'HTTP_HOST' => 'www.domain.com',
                 ],
                 '/var/www/myInstance/Web/typo3/index.php',
                 '/var/www/myInstance/Web',
                 'typo3/index.php?id=42&foo=bar',
             ],
             'in a sub directory' => [
                 [
-                    'SCRIPT_NAME' => '/some/sub/dir/typo3/index.php?id=42&foo=bar',
+                    'SCRIPT_NAME' => '/some/sub/dir/typo3/index.php',
+                    'REQUEST_URI' => '/some/sub/dir/typo3/index.php?id=42&foo=bar',
                     'HTTP_HOST' => 'www.domain.com',
                 ],
                 '/var/www/myInstance/Web/typo3/index.php',
                 '/var/www/myInstance/Web',
                 'typo3/index.php?id=42&foo=bar',
@@ -1021,13 +972,13 @@ class NormalizedParamsTest extends UnitTestCase
      * @test
      */
     public function getPathInfoReturnsExpectedValue(): void
     {
         $serverParams = [
-            'PATH_INFO' => '/typo3/index.php',
+            'PATH_INFO' => '/foo/bar',
         ];
-        $expected = '/typo3/index.php';
+        $expected = '/foo/bar';
         $serverRequestParameters = new NormalizedParams($serverParams, [], '', '');
         self::assertSame($expected, $serverRequestParameters->getPathInfo());
     }
 
     /**
diff --git a/typo3/sysext/core/Tests/Unit/Log/Processor/WebProcessorTest.php b/typo3/sysext/core/Tests/Unit/Log/Processor/WebProcessorTest.php
index adf5a8a6bc08..a386fa784b27 100644
--- a/typo3/sysext/core/Tests/Unit/Log/Processor/WebProcessorTest.php
+++ b/typo3/sysext/core/Tests/Unit/Log/Processor/WebProcessorTest.php
@@ -28,13 +28,12 @@ class WebProcessorTest extends UnitTestCase
     /**
      * @test
      */
     public function webProcessorAddsWebDataToLogRecord(): void
     {
-        $_SERVER['PATH_INFO'] = '';
         $_SERVER['REQUEST_URI'] = '';
-        $_SERVER['ORIG_SCRIPT_NAME'] = '';
+        $_SERVER['SCRIPT_NAME'] = '';
         $_SERVER['REMOTE_ADDR'] = '';
         $_SERVER['QUERY_STRING'] = '';
         $_SERVER['SSL_SESSION_ID'] = '';
         $_SERVER['HTTP_HOST'] = 'acme.com';
 
diff --git a/typo3/sysext/felogin/Tests/Unit/Validation/RedirectUrlValidatorTest.php b/typo3/sysext/felogin/Tests/Unit/Validation/RedirectUrlValidatorTest.php
index 133a3ffc75ac..0dbb17fc1dd8 100644
--- a/typo3/sysext/felogin/Tests/Unit/Validation/RedirectUrlValidatorTest.php
+++ b/typo3/sysext/felogin/Tests/Unit/Validation/RedirectUrlValidatorTest.php
@@ -62,11 +62,11 @@ class RedirectUrlValidatorTest extends UnitTestCase
     /**
      * Set up a fake site path and host
      */
     protected function setUpFakeSitePathAndHost(): void
     {
-        $_SERVER['ORIG_PATH_INFO'] = $_SERVER['PATH_INFO'] = $_SERVER['ORIG_SCRIPT_NAME'] = $_SERVER['SCRIPT_NAME'] = $this->testSitePath . 'index.php';
+        $_SERVER['SCRIPT_NAME'] = $this->testSitePath . 'index.php';
         $_SERVER['HTTP_HOST'] = $this->testHostName;
 
         $request = ServerRequestFactory::fromGlobals();
         $request = $request->withAttribute('applicationType', SystemEnvironmentBuilder::REQUESTTYPE_FE);
         $normalizedParams = NormalizedParams::createFromRequest($request);
diff --git a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
index 9b915dbdfa3e..5d9314723955 100644
--- a/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
+++ b/typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php
@@ -2421,27 +2421,28 @@ class TypoScriptFrontendController implements LoggerAwareInterface
     protected function setAbsRefPrefix()
     {
         if (!$this->absRefPrefix) {
             return;
         }
+        $encodedAbsRefPrefix = htmlspecialchars($this->absRefPrefix, ENT_QUOTES | ENT_HTML5);
         $search = [
             '"_assets/',
             '"typo3temp/',
             '"' . PathUtility::stripPathSitePrefix(Environment::getExtensionsPath()) . '/',
             '"' . PathUtility::stripPathSitePrefix(Environment::getFrameworkBasePath()) . '/',
         ];
         $replace = [
-            '"' . $this->absRefPrefix . '_assets/',
-            '"' . $this->absRefPrefix . 'typo3temp/',
-            '"' . $this->absRefPrefix . PathUtility::stripPathSitePrefix(Environment::getExtensionsPath()) . '/',
-            '"' . $this->absRefPrefix . PathUtility::stripPathSitePrefix(Environment::getFrameworkBasePath()) . '/',
+            '"' . $encodedAbsRefPrefix . '_assets/',
+            '"' . $encodedAbsRefPrefix . 'typo3temp/',
+            '"' . $encodedAbsRefPrefix . PathUtility::stripPathSitePrefix(Environment::getExtensionsPath()) . '/',
+            '"' . $encodedAbsRefPrefix . PathUtility::stripPathSitePrefix(Environment::getFrameworkBasePath()) . '/',
         ];
         // Process additional directories
         $directories = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['FE']['additionalAbsRefPrefixDirectories'], true);
         foreach ($directories as $directory) {
             $search[] = '"' . $directory;
-            $replace[] = '"' . $this->absRefPrefix . $directory;
+            $replace[] = '"' . $encodedAbsRefPrefix . $directory;
         }
         $this->content = str_replace(
             $search,
             $replace,
             $this->content

Vulnerable code:      * Calculate path to entry script if not in cli mode.
     *
     * Depending on the environment, the script path is found in different $_SERVER variables.
        $isCgi = Environment::isRunningOnCgiServer();
        if ($isCgi && Environment::usesCgiFixPathInfo()) {
            return $_SERVER['SCRIPT_FILENAME'];
        }
        $cgiPath = $_SERVER['ORIG_PATH_TRANSLATED'] ?? $_SERVER['PATH_TRANSLATED'] ?? '';
        if ($cgiPath && $isCgi) {
            return $cgiPath;
        return $_SERVER['ORIG_SCRIPT_FILENAME'] ?? $_SERVER['SCRIPT_FILENAME'];
        $scriptName = $this->scriptName = self::determineScriptName(
        $this->documentRoot = self::determineDocumentRoot($scriptName, $scriptFilename);
        // see https://forge.typo3.org/issues/89312
        // When using a CGI wrapper to dispatch the PHP process `ORIG_SCRIPT_NAME`
        // contains the name of the wrapper script (which is most probably outside
        // the TYPO3's project root) and leads to invalid prefixes, e.g. resolving
        // the `siteUrl` incorrectly as `http://ip10.local/fcgi/` instead of
        // actual `http://ip10.local/`
        $possiblePathInfo = ($serverParams['ORIG_PATH_INFO'] ?? '') ?: ($serverParams['PATH_INFO'] ?? '');
        $possibleScriptName = ($serverParams['ORIG_SCRIPT_NAME'] ?? '') ?: ($serverParams['SCRIPT_NAME'] ?? '');
        $scriptName = Environment::isRunningOnCgiServer() && $possiblePathInfo
            ? $possiblePathInfo
            : $possibleScriptName;
     * @param string $scriptName Entry script path of URI, without domain and without query parameters, with leading /
    protected static function determineDocumentRoot(string $scriptName, string $scriptFilename): string
        $scriptNameArray = explode('/', strrev($scriptName));
                $retVal = Environment::isRunningOnCgiServer()
                    && (($_SERVER['ORIG_PATH_INFO'] ?? false) ?: ($_SERVER['PATH_INFO'] ?? false))
                        ? (($_SERVER['ORIG_PATH_INFO'] ?? '') ?: ($_SERVER['PATH_INFO'] ?? ''))
                        : (($_SERVER['ORIG_SCRIPT_NAME'] ?? '') ?: ($_SERVER['SCRIPT_NAME'] ?? ''));
                // $_SERVER['PATH_INFO'] != $_SERVER['SCRIPT_NAME'] is necessary because some servers (Windows/CGI)
                // are seen to set PATH_INFO equal to script_name
                // Further, there must be at least one '/' in the path - else the PATH_INFO value does not make sense.
                // IF 'PATH_INFO' never works for our purpose in TYPO3 with CGI-servers,
                // then 'PHP_SAPI=='cgi'' might be a better check.
                // Right now strcmp($_SERVER['PATH_INFO'], GeneralUtility::getIndpEnv('SCRIPT_NAME')) will always
                // return FALSE for CGI-versions, but that is only as long as SCRIPT_NAME is set equal to PATH_INFO
                // because of PHP_SAPI=='cgi' (see above)
                if (!Environment::isRunningOnCgiServer()) {
                    $retVal = $_SERVER['PATH_INFO'] ?? '';
                }
                $SN_A = explode('/', strrev(self::getIndpEnv('SCRIPT_NAME')));
            'PATH_TRANSLATED' => $docRoot . '/index.php',
            'use ORIG_SCRIPT_NAME if ORIG_PATH_INFO is set but empty' => [
                [
                    'ORIG_PATH_INFO' => '',
                    'PATH_INFO' => '',
                    'ORIG_SCRIPT_NAME' => '/orig/script/name.php',
                    'SCRIPT_NAME' => '/script/name.php',
                ],
                [],
                '/orig/script/name.php',
            ],
            'use ORIG_SCRIPT_NAME if PATH_INFO is set but empty' => [
                [
                    'PATH_INFO' => '',
                    'ORIG_SCRIPT_NAME' => '/orig/script/name.php',
                    'SCRIPT_NAME' => '/script/name.php',
                ],
                [],
                '/orig/script/name.php',
            ],
            'use SCRIPT_NAME if ORIG_PATH_INFO is set but empty' => [
                [
                    'ORIG_PATH_INFO' => '',
                    'PATH_INFO' => '',
                    'ORIG_SCRIPT_NAME' => '',
                    'SCRIPT_NAME' => '/script/name.php',
                ],
                [],
                '/script/name.php',
            ],
            'use SCRIPT_NAME if PATH_INFO is set but empty' => [
                [
                    'PATH_INFO' => '',
                    'ORIG_SCRIPT_NAME' => '',
                    'SCRIPT_NAME' => '/script/name.php',
                ],
                [],
                '/script/name.php',
            ],
            'use SCRIPT_NAME if ORIG_PATH_INFO is set' => [
                [
                    'ORIG_PATH_INFO' => '/foo/bar',
                    'PATH_INFO' => '',
                    'ORIG_SCRIPT_NAME' => '',
                    'SCRIPT_NAME' => '/script/name.php',
                ],
                [],
                '/script/name.php',
            ],
            'use SCRIPT_NAME if PATH_INFO is set' => [
                    'PATH_INFO' => '/foo/bar',
                    'ORIG_SCRIPT_NAME' => '',
            'use ORIG_SCRIPT_NAME' => [
                [
                    'ORIG_SCRIPT_NAME' => '/orig/script/name.php',
                    'SCRIPT_NAME' => '/script/name.php',
                ],
                [],
                '/orig/script/name.php',
            ],
            'use SCRIPT_NAME' => [
                    'SCRIPT_NAME' => '/script/name.php',
                '/script/name.php',
            'PATH_INFO' => '/typo3/index.php',
                    'SCRIPT_NAME' => '/typo3/index.php?id=42&foo=bar',
                    'SCRIPT_NAME' => '/some/sub/dir/typo3/index.php?id=42&foo=bar',
            'PATH_INFO' => '/typo3/index.php',
        $expected = '/typo3/index.php';
        $_SERVER['PATH_INFO'] = '';
        $_SERVER['ORIG_SCRIPT_NAME'] = '';
        $_SERVER['ORIG_PATH_INFO'] = $_SERVER['PATH_INFO'] = $_SERVER['ORIG_SCRIPT_NAME'] = $_SERVER['SCRIPT_NAME'] = $this->testSitePath . 'index.php';
            '"' . $this->absRefPrefix . '_assets/',
            '"' . $this->absRefPrefix . 'typo3temp/',
            '"' . $this->absRefPrefix . PathUtility::stripPathSitePrefix(Environment::getExtensionsPath()) . '/',
            '"' . $this->absRefPrefix . PathUtility::stripPathSitePrefix(Environment::getFrameworkBasePath()) . '/',
            $replace[] = '"' . $this->absRefPrefix . $directory;
Fixed code:      * Return path to entry script if not in cli mode.
        if (Environment::isRunningOnCgiServer() && !Environment::usesCgiFixPathInfo()) {
            throw new \Exception('TYPO3 does only support being used with cgi.fix_pathinfo=1 on CGI server APIs.', 1675108421);

        return $_SERVER['SCRIPT_FILENAME'];
        $scriptNameOnFileSystem = self::determineScriptName(
        $scriptName = $this->scriptName = self::encodeFileSystemPathComponentForUrlPath($scriptNameOnFileSystem);
        $this->documentRoot = self::determineDocumentRoot($scriptNameOnFileSystem, $scriptFilename);
    private static function encodeFileSystemPathComponentForUrlPath(string $path): string
    {
        return implode('/', array_map('rawurlencode', explode('/', $path)));
    }

        $scriptName = $serverParams['SCRIPT_NAME'] ?? '';
     * @param string $scriptNameOnFileSystem Entry script path of URI on file system, without domain and without query parameters, with leading /
    protected static function determineDocumentRoot(string $scriptNameOnFileSystem, string $scriptFilename): string
        $scriptNameArray = explode('/', strrev($scriptNameOnFileSystem));
                $retVal = $_SERVER['SCRIPT_NAME'] ?? '';
                $retVal = self::encodeFileSystemPathComponentForUrlPath($retVal);
                $retVal = $_SERVER['PATH_INFO'] ?? '';
                // Use rawurldecode to reverse the result of self::encodeFileSystemPathComponentForUrlPath()
                // which has been applied to getIndpEnv(SCRIPT_NAME) for web URI usage.
                // We compare with a file system path (SCRIPT_FILENAME) in here and therefore need to undo the encoding.
                $SN_A = array_map('rawurldecode', explode('/', strrev(self::getIndpEnv('SCRIPT_NAME'))));
    protected static function encodeFileSystemPathComponentForUrlPath(string $path): string
    {
        return implode('/', array_map('rawurlencode', explode('/', $path)));
    }

            'use SCRIPT_NAME' => [
            'apply URL encoding to SCRIPT_NAME' => [
                    'SCRIPT_NAME' => '/test:site/script/name.php',
                '/test%3Asite/script/name.php',
            'use query string and script name in special subdirectory if REQUEST_URI is not set' => [
                [
                    'QUERY_STRING' => 'parameter=foo/bar&id=42',
                    'SCRIPT_NAME' => '/sub:dir/typo3/index.php',
                ],
                [],
                '/sub%3Adir/typo3/index.php?parameter=foo/bar&id=42',
            ],
                    'SCRIPT_NAME' => '/typo3/index.php',
                    'REQUEST_URI' => '/typo3/index.php?id=42&foo=bar',
                    'SCRIPT_NAME' => '/some/sub/dir/typo3/index.php',
                    'REQUEST_URI' => '/some/sub/dir/typo3/index.php?id=42&foo=bar',
            'PATH_INFO' => '/foo/bar',
        $expected = '/foo/bar';
        $_SERVER['SCRIPT_NAME'] = '';
        $_SERVER['SCRIPT_NAME'] = $this->testSitePath . 'index.php';
        $encodedAbsRefPrefix = htmlspecialchars($this->absRefPrefix, ENT_QUOTES | ENT_HTML5);
            '"' . $encodedAbsRefPrefix . '_assets/',
            '"' . $encodedAbsRefPrefix . 'typo3temp/',
            '"' . $encodedAbsRefPrefix . PathUtility::stripPathSitePrefix(Environment::getExtensionsPath()) . '/',
            '"' . $encodedAbsRefPrefix . PathUtility::stripPathSitePrefix(Environment::getFrameworkBasePath()) . '/',
            $replace[] = '"' . $encodedAbsRefPrefix . $directory;