Issue #3485174 by fago, arthur_lorenz, alexpott, smustgrave: Menu APIs provide invalid CSRF tokens
parent
2f09ecf6fa
commit
ec91b0d0cd
|
@ -1457,7 +1457,7 @@ services:
|
||||||
class: Drupal\Core\Access\RouteProcessorCsrf
|
class: Drupal\Core\Access\RouteProcessorCsrf
|
||||||
tags:
|
tags:
|
||||||
- { name: route_processor_outbound }
|
- { name: route_processor_outbound }
|
||||||
arguments: ['@csrf_token']
|
arguments: ['@csrf_token', '@request_stack']
|
||||||
transliteration:
|
transliteration:
|
||||||
class: Drupal\Core\Transliteration\PhpTransliteration
|
class: Drupal\Core\Transliteration\PhpTransliteration
|
||||||
arguments: [null, '@module_handler']
|
arguments: [null, '@module_handler']
|
||||||
|
|
|
@ -6,6 +6,7 @@ use Drupal\Component\Utility\Crypt;
|
||||||
use Drupal\Core\Render\BubbleableMetadata;
|
use Drupal\Core\Render\BubbleableMetadata;
|
||||||
use Drupal\Core\Security\TrustedCallbackInterface;
|
use Drupal\Core\Security\TrustedCallbackInterface;
|
||||||
use Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface;
|
use Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
use Symfony\Component\Routing\Route;
|
use Symfony\Component\Routing\Route;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,21 +14,21 @@ use Symfony\Component\Routing\Route;
|
||||||
*/
|
*/
|
||||||
class RouteProcessorCsrf implements OutboundRouteProcessorInterface, TrustedCallbackInterface {
|
class RouteProcessorCsrf implements OutboundRouteProcessorInterface, TrustedCallbackInterface {
|
||||||
|
|
||||||
/**
|
|
||||||
* The CSRF token generator.
|
|
||||||
*
|
|
||||||
* @var \Drupal\Core\Access\CsrfTokenGenerator
|
|
||||||
*/
|
|
||||||
protected $csrfToken;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a RouteProcessorCsrf object.
|
* Constructs a RouteProcessorCsrf object.
|
||||||
*
|
*
|
||||||
* @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
|
* @param \Drupal\Core\Access\CsrfTokenGenerator $csrfToken
|
||||||
* The CSRF token generator.
|
* The CSRF token generator.
|
||||||
|
* @param \Symfony\Component\HttpFoundation\RequestStack|null $requestStack
|
||||||
|
* The request stack.
|
||||||
*/
|
*/
|
||||||
public function __construct(CsrfTokenGenerator $csrf_token) {
|
public function __construct(
|
||||||
$this->csrfToken = $csrf_token;
|
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
|
// Adding this to the parameters means it will get merged into the query
|
||||||
// string when the route is compiled.
|
// string when the route is compiled.
|
||||||
if (!$bubbleable_metadata) {
|
if (!$bubbleable_metadata || $this->requestStack->getCurrentRequest()->getRequestFormat() !== 'html') {
|
||||||
$parameters['token'] = $this->csrfToken->get($path);
|
$parameters['token'] = $this->csrfToken->get($path);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -51,7 +52,6 @@ class RouteProcessorCsrf implements OutboundRouteProcessorInterface, TrustedCall
|
||||||
$placeholder_render_array = [
|
$placeholder_render_array = [
|
||||||
'#lazy_builder' => ['route_processor_csrf:renderPlaceholderCsrfToken', [$path]],
|
'#lazy_builder' => ['route_processor_csrf:renderPlaceholderCsrfToken', [$path]],
|
||||||
];
|
];
|
||||||
|
|
||||||
// Instead of setting an actual CSRF token as the query string, we set
|
// 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
|
// the placeholder, which will be replaced at the very last moment. This
|
||||||
// ensures links with CSRF tokens don't break cacheability.
|
// ensures links with CSRF tokens don't break cacheability.
|
||||||
|
|
|
@ -103,6 +103,7 @@ class MenuLinksetRoutes extends RouteSubscriberBase implements ContainerInjectio
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'_access' => 'TRUE',
|
'_access' => 'TRUE',
|
||||||
|
'_format' => 'json',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'parameters' => [
|
'parameters' => [
|
||||||
|
|
|
@ -5,9 +5,12 @@ declare(strict_types=1);
|
||||||
namespace Drupal\Tests\Core\Access;
|
namespace Drupal\Tests\Core\Access;
|
||||||
|
|
||||||
use Drupal\Component\Utility\Crypt;
|
use Drupal\Component\Utility\Crypt;
|
||||||
|
use Drupal\Core\Access\CsrfTokenGenerator;
|
||||||
use Drupal\Core\Render\BubbleableMetadata;
|
use Drupal\Core\Render\BubbleableMetadata;
|
||||||
use Drupal\Tests\UnitTestCase;
|
use Drupal\Tests\UnitTestCase;
|
||||||
use Drupal\Core\Access\RouteProcessorCsrf;
|
use Drupal\Core\Access\RouteProcessorCsrf;
|
||||||
|
use PHPUnit\Framework\MockObject\MockObject;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
use Symfony\Component\Routing\Route;
|
use Symfony\Component\Routing\Route;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -18,17 +21,18 @@ class RouteProcessorCsrfTest extends UnitTestCase {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The mock CSRF token generator.
|
* 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.
|
* The route processor.
|
||||||
*
|
|
||||||
* @var \Drupal\Core\Access\RouteProcessorCsrf
|
|
||||||
*/
|
*/
|
||||||
protected $processor;
|
protected RouteProcessorCsrf $processor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
|
@ -40,7 +44,20 @@ class RouteProcessorCsrfTest extends UnitTestCase {
|
||||||
->disableOriginalConstructor()
|
->disableOriginalConstructor()
|
||||||
->getMock();
|
->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);
|
$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