typo3/cms-core Information Disclosure due to Out-of-scope Site Resolution
Description
TYPO3 is an open source PHP based web content management system. Starting in version 9.4.0 and prior to versions 9.5.42 ELTS, 10.4.39 ELTS, 11.5.30, and 12.4.4, in multi-site scenarios, enumerating the HTTP query parameters id and L allowed out-of-scope access to rendered content in the website frontend. For instance, this allowed visitors to access content of an internal site by adding handcrafted query parameters to the URL of a site that was publicly available. TYPO3 versions 9.5.42 ELTS, 10.4.39 ELTS, 11.5.30, 12.4.4 fix the problem.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
TYPO3 multi-site information disclosure via id and L query parameters allows out-of-scope content access.
Vulnerability
CVE-2023-38499 is an information disclosure vulnerability in TYPO3 CMS affecting versions 9.4.0 through 9.5.41, 10.0.0 through 10.4.38, 11.0.0 through 11.5.29, and 12.0.0 through 12.4.3 [2]. In multi-site setups, the id and L HTTP query parameters could be used to bypass site scope boundaries, allowing an attacker to view rendered content from a different site (e.g., an internal site) by simply appending crafted parameters to a publicly accessible URL [1][4].
Exploitation
Exploitation requires no authentication and can be performed remotely by enumerating the id (page ID) and L (language ID) parameters in the URL. The vulnerability is rated low severity (CVSS 3.1 base score 3.7) due to the high complexity of crafting a valid request and the limited confidentiality impact [4]. An attacker must know or guess valid page IDs belonging to another site in the same TYPO3 instance.
Impact
Successful exploitation allows an attacker to retrieve the frontend-rendered content of pages from a different site, potentially exposing confidential information that should not be publicly accessible. The disclosure is limited to the rendered HTML output, and no direct code execution or data modification is possible [2].
Mitigation
The issue is fixed in TYPO3 versions 9.5.42 ELTS, 10.4.39 ELTS, 11.5.30, and 12.4.4 [4]. The fix disables resolvability via id and L parameters by default; administrators who require the old behavior can enable the security.frontend.allowInsecureSiteResolutionByQueryParameters feature flag [1]. Upgrading to the patched versions is strongly recommended.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
typo3/cms-corePackagist | >= 9.4.0, < 9.5.42 | 9.5.42 |
typo3/cms-corePackagist | >= 10.0.0, < 10.4.39 | 10.4.39 |
typo3/cms-corePackagist | >= 11.0.0, < 11.5.30 | 11.5.30 |
typo3/cms-corePackagist | >= 12.0.0, < 12.4.4 | 12.4.4 |
Affected products
3- osv-coords2 versions
>= 9.4.0, < 9.5.42+ 1 more
- (no CPE)range: >= 9.4.0, < 9.5.42
- (no CPE)range: >= 9.4.0, < 9.5.42
Patches
1702e2debd4b2[SECURITY] Avoid out-of-scope page access for non-matching site
9 files changed · +416 −66
typo3/sysext/core/Classes/Routing/SiteMatcher.php+119 −59 modified@@ -18,9 +18,11 @@ namespace TYPO3\CMS\Core\Routing; use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\UriInterface; use Symfony\Component\Routing\Exception\NoConfigurationException; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use TYPO3\CMS\Core\Cache\CacheManager; +use TYPO3\CMS\Core\Configuration\Features; use TYPO3\CMS\Core\Exception\SiteNotFoundException; use TYPO3\CMS\Core\Http\NormalizedParams; use TYPO3\CMS\Core\SingletonInterface; @@ -48,6 +50,7 @@ class SiteMatcher implements SingletonInterface { public function __construct( + protected readonly Features $features, protected readonly SiteFinder $finder, protected readonly RequestContextFactory $requestContextFactory ) { @@ -79,75 +82,42 @@ public function refresh() */ public function matchRequest(ServerRequestInterface $request): RouteResultInterface { - $site = new NullSite(); - $language = null; - $defaultLanguage = null; + // Remove script file name (index.php) from request uri + $uri = $this->canonicalizeUri($request->getUri(), $request); + $pageId = $this->resolvePageIdQueryParam($request); + $languageId = $this->resolveLanguageIdQueryParam($request); - $pageId = $request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? 0; + $routeResult = $this->matchSiteByUri($uri, $request); - // First, check if we have a _GET/_POST parameter for "id", then a site information can be resolved based. - if ($pageId > 0) { - // Loop over the whole rootline without permissions to get the actual site information - try { - $site = $this->finder->getSiteByPageId((int)$pageId); - // If a "L" parameter is given, we take that one into account. - $languageId = $request->getQueryParams()['L'] ?? $request->getParsedBody()['L'] ?? null; - if ($languageId !== null) { - $language = $site->getLanguageById((int)$languageId); - } else { - // Use this later below - $defaultLanguage = $site->getDefaultLanguage(); - } - } catch (SiteNotFoundException $e) { - // No site found by the given page - } catch (\InvalidArgumentException $e) { - // The language fetched by getLanguageById() was not available, now the PSR-15 middleware - // redirects to the default page. - } + // Allow insecure pageId based site resolution if explicitly enabled and only if both, ?id= and ?L= are defined + // (pageId based site resolution without L parameter has always been prohibited, so we do not support that) + if ( + $this->features->isFeatureEnabled('security.frontend.allowInsecureSiteResolutionByQueryParameters') && + $pageId !== null && $languageId !== null + ) { + return $this->matchSiteByQueryParams($pageId, $languageId, $routeResult, $uri); } - $uri = $request->getUri(); - if (!empty($uri->getPath())) { - $normalizedParams = $request->getAttribute('normalizedParams'); - if ($normalizedParams instanceof NormalizedParams) { - $urlPath = ltrim($uri->getPath(), '/'); - $scriptName = ltrim($normalizedParams->getScriptName(), '/'); - $scriptPath = ltrim($normalizedParams->getSitePath(), '/'); - if ($scriptName !== '' && str_starts_with($urlPath, $scriptName)) { - $urlPath = '/' . $scriptPath . substr($urlPath, mb_strlen($scriptName)); - $uri = $uri->withPath($urlPath); - } - } + // Allow the default language to be resolved in case all languages use a prefix + // and therefore did not match based on path if an explicit pageId is given, + // (example "https://www.example.com/?id=.." was entered, but all languages have "https://www.example.com/lang-key/") + // @todo remove this fallback, in order for SiteBaseRedirectResolver to produce a redirect instead (requires functionals to be adapted) + if ($pageId !== null && $routeResult->getLanguage() === null) { + $routeResult = $routeResult->withLanguage($routeResult->getSite()->getDefaultLanguage()); } - // No language found at this point means that the URL was not used with a valid "?id=1&L=2" parameter - // which resulted in a site / language combination that was found. Now, the matching is done - // on the incoming URL. - if (!($language instanceof SiteLanguage)) { - $collection = $this->getRouteCollectionForAllSites(); - $requestContext = $this->requestContextFactory->fromUri($uri, $request->getMethod()); - $matcher = new BestUrlMatcher($collection, $requestContext); + // adjust the language aspect if it was given by query param `&L` (and ?id is given) + // @todo remove, this is added for backwards (and functional tests) compatibility reasons + if ($languageId !== null && $pageId !== null) { try { - $result = $matcher->match($uri->getPath()); - return new SiteRouteResult( - $uri, - $result['site'], - // if no language is found, this usually results due to "/" called instead of "/fr/" - // but it could also be the reason that "/index.php?id=23" was called, so the default - // language is used as a fallback here then. - $result['language'] ?? $defaultLanguage, - $result['tail'] - ); - } catch (NoConfigurationException | ResourceNotFoundException $e) { - // At this point we discard a possible found site via ?id=123 - // Because ?id=123 _can_ only work if the actual domain/site base works - // so www.domain-without-site-configuration/index.php?id=123 (where 123 is a page referring - // to a page within a site configuration will never be resolved here) properly - $site = new NullSite(); + // override/set language by `&L=` query param + $routeResult = $routeResult->withLanguage($routeResult->getSite()->getLanguageById($languageId)); + } catch (\InvalidArgumentException) { + // ignore; language id not available } } - return new SiteRouteResult($uri, $site, $language); + return $routeResult; } /** @@ -206,4 +176,94 @@ protected function getRouteCollectionForAllSites(): RouteCollection } return $collection; } + + /** + * @return ?positive-int + */ + protected function resolvePageIdQueryParam(ServerRequestInterface $request): ?int + { + $pageId = $request->getQueryParams()['id'] ?? $request->getParsedBody()['id'] ?? null; + if ($pageId === null) { + return null; + } + return (int)$pageId <= 0 ? null : (int)$pageId; + } + + /** + * @return ?positive-int + */ + protected function resolveLanguageIdQueryParam(ServerRequestInterface $request): ?int + { + $languageId = $request->getQueryParams()['L'] ?? $request->getParsedBody()['L'] ?? null; + if ($languageId === null) { + return null; + } + return (int)$languageId < 0 ? null : (int)$languageId; + } + + /** + * Remove script file name (index.php) from request uri + */ + protected function canonicalizeUri(UriInterface $uri, ServerRequestInterface $request): UriInterface + { + if ($uri->getPath() === '') { + return $uri; + } + + $normalizedParams = $request->getAttribute('normalizedParams'); + if (!$normalizedParams instanceof NormalizedParams) { + return $uri; + } + + $urlPath = ltrim($uri->getPath(), '/'); + $scriptName = ltrim($normalizedParams->getScriptName(), '/'); + $scriptPath = ltrim($normalizedParams->getSitePath(), '/'); + if ($scriptName !== '' && str_starts_with($urlPath, $scriptName)) { + $urlPath = '/' . $scriptPath . substr($urlPath, mb_strlen($scriptName)); + $uri = $uri->withPath($urlPath); + } + + return $uri; + } + + protected function matchSiteByUri(UriInterface $uri, ServerRequestInterface $request): SiteRouteResult + { + $collection = $this->getRouteCollectionForAllSites(); + $requestContext = $this->requestContextFactory->fromUri($uri, $request->getMethod()); + $matcher = new BestUrlMatcher($collection, $requestContext); + try { + /** @var array{site: SiteInterface, language: ?SiteLanguage, tail: string} $match */ + $match = $matcher->match($uri->getPath()); + return new SiteRouteResult( + $uri, + $match['site'], + $match['language'], + $match['tail'] + ); + } catch (NoConfigurationException | ResourceNotFoundException) { + return new SiteRouteResult($uri, new NullSite(), null, ''); + } + } + + protected function matchSiteByQueryParams( + int $pageId, + int $languageId, + SiteRouteResult $fallback, + UriInterface $uri, + ): SiteRouteResult { + try { + $site = $this->finder->getSiteByPageId($pageId); + } catch (SiteNotFoundException) { + return $fallback; + } + + try { + // override/set language by `&L=` query param + $language = $site->getLanguageById($languageId); + } catch (\InvalidArgumentException) { + return $fallback; + } + + return new SiteRouteResult($uri, $site, $language); + } }
typo3/sysext/core/Classes/Routing/SiteRouteResult.php+11 −0 modified@@ -95,6 +95,17 @@ public function offsetExists($offset): bool return in_array($offset, $this->validProperties, true) || isset($this->data[$offset]); } + /** + * @internal + */ + public function withLanguage(SiteLanguage $language): self + { + $clone = clone $this; + $clone->language = $language; + + return $clone; + } + /** * @param mixed $offset * @return mixed|UriInterface|string|SiteInterface|SiteLanguage
typo3/sysext/core/Configuration/DefaultConfigurationDescription.yaml+3 −0 modified@@ -210,6 +210,9 @@ SYS: security.frontend.enforceContentSecurityPolicy: type: bool description: 'If on, HTTP Content-Security-Policy header will be applied for each HTTP frontend request.' + security.frontend.allowInsecureSiteResolutionByQueryParameters: + type: bool + description: 'If on, site resolution can be overwritten by `&id=...&L=...` parameters, URI path & host are just used as default.' security.usePasswordPolicyForFrontendUsers: type: bool description: 'If on, the configured password policy in `$GLOBALS[TYPO3_CONF_VARS][FE][passwordPolicy]`
typo3/sysext/core/Configuration/DefaultConfiguration.php+1 −0 modified@@ -76,6 +76,7 @@ 'security.backend.enforceReferrer' => true, 'security.backend.enforceContentSecurityPolicy' => false, 'security.frontend.enforceContentSecurityPolicy' => false, + 'security.frontend.allowInsecureSiteResolutionByQueryParameters' => false, 'security.usePasswordPolicyForFrontendUsers' => false, ], 'createGroup' => '',
typo3/sysext/core/Tests/Unit/Routing/SiteMatcherTest.php+20 −3 modified@@ -17,6 +17,8 @@ namespace TYPO3\CMS\Core\Tests\Unit\Routing; +use PHPUnit\Framework\MockObject\MockObject; +use TYPO3\CMS\Core\Configuration\Features; use TYPO3\CMS\Core\Configuration\SiteConfiguration; use TYPO3\CMS\Core\Http\ServerRequest; use TYPO3\CMS\Core\Routing\BackendEntryPointResolver; @@ -74,9 +76,10 @@ public function fullUrlMatchesSpecificLanguageWithSubdomainsAndDomainSuffixes(): ], ], ]); + $featuresMock = $this->createFeaturesMock(); $finderMock = $this->createSiteFinder($site, $secondSite); $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver()); - $subject = new SiteMatcher($finderMock, $requestContextFactory); + $subject = new SiteMatcher($featuresMock, $finderMock, $requestContextFactory); $request = new ServerRequest('http://9-5.typo3.test/da/my-page/'); /** @var SiteRouteResult $result */ @@ -171,9 +174,10 @@ public function fullUrlMatchesSpecificLanguageWithSubdomainsAndPathSuffixes(): v ], ], ]); + $featuresMock = $this->createFeaturesMock(); $finderMock = $this->createSiteFinder($site, $secondSite); $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver()); - $subject = new SiteMatcher($finderMock, $requestContextFactory); + $subject = new SiteMatcher($featuresMock, $finderMock, $requestContextFactory); $request = new ServerRequest('https://www.example.com/de'); /** @var SiteRouteResult $result */ @@ -253,9 +257,10 @@ public function bestMatchingUrlIsUsed(string $requestUri, string $expectedSite, ], ]); + $featuresMock = $this->createFeaturesMock(); $finderMock = $this->createSiteFinder($mainSite, $dkSite, $frSite); $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver()); - $subject = new SiteMatcher($finderMock, $requestContextFactory); + $subject = new SiteMatcher($featuresMock, $finderMock, $requestContextFactory); $request = new ServerRequest($requestUri); /** @var SiteRouteResult $result */ @@ -265,6 +270,18 @@ public function bestMatchingUrlIsUsed(string $requestUri, string $expectedSite, self::assertSame($expectedLocale, (string)$result->getLanguage()->getLocale()); } + private function createFeaturesMock(): MockObject&Features + { + $mock = $this->getMockBuilder(Features::class) + ->onlyMethods(['isFeatureEnabled']) + ->getMock(); + $mock->expects(self::any()) + ->method('isFeatureEnabled') + ->with('security.frontend.allowInsecureSiteResolutionByQueryParameters') + ->willReturn(false); + return $mock; + } + private function createSiteFinder(Site ...$sites): SiteFinder { $siteConfiguration = new class ($sites) extends SiteConfiguration {
typo3/sysext/frontend/Tests/Functional/SiteHandling/SlugSiteRequestAllowInsecureSiteResolutionByQueryParametersDisabledTest.php+116 −0 added@@ -0,0 +1,116 @@ +<?php + +declare(strict_types=1); + +/* + * 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! + */ + +namespace TYPO3\CMS\Frontend\Tests\Functional\SiteHandling; + +use TYPO3\CMS\Core\Core\Bootstrap; +use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerFactory; +use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerWriter; +use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest; + +final class SlugSiteRequestAllowInsecureSiteResolutionByQueryParametersDisabledTest extends AbstractTestCase +{ + protected array $configurationToUseInTestInstance = [ + 'SYS' => [ + 'devIPmask' => '123.123.123.123', + 'encryptionKey' => '4408d27a916d51e624b69af3554f516dbab61037a9f7b9fd6f81b4d3bedeccb6', + 'features' => [ + 'security.frontend.allowInsecureSiteResolutionByQueryParameters' => false, + ], + ], + 'FE' => [ + 'cacheHash' => [ + 'requireCacheHashPresenceParameters' => ['value', 'testing[value]', 'tx_testing_link[value]'], + 'excludedParameters' => ['L', 'tx_testing_link[excludedValue]'], + 'enforceValidation' => true, + ], + 'debug' => false, + ], + ]; + + protected function setUp(): void + { + parent::setUp(); + $this->withDatabaseSnapshot(function () { + $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv'); + $backendUser = $this->setUpBackendUser(1); + Bootstrap::initializeLanguageObject(); + $scenarioFile = __DIR__ . '/Fixtures/SlugScenario.yaml'; + $factory = DataHandlerFactory::fromYamlFile($scenarioFile); + $writer = DataHandlerWriter::withBackendUser($backendUser); + $writer->invokeFactory($factory); + static::failIfArrayIsNotEmpty($writer->getErrors()); + $this->setUpFrontendRootPage( + 1000, + [ + 'typo3/sysext/core/Tests/Functional/Fixtures/Frontend/JsonRenderer.typoscript', + 'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/JsonRenderer.typoscript', + ], + [ + 'title' => 'ACME Root', + ] + ); + $this->setUpFrontendRootPage( + 3000, + [ + 'typo3/sysext/core/Tests/Functional/Fixtures/Frontend/JsonRenderer.typoscript', + 'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/JsonRenderer.typoscript', + ], + [ + 'title' => 'ACME Archive', + ] + ); + }); + } + + public static function siteWithPageIdRequestsAreCorrectlyHandledDataProvider(): \Generator + { + yield 'valid same-site request is redirected' => ['https://website.local/?id=1000&L=0', 307]; + yield 'valid same-site request is processed' => ['https://website.local/?id=1100&L=0', 200]; + yield 'invalid off-site request with unknown domain is denied' => ['https://otherdomain.website.local/?id=3000&L=0', 404]; + yield 'invalid off-site request with unknown domain and without L parameter is denied' => ['https://otherdomain.website.local/?id=3000', 404]; + yield 'invalid cross-site request without L parameter is denied' => ['https://website.local/?id=3000', 404]; + yield 'invalid cross-site request *not* denied' => ['https://website.local/?id=3000&L=0', 404]; + } + + /** + * @test + * @dataProvider siteWithPageIdRequestsAreCorrectlyHandledDataProvider + */ + public function siteWithPageIdRequestsAreCorrectlyHandled(string $uri, int $expectation): void + { + $this->writeSiteConfiguration( + 'website-local', + $this->buildSiteConfiguration(1000, 'https://website.local/'), + [ + $this->buildDefaultLanguageConfiguration('EN', '/'), + ], + $this->buildErrorHandlingConfiguration('Fluid', [404]) + ); + $this->writeSiteConfiguration( + 'archive-acme-com', + $this->buildSiteConfiguration(3000, 'https://archive.acme.com/'), + [ + $this->buildDefaultLanguageConfiguration('EN', '/'), + ], + $this->buildErrorHandlingConfiguration('Fluid', [404]) + ); + + $response = $this->executeFrontendSubRequest(new InternalRequest($uri)); + self::assertSame($expectation, $response->getStatusCode()); + } +}
typo3/sysext/frontend/Tests/Functional/SiteHandling/SlugSiteRequestAllowInsecureSiteResolutionByQueryParametersEnabledTest.php+118 −0 added@@ -0,0 +1,118 @@ +<?php + +declare(strict_types=1); + +/* + * 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! + */ + +namespace TYPO3\CMS\Frontend\Tests\Functional\SiteHandling; + +use TYPO3\CMS\Core\Core\Bootstrap; +use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerFactory; +use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\Scenario\DataHandlerWriter; +use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest; + +final class SlugSiteRequestAllowInsecureSiteResolutionByQueryParametersEnabledTest extends AbstractTestCase +{ + protected array $configurationToUseInTestInstance = [ + 'SYS' => [ + 'devIPmask' => '123.123.123.123', + 'encryptionKey' => '4408d27a916d51e624b69af3554f516dbab61037a9f7b9fd6f81b4d3bedeccb6', + 'features' => [ + 'security.frontend.allowInsecureSiteResolutionByQueryParameters' => true, + ], + ], + 'FE' => [ + 'cacheHash' => [ + 'requireCacheHashPresenceParameters' => ['value', 'testing[value]', 'tx_testing_link[value]'], + 'excludedParameters' => ['L', 'tx_testing_link[excludedValue]'], + 'enforceValidation' => true, + ], + 'debug' => false, + ], + ]; + + protected function setUp(): void + { + parent::setUp(); + $this->withDatabaseSnapshot(function () { + $this->importCSVDataSet(__DIR__ . '/../Fixtures/be_users.csv'); + $backendUser = $this->setUpBackendUser(1); + Bootstrap::initializeLanguageObject(); + $scenarioFile = __DIR__ . '/Fixtures/SlugScenario.yaml'; + $factory = DataHandlerFactory::fromYamlFile($scenarioFile); + $writer = DataHandlerWriter::withBackendUser($backendUser); + $writer->invokeFactory($factory); + static::failIfArrayIsNotEmpty($writer->getErrors()); + $this->setUpFrontendRootPage( + 1000, + [ + 'typo3/sysext/core/Tests/Functional/Fixtures/Frontend/JsonRenderer.typoscript', + 'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/JsonRenderer.typoscript', + ], + [ + 'title' => 'ACME Root', + ] + ); + $this->setUpFrontendRootPage( + 3000, + [ + 'typo3/sysext/core/Tests/Functional/Fixtures/Frontend/JsonRenderer.typoscript', + 'typo3/sysext/frontend/Tests/Functional/SiteHandling/Fixtures/JsonRenderer.typoscript', + ], + [ + 'title' => 'ACME Archive', + ] + ); + }); + } + + public static function siteWithPageIdRequestsAreCorrectlyHandledDataProvider(): \Generator + { + yield 'valid same-site request is redirected' => ['https://website.local/?id=1000&L=0', 307]; + yield 'valid same-site request is processed' => ['https://website.local/?id=1100&L=0', 200]; + // This case is allowed due to security.frontend.allowInsecureSiteResolutionByQueryParameters, should otherwise be 404 + yield 'invalid off-site request with unknown domain is denied' => ['https://otherdomain.website.local/?id=3000&L=0', 200]; + yield 'invalid off-site request with unknown domain and without L parameter is denied' => ['https://otherdomain.website.local/?id=3000', 404]; + yield 'invalid cross-site request without L parameter is denied' => ['https://website.local/?id=3000', 404]; + // This case is allowed due to security.frontend.allowInsecureSiteResolutionByQueryParameters, should otherwise be 404 + yield 'invalid cross-site request *not* denied' => ['https://website.local/?id=3000&L=0', 200]; + } + + /** + * @test + * @dataProvider siteWithPageIdRequestsAreCorrectlyHandledDataProvider + */ + public function siteWithPageIdRequestsAreCorrectlyHandled(string $uri, int $expectation): void + { + $this->writeSiteConfiguration( + 'website-local', + $this->buildSiteConfiguration(1000, 'https://website.local/'), + [ + $this->buildDefaultLanguageConfiguration('EN', '/'), + ], + $this->buildErrorHandlingConfiguration('Fluid', [404]) + ); + $this->writeSiteConfiguration( + 'archive-acme-com', + $this->buildSiteConfiguration(3000, 'https://archive.acme.com/'), + [ + $this->buildDefaultLanguageConfiguration('EN', '/'), + ], + $this->buildErrorHandlingConfiguration('Fluid', [404]) + ); + + $response = $this->executeFrontendSubRequest(new InternalRequest($uri)); + self::assertSame($expectation, $response->getStatusCode()); + } +}
typo3/sysext/frontend/Tests/Functional/SiteHandling/SlugSiteRequestTest.php+6 −0 modified@@ -273,9 +273,15 @@ public static function siteWithPageIdRequestsAreCorrectlyHandledDataProvider(): { yield 'valid same-site request is redirected' => ['https://website.local/?id=1000&L=0', 307]; yield 'valid same-site request is processed' => ['https://website.local/?id=1100&L=0', 200]; + yield 'invalid off-site request with unknown domain is denied' => ['https://otherdomain.website.local/?id=3000&L=0', 404]; + yield 'invalid off-site request with unknown domain and without L parameter is denied' => ['https://otherdomain.website.local/?id=3000', 404]; } /** + * For variants, please see `SlugSiteRequestAllowInsecureSiteResolutionByQueryParametersEnabledTest` + * and `SlugSiteRequestAllowInsecureSiteResolutionByQueryParametersDisabledTest` which had to be placed + * in separate test class files, due to hard limitations of the TYPO3 Testing Framework. + * * @test * @dataProvider siteWithPageIdRequestsAreCorrectlyHandledDataProvider */
typo3/sysext/frontend/Tests/Unit/Middleware/SiteResolverTest.php+22 −4 modified@@ -17,10 +17,12 @@ namespace TYPO3\CMS\Frontend\Tests\Unit\Middleware; +use PHPUnit\Framework\MockObject\MockObject; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use TYPO3\CMS\Core\Cache\CacheManager; +use TYPO3\CMS\Core\Configuration\Features; use TYPO3\CMS\Core\Configuration\SiteConfiguration; use TYPO3\CMS\Core\Http\JsonResponse; use TYPO3\CMS\Core\Http\NullResponse; @@ -107,8 +109,9 @@ public function detectASingleSiteWhenProperRequestIsGiven(): void ], ], ])); + $featuresMock = $this->createFeaturesMock(); $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver()); - $subject = new SiteResolver(new SiteMatcher($siteFinder, $requestContextFactory)); + $subject = new SiteResolver(new SiteMatcher($featuresMock, $siteFinder, $requestContextFactory)); $request = new ServerRequest($incomingUrl, 'GET'); $response = $subject->process($request, $this->siteFoundRequestHandler); @@ -161,8 +164,9 @@ public function detectSubsiteInsideNestedUrlStructure(): void ]), ); + $featuresMock = $this->createFeaturesMock(); $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver()); - $subject = new SiteResolver(new SiteMatcher($siteFinder, $requestContextFactory)); + $subject = new SiteResolver(new SiteMatcher($featuresMock, $siteFinder, $requestContextFactory)); $request = new ServerRequest($incomingUrl, 'GET'); $response = $subject->process($request, $this->siteFoundRequestHandler); @@ -252,8 +256,9 @@ public function detectSubSubsiteInsideNestedUrlStructure($incomingUrl, $expected ]), ); + $featuresMock = $this->createFeaturesMock(); $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver()); - $subject = new SiteResolver(new SiteMatcher($siteFinder, $requestContextFactory)); + $subject = new SiteResolver(new SiteMatcher($featuresMock, $siteFinder, $requestContextFactory)); $request = new ServerRequest($incomingUrl, 'GET'); $response = $subject->process($request, $this->siteFoundRequestHandler); @@ -363,8 +368,9 @@ public function detectProperLanguageByIncomingUrl($incomingUrl, $expectedSiteIde ]), ); + $featuresMock = $this->createFeaturesMock(); $requestContextFactory = new RequestContextFactory(new BackendEntryPointResolver()); - $subject = new SiteResolver(new SiteMatcher($siteFinder, $requestContextFactory)); + $subject = new SiteResolver(new SiteMatcher($featuresMock, $siteFinder, $requestContextFactory)); $request = new ServerRequest($incomingUrl, 'GET'); $response = $subject->process($request, $this->siteFoundRequestHandler); @@ -381,6 +387,18 @@ public function detectProperLanguageByIncomingUrl($incomingUrl, $expectedSiteIde } } + private function createFeaturesMock(): MockObject&Features + { + $mock = $this->getMockBuilder(Features::class) + ->onlyMethods(['isFeatureEnabled']) + ->getMock(); + $mock->expects(self::any()) + ->method('isFeatureEnabled') + ->with('security.frontend.allowInsecureSiteResolutionByQueryParameters') + ->willReturn(false); + return $mock; + } + private function createSiteFinder(Site ...$sites): SiteFinder { $siteConfiguration = new class ($sites) extends SiteConfiguration {
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-jq6g-4v5m-wm9rghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-38499ghsaADVISORY
- github.com/TYPO3/typo3/commit/702e2debd4b28f9cdb540544565fe6a8627ccb6aghsax_refsource_MISCWEB
- github.com/TYPO3/typo3/security/advisories/GHSA-jq6g-4v5m-wm9rghsax_refsource_CONFIRMWEB
- typo3.org/security/advisory/typo3-core-sa-2023-003ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.