Issue #3485174 by fago, arthur_lorenz, alexpott, smustgrave: Menu APIs provide invalid CSRF tokens
(cherry picked from commit ec91b0d0cd
)
merge-requests/10983/merge
parent
3fe7518830
commit
eb59afdbf5
|
@ -1481,7 +1481,7 @@ services:
|
|||
class: Drupal\Core\Access\RouteProcessorCsrf
|
||||
tags:
|
||||
- { name: route_processor_outbound }
|
||||
arguments: ['@csrf_token']
|
||||
arguments: ['@csrf_token', '@request_stack']
|
||||
transliteration:
|
||||
class: Drupal\Core\Transliteration\PhpTransliteration
|
||||
arguments: [null, '@module_handler']
|
||||
|
|
|
@ -6,6 +6,7 @@ use Drupal\Component\Utility\Crypt;
|
|||
use Drupal\Core\Render\BubbleableMetadata;
|
||||
use Drupal\Core\Security\TrustedCallbackInterface;
|
||||
use Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Routing\Route;
|
||||
|
||||
/**
|
||||
|
@ -13,21 +14,21 @@ use Symfony\Component\Routing\Route;
|
|||
*/
|
||||
class RouteProcessorCsrf implements OutboundRouteProcessorInterface, TrustedCallbackInterface {
|
||||
|
||||
/**
|
||||
* The CSRF token generator.
|
||||
*
|
||||
* @var \Drupal\Core\Access\CsrfTokenGenerator
|
||||
*/
|
||||
protected $csrfToken;
|
||||
|
||||
/**
|
||||
* Constructs a RouteProcessorCsrf object.
|
||||
*
|
||||
* @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
|
||||
* @param \Drupal\Core\Access\CsrfTokenGenerator $csrfToken
|
||||
* The CSRF token generator.
|
||||
* @param \Symfony\Component\HttpFoundation\RequestStack|null $requestStack
|
||||
* The request stack.
|
||||
*/
|
||||
public function __construct(CsrfTokenGenerator $csrf_token) {
|
||||
$this->csrfToken = $csrf_token;
|
||||
public function __construct(
|
||||
protected CsrfTokenGenerator $csrfToken,
|
||||
protected ?RequestStack $requestStack = NULL,
|
||||
) {
|
||||
if ($requestStack === NULL) {
|
||||
$this->requestStack = \Drupal::service('request_stack');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -42,7 +43,7 @@ class RouteProcessorCsrf implements OutboundRouteProcessorInterface, TrustedCall
|
|||
}
|
||||
// Adding this to the parameters means it will get merged into the query
|
||||
// string when the route is compiled.
|
||||
if (!$bubbleable_metadata) {
|
||||
if (!$bubbleable_metadata || $this->requestStack->getCurrentRequest()->getRequestFormat() !== 'html') {
|
||||
$parameters['token'] = $this->csrfToken->get($path);
|
||||
}
|
||||
else {
|
||||
|
@ -51,7 +52,6 @@ class RouteProcessorCsrf implements OutboundRouteProcessorInterface, TrustedCall
|
|||
$placeholder_render_array = [
|
||||
'#lazy_builder' => ['route_processor_csrf:renderPlaceholderCsrfToken', [$path]],
|
||||
];
|
||||
|
||||
// Instead of setting an actual CSRF token as the query string, we set
|
||||
// the placeholder, which will be replaced at the very last moment. This
|
||||
// ensures links with CSRF tokens don't break cacheability.
|
||||
|
|
|
@ -103,6 +103,7 @@ class MenuLinksetRoutes extends RouteSubscriberBase implements ContainerInjectio
|
|||
],
|
||||
[
|
||||
'_access' => 'TRUE',
|
||||
'_format' => 'json',
|
||||
],
|
||||
[
|
||||
'parameters' => [
|
||||
|
|
|
@ -5,9 +5,12 @@ declare(strict_types=1);
|
|||
namespace Drupal\Tests\Core\Access;
|
||||
|
||||
use Drupal\Component\Utility\Crypt;
|
||||
use Drupal\Core\Access\CsrfTokenGenerator;
|
||||
use Drupal\Core\Render\BubbleableMetadata;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Drupal\Core\Access\RouteProcessorCsrf;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Routing\Route;
|
||||
|
||||
/**
|
||||
|
@ -18,17 +21,18 @@ class RouteProcessorCsrfTest extends UnitTestCase {
|
|||
|
||||
/**
|
||||
* The mock CSRF token generator.
|
||||
*
|
||||
* @var \Drupal\Core\Access\CsrfTokenGenerator|\PHPUnit\Framework\MockObject\MockObject
|
||||
*/
|
||||
protected $csrfToken;
|
||||
protected CsrfTokenGenerator&MockObject $csrfToken;
|
||||
|
||||
/**
|
||||
* The mock request stack.
|
||||
*/
|
||||
protected RequestStack&MockObject $requestStack;
|
||||
|
||||
/**
|
||||
* The route processor.
|
||||
*
|
||||
* @var \Drupal\Core\Access\RouteProcessorCsrf
|
||||
*/
|
||||
protected $processor;
|
||||
protected RouteProcessorCsrf $processor;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
|
@ -40,7 +44,20 @@ class RouteProcessorCsrfTest extends UnitTestCase {
|
|||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
$this->processor = new RouteProcessorCsrf($this->csrfToken);
|
||||
$this->requestStack = $this->getMockBuilder('Symfony\Component\HttpFoundation\RequestStack')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
$request = $this->createMock('Symfony\Component\HttpFoundation\Request');
|
||||
$request->expects($this->any())
|
||||
->method('getRequestFormat')
|
||||
->willReturn('html');
|
||||
|
||||
$this->requestStack->expects($this->any())
|
||||
->method('getCurrentRequest')
|
||||
->willReturn($request);
|
||||
|
||||
$this->processor = new RouteProcessorCsrf($this->csrfToken, $this->requestStack);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -122,4 +139,37 @@ class RouteProcessorCsrfTest extends UnitTestCase {
|
|||
$this->assertEquals((new BubbleableMetadata())->setAttachments(['placeholders' => [$placeholder => $placeholder_render_array]]), $bubbleable_metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests JSON requests to get no placeholders, but real tokens.
|
||||
*/
|
||||
public function testProcessOutboundJsonFormat(): void {
|
||||
// Create a new request mock that returns 'json' format.
|
||||
$request = $this->createMock('Symfony\Component\HttpFoundation\Request');
|
||||
$request->expects($this->once())
|
||||
->method('getRequestFormat')
|
||||
->willReturn('json');
|
||||
$this->requestStack = $this->createMock('Symfony\Component\HttpFoundation\RequestStack');
|
||||
$this->requestStack->expects($this->once())
|
||||
->method('getCurrentRequest')
|
||||
->willReturn($request);
|
||||
|
||||
// Mock that the CSRF token service should be called once with 'test-path'
|
||||
// and return a test token.
|
||||
$this->csrfToken->expects($this->any())
|
||||
->method('get')
|
||||
->with('test-path')
|
||||
->willReturn('real_token_value');
|
||||
|
||||
$this->processor = new RouteProcessorCsrf($this->csrfToken, $this->requestStack);
|
||||
|
||||
$route = new Route('/test-path', [], ['_csrf_token' => 'TRUE']);
|
||||
$parameters = [];
|
||||
// For JSON requests, the actual CSRF token should be in parameters,
|
||||
// regardless of whether cache metadata is present.
|
||||
$this->processor->processOutbound('test', $route, $parameters);
|
||||
$this->assertEquals('real_token_value', $parameters['token']);
|
||||
$this->processor->processOutbound('test', $route, $parameters, new BubbleableMetadata());
|
||||
$this->assertEquals('real_token_value', $parameters['token']);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue