feature #40441 [WebProfilerBundle] Disable CSP if dumper was used (monojp)

This PR was squashed before being merged into the 5.3-dev branch.

Discussion
----------

[WebProfilerBundle] Disable CSP if dumper was used

| Q             | A
| ------------- | ---
| Branch?       | 5.x for features
| Bug fix?      | no
| New feature?  | no
| Deprecations? | no
| Tickets       | Fix #29084
| License       | MIT
| Doc PR        | -

Disables a configured Content Security Policy if the dumper was used to avoid breaking the toolbar. This is a less invasive alternative to #29155 which fixes #29084 (there is a project with a test case linked there).

Commits
-------

5dc2637263 [WebProfilerBundle] Disable CSP if dumper was used
This commit is contained in:
Fabien Potencier 2021-03-12 07:08:17 +01:00
commit bbb4d9f26e
5 changed files with 61 additions and 2 deletions

View File

@ -16,6 +16,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Flash\AutoExpireFlashBag; use Symfony\Component\HttpFoundation\Session\Flash\AutoExpireFlashBag;
use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector;
use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
@ -44,8 +45,9 @@ class WebDebugToolbarListener implements EventSubscriberInterface
protected $mode; protected $mode;
protected $excludedAjaxPaths; protected $excludedAjaxPaths;
private $cspHandler; private $cspHandler;
private $dumpDataCollector;
public function __construct(Environment $twig, bool $interceptRedirects = false, int $mode = self::ENABLED, UrlGeneratorInterface $urlGenerator = null, string $excludedAjaxPaths = '^/bundles|^/_wdt', ContentSecurityPolicyHandler $cspHandler = null) public function __construct(Environment $twig, bool $interceptRedirects = false, int $mode = self::ENABLED, UrlGeneratorInterface $urlGenerator = null, string $excludedAjaxPaths = '^/bundles|^/_wdt', ContentSecurityPolicyHandler $cspHandler = null, DumpDataCollector $dumpDataCollector = null)
{ {
$this->twig = $twig; $this->twig = $twig;
$this->urlGenerator = $urlGenerator; $this->urlGenerator = $urlGenerator;
@ -53,6 +55,7 @@ class WebDebugToolbarListener implements EventSubscriberInterface
$this->mode = $mode; $this->mode = $mode;
$this->excludedAjaxPaths = $excludedAjaxPaths; $this->excludedAjaxPaths = $excludedAjaxPaths;
$this->cspHandler = $cspHandler; $this->cspHandler = $cspHandler;
$this->dumpDataCollector = $dumpDataCollector;
} }
public function isEnabled(): bool public function isEnabled(): bool
@ -89,7 +92,14 @@ class WebDebugToolbarListener implements EventSubscriberInterface
return; return;
} }
$nonces = $this->cspHandler ? $this->cspHandler->updateResponseHeaders($request, $response) : []; $nonces = [];
if ($this->cspHandler) {
if ($this->dumpDataCollector && $this->dumpDataCollector->getDumpsCount() > 0) {
$this->cspHandler->disableCsp();
}
$nonces = $this->cspHandler->updateResponseHeaders($request, $response);
}
// do not capture redirects or modify XML HTTP Requests // do not capture redirects or modify XML HTTP Requests
if ($request->isXmlHttpRequest()) { if ($request->isXmlHttpRequest()) {

View File

@ -24,6 +24,7 @@ return static function (ContainerConfigurator $container) {
service('router')->ignoreOnInvalid(), service('router')->ignoreOnInvalid(),
abstract_arg('paths that should be excluded from the AJAX requests shown in the toolbar'), abstract_arg('paths that should be excluded from the AJAX requests shown in the toolbar'),
service('web_profiler.csp.handler'), service('web_profiler.csp.handler'),
service('data_collector.dump')->ignoreOnInvalid(),
]) ])
->tag('kernel.event_subscriber') ->tag('kernel.event_subscriber')
; ;

View File

