Loading core/lib/Drupal/Core/Access/RouteProcessorCsrf.php +3 −0 Original line number Diff line number Diff line Loading @@ -40,6 +40,9 @@ public function processOutbound($route_name, Route $route, array &$parameters, ? // string when the route is compiled. if (!$bubbleable_metadata || $this->requestStack->getCurrentRequest()->getRequestFormat() !== 'html') { $parameters['token'] = $this->csrfToken->get($path); // Tokens are per session; the response carrying the URL must vary by // session so it isn't cached across users sharing other contexts. $bubbleable_metadata?->addCacheContexts(['session']); } else { // Generate a placeholder and a render array to replace it. Loading core/modules/block/tests/src/Functional/Rest/BlockResourceTestBase.php +3 −1 Original line number Diff line number Diff line Loading @@ -122,7 +122,9 @@ protected function getNormalizedPostEntity() { */ protected function getExpectedCacheContexts() { // @see ::createEntity() return ['url.site']; // 'session' is bubbled by URL generation for CSRF-protected routes // referenced in the response normalization. return ['session', 'url.site']; } /** Loading core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php +21 −0 Original line number Diff line number Diff line Loading @@ -62,4 +62,25 @@ protected function assertResponseWhenMissingAuthentication($method, ResponseInte protected function assertAuthenticationEdgeCases($method, Url $url, array $request_options): void { } /** * {@inheritdoc} * * Stateless authentication via basic_auth does not persist a session between * requests, so the CSRF token seed in the session metadata bag is regenerated * on every request. Any URL carrying a `?token=` (e.g. admin operation links * surfaced as Link headers) therefore legitimately differs between the HEAD * and GET request the test base issues for the same resource. Replace the * token value with a placeholder so the comparison still asserts URL * structure and link relations exactly. */ protected function normalizeHeadersForGetHeadComparison(array $headers): array { if (isset($headers['Link'])) { $headers['Link'] = array_map( fn ($value) => preg_replace('/(\?|&)token=[^&>]+/', '$1token=NORMALIZED', $value), $headers['Link'] ); } return $headers; } } core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +22 −2 Original line number Diff line number Diff line Loading @@ -414,6 +414,26 @@ protected function getExpectedCacheContexts() { ]; } /** * Normalizes response headers before HEAD vs GET equality assertion. * * Authentication providers that don't persist a session between requests * (e.g. basic_auth) cause a fresh CSRF token seed to be generated for each * request, so HEAD and GET produce different token values in any URL that * carries `?token=`. The URL structure and link relations are still the * same. Override this hook to neutralize such legitimately-varying values * for the comparison. * * @param array $headers * Response headers as returned by Guzzle. * * @return array * The headers, with provider-specific normalization applied. */ protected function normalizeHeadersForGetHeadComparison(array $headers): array { return $headers; } /** * Tests all CRUD operations in a single test method. */ Loading Loading @@ -689,8 +709,8 @@ protected function doTestGet(): void { } return $headers; }; $get_headers = $header_cleaner($get_headers); $head_headers = $header_cleaner($head_headers); $get_headers = $this->normalizeHeadersForGetHeadComparison($header_cleaner($get_headers)); $head_headers = $this->normalizeHeadersForGetHeadComparison($header_cleaner($head_headers)); $this->assertSame($get_headers, $head_headers); $this->resourceConfigStorage->load(static::$resourceConfigId)->disable()->save(); Loading core/modules/search/tests/src/Functional/Rest/SearchPageResourceTestBase.php +9 −0 Original line number Diff line number Diff line Loading @@ -90,6 +90,15 @@ protected function getNormalizedPostEntity() { return []; } /** * {@inheritdoc} */ protected function getExpectedCacheContexts() { // 'session' is bubbled by URL generation for CSRF-protected routes // referenced in the response normalization. return array_merge(['session'], parent::getExpectedCacheContexts()); } /** * {@inheritdoc} */ Loading Loading
core/lib/Drupal/Core/Access/RouteProcessorCsrf.php +3 −0 Original line number Diff line number Diff line Loading @@ -40,6 +40,9 @@ public function processOutbound($route_name, Route $route, array &$parameters, ? // string when the route is compiled. if (!$bubbleable_metadata || $this->requestStack->getCurrentRequest()->getRequestFormat() !== 'html') { $parameters['token'] = $this->csrfToken->get($path); // Tokens are per session; the response carrying the URL must vary by // session so it isn't cached across users sharing other contexts. $bubbleable_metadata?->addCacheContexts(['session']); } else { // Generate a placeholder and a render array to replace it. Loading
core/modules/block/tests/src/Functional/Rest/BlockResourceTestBase.php +3 −1 Original line number Diff line number Diff line Loading @@ -122,7 +122,9 @@ protected function getNormalizedPostEntity() { */ protected function getExpectedCacheContexts() { // @see ::createEntity() return ['url.site']; // 'session' is bubbled by URL generation for CSRF-protected routes // referenced in the response normalization. return ['session', 'url.site']; } /** Loading
core/modules/rest/tests/src/Functional/BasicAuthResourceTestTrait.php +21 −0 Original line number Diff line number Diff line Loading @@ -62,4 +62,25 @@ protected function assertResponseWhenMissingAuthentication($method, ResponseInte protected function assertAuthenticationEdgeCases($method, Url $url, array $request_options): void { } /** * {@inheritdoc} * * Stateless authentication via basic_auth does not persist a session between * requests, so the CSRF token seed in the session metadata bag is regenerated * on every request. Any URL carrying a `?token=` (e.g. admin operation links * surfaced as Link headers) therefore legitimately differs between the HEAD * and GET request the test base issues for the same resource. Replace the * token value with a placeholder so the comparison still asserts URL * structure and link relations exactly. */ protected function normalizeHeadersForGetHeadComparison(array $headers): array { if (isset($headers['Link'])) { $headers['Link'] = array_map( fn ($value) => preg_replace('/(\?|&)token=[^&>]+/', '$1token=NORMALIZED', $value), $headers['Link'] ); } return $headers; } }
core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +22 −2 Original line number Diff line number Diff line Loading @@ -414,6 +414,26 @@ protected function getExpectedCacheContexts() { ]; } /** * Normalizes response headers before HEAD vs GET equality assertion. * * Authentication providers that don't persist a session between requests * (e.g. basic_auth) cause a fresh CSRF token seed to be generated for each * request, so HEAD and GET produce different token values in any URL that * carries `?token=`. The URL structure and link relations are still the * same. Override this hook to neutralize such legitimately-varying values * for the comparison. * * @param array $headers * Response headers as returned by Guzzle. * * @return array * The headers, with provider-specific normalization applied. */ protected function normalizeHeadersForGetHeadComparison(array $headers): array { return $headers; } /** * Tests all CRUD operations in a single test method. */ Loading Loading @@ -689,8 +709,8 @@ protected function doTestGet(): void { } return $headers; }; $get_headers = $header_cleaner($get_headers); $head_headers = $header_cleaner($head_headers); $get_headers = $this->normalizeHeadersForGetHeadComparison($header_cleaner($get_headers)); $head_headers = $this->normalizeHeadersForGetHeadComparison($header_cleaner($head_headers)); $this->assertSame($get_headers, $head_headers); $this->resourceConfigStorage->load(static::$resourceConfigId)->disable()->save(); Loading
core/modules/search/tests/src/Functional/Rest/SearchPageResourceTestBase.php +9 −0 Original line number Diff line number Diff line Loading @@ -90,6 +90,15 @@ protected function getNormalizedPostEntity() { return []; } /** * {@inheritdoc} */ protected function getExpectedCacheContexts() { // 'session' is bubbled by URL generation for CSRF-protected routes // referenced in the response normalization. return array_merge(['session'], parent::getExpectedCacheContexts()); } /** * {@inheritdoc} */ Loading