@ -20,6 +20,7 @@ use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector;
use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\HttpKernel\KernelInterface;
class WebProfilerExtensionTest extends TestCase class WebProfilerExtensionTest extends TestCase
@ -55,6 +56,7 @@ class WebProfilerExtensionTest extends TestCase
$this->kernel = $this->createMock(KernelInterface::class); $this->kernel = $this->createMock(KernelInterface::class);
$this->container = new ContainerBuilder(); $this->container = new ContainerBuilder();
$this->container->register('data_collector.dump', DumpDataCollector::class)->setPublic(true);
$this->container->register('error_handler.error_renderer.html', HtmlErrorRenderer::class)->setPublic(true); $this->container->register('error_handler.error_renderer.html', HtmlErrorRenderer::class)->setPublic(true);
$this->container->register('event_dispatcher', EventDispatcher::class)->setPublic(true); $this->container->register('event_dispatcher', EventDispatcher::class)->setPublic(true);
$this->container->register('router', $this->getMockClass('Symfony\\Component\\Routing\\RouterInterface'))->setPublic(true); $this->container->register('router', $this->getMockClass('Symfony\\Component\\Routing\\RouterInterface'))->setPublic(true);

View File

@ -12,11 +12,13 @@
namespace Symfony\Bundle\WebProfilerBundle\Tests\EventListener; namespace Symfony\Bundle\WebProfilerBundle\Tests\EventListener;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler;
use Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener; use Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener;
use Symfony\Component\HttpFoundation\HeaderBag; use Symfony\Component\HttpFoundation\HeaderBag;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector;
use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\Kernel;
@ -300,6 +302,48 @@ class WebDebugToolbarListenerTest extends TestCase
$this->assertEquals('Exception: This multiline tabbed text should come out on a single plain line', $response->headers->get('X-Debug-Error')); $this->assertEquals('Exception: This multiline tabbed text should come out on a single plain line', $response->headers->get('X-Debug-Error'));
} }
public function testCspIsDisabledIfDumperWasUsed()
{
$response = new Response('<html><head></head><body></body></html>');
$response->headers->set('X-Debug-Token', 'xxxxxxxx');
$event = new ResponseEvent($this->createMock(Kernel::class), $this->getRequestMock(), HttpKernelInterface::MASTER_REQUEST, $response);
$cspHandler = $this->createMock(ContentSecurityPolicyHandler::class);
$cspHandler->expects($this->once())
->method('disableCsp');
$dumpDataCollector = $this->createMock(DumpDataCollector::class);
$dumpDataCollector->expects($this->once())
->method('getDumpsCount')
->willReturn(1);
$listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, null, '', $cspHandler, $dumpDataCollector);
$listener->onKernelResponse($event);
$this->assertEquals("<html><head></head><body>\nWDT\n</body></html>", $response->getContent());
}
public function testCspIsKeptEnabledIfDumperWasNotUsed()
{
$response = new Response('<html><head></head><body></body></html>');
$response->headers->set('X-Debug-Token', 'xxxxxxxx');
$event = new ResponseEvent($this->createMock(Kernel::class), $this->getRequestMock(), HttpKernelInterface::MASTER_REQUEST, $response);
$cspHandler = $this->createMock(ContentSecurityPolicyHandler::class);
$cspHandler->expects($this->never())
->method('disableCsp');
$dumpDataCollector = $this->createMock(DumpDataCollector::class);
$dumpDataCollector->expects($this->once())
->method('getDumpsCount')
->willReturn(0);
$listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, null, '', $cspHandler, $dumpDataCollector);
$listener->onKernelResponse($event);
$this->assertEquals("<html><head></head><body>\nWDT\n</body></html>", $response->getContent());
}
protected function getRequestMock($isXmlHttpRequest = false, $requestFormat = 'html', $hasSession = true) protected function getRequestMock($isXmlHttpRequest = false, $requestFormat = 'html', $hasSession = true)
{ {
$request = $this->getMockBuilder(Request::class)->setMethods(['getSession', 'isXmlHttpRequest', 'getRequestFormat'])->disableOriginalConstructor()->getMock(); $request = $this->getMockBuilder(Request::class)->setMethods(['getSession', 'isXmlHttpRequest', 'getRequestFormat'])->disableOriginalConstructor()->getMock();

View File

@ -10,6 +10,7 @@ use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle;
use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector;
use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
@ -65,6 +66,7 @@ class WebProfilerBundleKernel extends Kernel
protected function build(ContainerBuilder $container) protected function build(ContainerBuilder $container)
{ {
$container->register('data_collector.dump', DumpDataCollector::class);
$container->register('logger', NullLogger::class); $container->register('logger', NullLogger::class);
} }