feature #18568 [3.2][WebProfilerBundle] Fix bundle usage in Content-Security-Policy context without unsafe-inline (romainneutron)
This PR was merged into the 3.2-dev branch.
Discussion
----------
[3.2][WebProfilerBundle] Fix bundle usage in Content-Security-Policy context without unsafe-inline
| Q | A
| ------------- | ---
| Branch? | 3.2
| Bug fix? | yes
| New feature? | yes
| BC breaks? | no
| Deprecations? | no
| Tests pass? | yes
| Fixed tickets | #15397
| License | MIT
| Doc PR | N/A
Hello, this PR fixes the compatibility of the WebprofilerBundle in a context where Content-Security-Policy headers are could prevent `unsafe-inline` of `script-src` or `style-src` directives.
This PR has been originally proposed in 2.8 in #18434
Commits
-------
571a1f2
[WebProfilerBundle] Fix bundle usage in Content-Security-Policy context without unsafe-inline
This commit is contained in:
commit
856c9f6024
@ -27,7 +27,6 @@
|
|||||||
{{ dump.data|raw }}
|
{{ dump.data|raw }}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" onload="var h = this.parentNode.innerHTML, rx=/<script>(.*?)<\/script>/g, s; while (s = rx.exec(h)) {eval(s[1]);};" />
|
|
||||||
{% endset %}
|
{% endset %}
|
||||||
|
|
||||||
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { 'link': true }) }}
|
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { 'link': true }) }}
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
namespace Symfony\Bundle\WebProfilerBundle\Controller;
|
namespace Symfony\Bundle\WebProfilerBundle\Controller;
|
||||||
|
|
||||||
|
use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler;
|
||||||
use Symfony\Bundle\WebProfilerBundle\Profiler\TemplateManager;
|
use Symfony\Bundle\WebProfilerBundle\Profiler\TemplateManager;
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
@ -33,6 +34,7 @@ class ProfilerController
|
|||||||
private $twig;
|
private $twig;
|
||||||
private $templates;
|
private $templates;
|
||||||
private $toolbarPosition;
|
private $toolbarPosition;
|
||||||
|
private $cspHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
@ -43,13 +45,14 @@ class ProfilerController
|
|||||||
* @param array $templates The templates
|
* @param array $templates The templates
|
||||||
* @param string $toolbarPosition The toolbar position (top, bottom, normal, or null -- use the configuration)
|
* @param string $toolbarPosition The toolbar position (top, bottom, normal, or null -- use the configuration)
|
||||||
*/
|
*/
|
||||||
public function __construct(UrlGeneratorInterface $generator, Profiler $profiler = null, \Twig_Environment $twig, array $templates, $toolbarPosition = 'normal')
|
public function __construct(UrlGeneratorInterface $generator, Profiler $profiler = null, \Twig_Environment $twig, array $templates, $toolbarPosition = 'normal', ContentSecurityPolicyHandler $cspHandler = null)
|
||||||
{
|
{
|
||||||
$this->generator = $generator;
|
$this->generator = $generator;
|
||||||
$this->profiler = $profiler;
|
$this->profiler = $profiler;
|
||||||
$this->twig = $twig;
|
$this->twig = $twig;
|
||||||
$this->templates = $templates;
|
$this->templates = $templates;
|
||||||
$this->toolbarPosition = $toolbarPosition;
|
$this->toolbarPosition = $toolbarPosition;
|
||||||
|
$this->cspHandler = $cspHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -88,6 +91,10 @@ class ProfilerController
|
|||||||
|
|
||||||
$this->profiler->disable();
|
$this->profiler->disable();
|
||||||
|
|
||||||
|
if (null !== $this->cspHandler) {
|
||||||
|
$this->cspHandler->disableCsp();
|
||||||
|
}
|
||||||
|
|
||||||
$panel = $request->query->get('panel', 'request');
|
$panel = $request->query->get('panel', 'request');
|
||||||
$page = $request->query->get('page', 'home');
|
$page = $request->query->get('page', 'home');
|
||||||
|
|
||||||
@ -134,6 +141,10 @@ class ProfilerController
|
|||||||
|
|
||||||
$this->profiler->disable();
|
$this->profiler->disable();
|
||||||
|
|
||||||
|
if (null !== $this->cspHandler) {
|
||||||
|
$this->cspHandler->disableCsp();
|
||||||
|
}
|
||||||
|
|
||||||
return new Response($this->twig->render('@WebProfiler/Profiler/info.html.twig', array(
|
return new Response($this->twig->render('@WebProfiler/Profiler/info.html.twig', array(
|
||||||
'about' => $about,
|
'about' => $about,
|
||||||
'request' => $request,
|
'request' => $request,
|
||||||
@ -185,7 +196,7 @@ class ProfilerController
|
|||||||
// the profiler is not enabled
|
// the profiler is not enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response($this->twig->render('@WebProfiler/Profiler/toolbar.html.twig', array(
|
return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/toolbar.html.twig', array(
|
||||||
'request' => $request,
|
'request' => $request,
|
||||||
'position' => $position,
|
'position' => $position,
|
||||||
'profile' => $profile,
|
'profile' => $profile,
|
||||||
@ -193,7 +204,7 @@ class ProfilerController
|
|||||||
'profiler_url' => $url,
|
'profiler_url' => $url,
|
||||||
'token' => $token,
|
'token' => $token,
|
||||||
'profiler_markup_version' => 2, // 1 = original toolbar, 2 = Symfony 2.8+ toolbar
|
'profiler_markup_version' => 2, // 1 = original toolbar, 2 = Symfony 2.8+ toolbar
|
||||||
)), 200, array('Content-Type' => 'text/html'));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -213,6 +224,10 @@ class ProfilerController
|
|||||||
|
|
||||||
$this->profiler->disable();
|
$this->profiler->disable();
|
||||||
|
|
||||||
|
if (null !== $this->cspHandler) {
|
||||||
|
$this->cspHandler->disableCsp();
|
||||||
|
}
|
||||||
|
|
||||||
if (null === $session = $request->getSession()) {
|
if (null === $session = $request->getSession()) {
|
||||||
$ip =
|
$ip =
|
||||||
$method =
|
$method =
|
||||||
@ -268,6 +283,10 @@ class ProfilerController
|
|||||||
|
|
||||||
$this->profiler->disable();
|
$this->profiler->disable();
|
||||||
|
|
||||||
|
if (null !== $this->cspHandler) {
|
||||||
|
$this->cspHandler->disableCsp();
|
||||||
|
}
|
||||||
|
|
||||||
$profile = $this->profiler->loadProfile($token);
|
$profile = $this->profiler->loadProfile($token);
|
||||||
|
|
||||||
$ip = $request->query->get('ip');
|
$ip = $request->query->get('ip');
|
||||||
@ -364,6 +383,10 @@ class ProfilerController
|
|||||||
|
|
||||||
$this->profiler->disable();
|
$this->profiler->disable();
|
||||||
|
|
||||||
|
if (null !== $this->cspHandler) {
|
||||||
|
$this->cspHandler->disableCsp();
|
||||||
|
}
|
||||||
|
|
||||||
ob_start();
|
ob_start();
|
||||||
phpinfo();
|
phpinfo();
|
||||||
$phpinfo = ob_get_clean();
|
$phpinfo = ob_get_clean();
|
||||||
@ -384,4 +407,18 @@ class ProfilerController
|
|||||||
|
|
||||||
return $this->templateManager;
|
return $this->templateManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function renderWithCspNonces(Request $request, $template, $variables, $code = 200, $headers = array('Content-Type' => 'text/html'))
|
||||||
|
{
|
||||||
|
$response = new Response('', $code, $headers);
|
||||||
|
|
||||||
|
$nonces = $this->cspHandler ? $this->cspHandler->getNonces($request, $response) : array();
|
||||||
|
|
||||||
|
$variables['csp_script_nonce'] = isset($nonces['csp_script_nonce']) ? $nonces['csp_script_nonce'] : null;
|
||||||
|
$variables['csp_style_nonce'] = isset($nonces['csp_style_nonce']) ? $nonces['csp_style_nonce'] : null;
|
||||||
|
|
||||||
|
$response->setContent($this->twig->render($template, $variables));
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,265 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Bundle\WebProfilerBundle\Csp;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles Content-Security-Policy HTTP header for the WebProfiler Bundle.
|
||||||
|
*
|
||||||
|
* @author Romain Neutron <imprec@gmail.com>
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class ContentSecurityPolicyHandler
|
||||||
|
{
|
||||||
|
private $nonceGenerator;
|
||||||
|
private $cspDisabled = false;
|
||||||
|
|
||||||
|
public function __construct(NonceGenerator $nonceGenerator)
|
||||||
|
{
|
||||||
|
$this->nonceGenerator = $nonceGenerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of nonces to be used in Twig templates and Content-Security-Policy headers.
|
||||||
|
*
|
||||||
|
* Nonce can be provided by;
|
||||||
|
* - The request - In case HTML content is fetched via AJAX and inserted in DOM, it must use the same nonce as origin
|
||||||
|
* - The response - A call to getNonces() has already been done previously. Same nonce are returned
|
||||||
|
* - They are otherwise randomly generated
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getNonces(Request $request, Response $response)
|
||||||
|
{
|
||||||
|
if ($request->headers->has('X-SymfonyProfiler-Script-Nonce') && $request->headers->has('X-SymfonyProfiler-Style-Nonce')) {
|
||||||
|
return array(
|
||||||
|
'csp_script_nonce' => $request->headers->get('X-SymfonyProfiler-Script-Nonce'),
|
||||||
|
'csp_style_nonce' => $request->headers->get('X-SymfonyProfiler-Style-Nonce'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($response->headers->has('X-SymfonyProfiler-Script-Nonce') && $response->headers->has('X-SymfonyProfiler-Style-Nonce')) {
|
||||||
|
return array(
|
||||||
|
'csp_script_nonce' => $response->headers->get('X-SymfonyProfiler-Script-Nonce'),
|
||||||
|
'csp_style_nonce' => $response->headers->get('X-SymfonyProfiler-Style-Nonce'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$nonces = array(
|
||||||
|
'csp_script_nonce' => $this->generateNonce(),
|
||||||
|
'csp_style_nonce' => $this->generateNonce(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$response->headers->set('X-SymfonyProfiler-Script-Nonce', $nonces['csp_script_nonce']);
|
||||||
|
$response->headers->set('X-SymfonyProfiler-Style-Nonce', $nonces['csp_style_nonce']);
|
||||||
|
|
||||||
|
return $nonces;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables Content-Security-Policy.
|
||||||
|
*
|
||||||
|
* All related headers will be removed.
|
||||||
|
*/
|
||||||
|
public function disableCsp()
|
||||||
|
{
|
||||||
|
$this->cspDisabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup temporary headers and updates Content-Security-Policy headers.
|
||||||
|
*
|
||||||
|
* @return array Nonces used by the bundle in Content-Security-Policy header
|
||||||
|
*/
|
||||||
|
public function updateResponseHeaders(Request $request, Response $response)
|
||||||
|
{
|
||||||
|
if ($this->cspDisabled) {
|
||||||
|
$this->removeCspHeaders($response);
|
||||||
|
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$nonces = $this->getNonces($request, $response);
|
||||||
|
$this->cleanHeaders($response);
|
||||||
|
$this->updateCspHeaders($response, $nonces);
|
||||||
|
|
||||||
|
return $nonces;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function cleanHeaders(Response $response)
|
||||||
|
{
|
||||||
|
$response->headers->remove('X-SymfonyProfiler-Script-Nonce');
|
||||||
|
$response->headers->remove('X-SymfonyProfiler-Style-Nonce');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function removeCspHeaders(Response $response)
|
||||||
|
{
|
||||||
|
$response->headers->remove('X-Content-Security-Policy');
|
||||||
|
$response->headers->remove('Content-Security-Policy');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates Content-Security-Policy headers in a response.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function updateCspHeaders(Response $response, array $nonces = array())
|
||||||
|
{
|
||||||
|
$nonces = array_replace(array(
|
||||||
|
'csp_script_nonce' => $this->generateNonce(),
|
||||||
|
'csp_style_nonce' => $this->generateNonce(),
|
||||||
|
), $nonces);
|
||||||
|
|
||||||
|
$ruleIsSet = false;
|
||||||
|
|
||||||
|
$headers = $this->getCspHeaders($response);
|
||||||
|
|
||||||
|
foreach ($headers as $header => $directives) {
|
||||||
|
foreach (array('script-src' => 'csp_script_nonce', 'style-src' => 'csp_style_nonce') as $type => $tokenName) {
|
||||||
|
if ($this->authorizesInline($directives, $type)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!isset($headers[$header][$type])) {
|
||||||
|
if (isset($headers[$header]['default-src'])) {
|
||||||
|
$headers[$header][$type] = $headers[$header]['default-src'];
|
||||||
|
} else {
|
||||||
|
$headers[$header][$type] = array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$ruleIsSet = true;
|
||||||
|
if (!in_array('\'unsafe-inline\'', $headers[$header][$type], true)) {
|
||||||
|
$headers[$header][$type][] = '\'unsafe-inline\'';
|
||||||
|
}
|
||||||
|
$headers[$header][$type][] = sprintf('\'nonce-%s\'', $nonces[$tokenName]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$ruleIsSet) {
|
||||||
|
return $nonces;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($headers as $header => $directives) {
|
||||||
|
$response->headers->set($header, $this->generateCspHeader($directives));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $nonces;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a valid Content-Security-Policy nonce.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function generateNonce()
|
||||||
|
{
|
||||||
|
return $this->nonceGenerator->generate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a directive set array into Content-Security-Policy header.
|
||||||
|
*
|
||||||
|
* @param array $directives The directive set
|
||||||
|
*
|
||||||
|
* @return string The Content-Security-Policy header
|
||||||
|
*/
|
||||||
|
private function generateCspHeader(array $directives)
|
||||||
|
{
|
||||||
|
return array_reduce(array_keys($directives), function ($res, $name) use ($directives) {
|
||||||
|
return ($res !== '' ? $res.'; ' : '').sprintf('%s %s', $name, implode(' ', $directives[$name]));
|
||||||
|
}, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Content-Security-Policy header value into a directive set array.
|
||||||
|
*
|
||||||
|
* @param string $header The header value
|
||||||
|
*
|
||||||
|
* @return array The directive set
|
||||||
|
*/
|
||||||
|
private function parseDirectives($header)
|
||||||
|
{
|
||||||
|
$directives = array();
|
||||||
|
|
||||||
|
foreach (explode(';', $header) as $directive) {
|
||||||
|
$parts = explode(' ', trim($directive));
|
||||||
|
if (count($parts) < 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$name = array_shift($parts);
|
||||||
|
$directives[$name] = $parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $directives;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects if the 'unsafe-inline' is prevented for a directive within the directive set.
|
||||||
|
*
|
||||||
|
* @param array $directivesSet The directive set
|
||||||
|
* @param string $type The name of the directive to check
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function authorizesInline(array $directivesSet, $type)
|
||||||
|
{
|
||||||
|
if (isset($directivesSet[$type])) {
|
||||||
|
$directives = $directivesSet[$type];
|
||||||
|
} elseif (isset($directivesSet['default-src'])) {
|
||||||
|
$directives = $directivesSet['default-src'];
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array('\'unsafe-inline\'', $directives, true) && !$this->hasHashOrNonce($directives);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hasHashOrNonce(array $directives)
|
||||||
|
{
|
||||||
|
foreach ($directives as $directive) {
|
||||||
|
if ('\'' !== substr($directive, -1)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ('\'nonce-' === substr($directive, 0, 7)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (in_array(substr($directive, 0, 8), array('\'sha256-', '\'sha384-', '\'sha512-'), true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the Content-Security-Policy headers (either X-Content-Security-Policy or Content-Security-Policy) from
|
||||||
|
* a response.
|
||||||
|
*
|
||||||
|
* @return array An associative array of headers
|
||||||
|
*/
|
||||||
|
private function getCspHeaders(Response $response)
|
||||||
|
{
|
||||||
|
$headers = array();
|
||||||
|
|
||||||
|
if ($response->headers->has('Content-Security-Policy')) {
|
||||||
|
$headers['Content-Security-Policy'] = $this->parseDirectives($response->headers->get('Content-Security-Policy'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($response->headers->has('X-Content-Security-Policy')) {
|
||||||
|
$headers['X-Content-Security-Policy'] = $this->parseDirectives($response->headers->get('X-Content-Security-Policy'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $headers;
|
||||||
|
}
|
||||||
|
}
|
27
src/Symfony/Bundle/WebProfilerBundle/Csp/NonceGenerator.php
Normal file
27
src/Symfony/Bundle/WebProfilerBundle/Csp/NonceGenerator.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Bundle\WebProfilerBundle\Csp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates Content-Security-Policy nonce.
|
||||||
|
*
|
||||||
|
* @author Romain Neutron <imprec@gmail.com>
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class NonceGenerator
|
||||||
|
{
|
||||||
|
public function generate()
|
||||||
|
{
|
||||||
|
return bin2hex(random_bytes(16));
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
namespace Symfony\Bundle\WebProfilerBundle\EventListener;
|
namespace Symfony\Bundle\WebProfilerBundle\EventListener;
|
||||||
|
|
||||||
|
use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler;
|
||||||
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;
|
||||||
@ -40,8 +41,9 @@ class WebDebugToolbarListener implements EventSubscriberInterface
|
|||||||
protected $mode;
|
protected $mode;
|
||||||
protected $position;
|
protected $position;
|
||||||
protected $excludedAjaxPaths;
|
protected $excludedAjaxPaths;
|
||||||
|
private $cspHandler;
|
||||||
|
|
||||||
public function __construct(\Twig_Environment $twig, $interceptRedirects = false, $mode = self::ENABLED, $position = 'bottom', UrlGeneratorInterface $urlGenerator = null, $excludedAjaxPaths = '^/bundles|^/_wdt')
|
public function __construct(\Twig_Environment $twig, $interceptRedirects = false, $mode = self::ENABLED, $position = 'bottom', UrlGeneratorInterface $urlGenerator = null, $excludedAjaxPaths = '^/bundles|^/_wdt', ContentSecurityPolicyHandler $cspHandler = null)
|
||||||
{
|
{
|
||||||
$this->twig = $twig;
|
$this->twig = $twig;
|
||||||
$this->urlGenerator = $urlGenerator;
|
$this->urlGenerator = $urlGenerator;
|
||||||
@ -49,6 +51,7 @@ class WebDebugToolbarListener implements EventSubscriberInterface
|
|||||||
$this->mode = (int) $mode;
|
$this->mode = (int) $mode;
|
||||||
$this->position = $position;
|
$this->position = $position;
|
||||||
$this->excludedAjaxPaths = $excludedAjaxPaths;
|
$this->excludedAjaxPaths = $excludedAjaxPaths;
|
||||||
|
$this->cspHandler = $cspHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isEnabled()
|
public function isEnabled()
|
||||||
@ -76,6 +79,8 @@ class WebDebugToolbarListener implements EventSubscriberInterface
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$nonces = $this->cspHandler ? $this->cspHandler->updateResponseHeaders($request, $response) : array();
|
||||||
|
|
||||||
// do not capture redirects or modify XML HTTP Requests
|
// do not capture redirects or modify XML HTTP Requests
|
||||||
if ($request->isXmlHttpRequest()) {
|
if ($request->isXmlHttpRequest()) {
|
||||||
return;
|
return;
|
||||||
@ -102,7 +107,7 @@ class WebDebugToolbarListener implements EventSubscriberInterface
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->injectToolbar($response, $request);
|
$this->injectToolbar($response, $request, $nonces);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -110,7 +115,7 @@ class WebDebugToolbarListener implements EventSubscriberInterface
|
|||||||
*
|
*
|
||||||
* @param Response $response A Response instance
|
* @param Response $response A Response instance
|
||||||
*/
|
*/
|
||||||
protected function injectToolbar(Response $response, Request $request)
|
protected function injectToolbar(Response $response, Request $request, array $nonces)
|
||||||
{
|
{
|
||||||
$content = $response->getContent();
|
$content = $response->getContent();
|
||||||
$pos = strripos($content, '</body>');
|
$pos = strripos($content, '</body>');
|
||||||
@ -123,6 +128,8 @@ class WebDebugToolbarListener implements EventSubscriberInterface
|
|||||||
'excluded_ajax_paths' => $this->excludedAjaxPaths,
|
'excluded_ajax_paths' => $this->excludedAjaxPaths,
|
||||||
'token' => $response->headers->get('X-Debug-Token'),
|
'token' => $response->headers->get('X-Debug-Token'),
|
||||||
'request' => $request,
|
'request' => $request,
|
||||||
|
'csp_script_nonce' => isset($nonces['csp_script_nonce']) ? $nonces['csp_script_nonce'] : null,
|
||||||
|
'csp_style_nonce' => isset($nonces['csp_style_nonce']) ? $nonces['csp_style_nonce'] : null,
|
||||||
)
|
)
|
||||||
))."\n";
|
))."\n";
|
||||||
$content = substr($content, 0, $pos).$toolbar.substr($content, $pos);
|
$content = substr($content, 0, $pos).$toolbar.substr($content, $pos);
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
<argument type="service" id="twig" />
|
<argument type="service" id="twig" />
|
||||||
<argument>%data_collector.templates%</argument>
|
<argument>%data_collector.templates%</argument>
|
||||||
<argument>%web_profiler.debug_toolbar.position%</argument>
|
<argument>%web_profiler.debug_toolbar.position%</argument>
|
||||||
|
<argument type="service" id="web_profiler.csp.handler" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service id="web_profiler.controller.router" class="Symfony\Bundle\WebProfilerBundle\Controller\RouterController">
|
<service id="web_profiler.controller.router" class="Symfony\Bundle\WebProfilerBundle\Controller\RouterController">
|
||||||
@ -25,6 +26,12 @@
|
|||||||
<argument>%kernel.debug%</argument>
|
<argument>%kernel.debug%</argument>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<service id="web_profiler.csp.handler" class="Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler" public="false">
|
||||||
|
<argument type="service">
|
||||||
|
<service class="Symfony\Bundle\WebProfilerBundle\Csp\NonceGenerator" />
|
||||||
|
</argument>
|
||||||
|
</service>
|
||||||
|
|
||||||
<service id="twig.extension.webprofiler" class="Symfony\Bundle\WebProfilerBundle\Twig\WebProfilerExtension" public="false">
|
<service id="twig.extension.webprofiler" class="Symfony\Bundle\WebProfilerBundle\Twig\WebProfilerExtension" public="false">
|
||||||
<tag name="twig.extension" />
|
<tag name="twig.extension" />
|
||||||
</service>
|
</service>
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
<argument>%web_profiler.debug_toolbar.position%</argument>
|
<argument>%web_profiler.debug_toolbar.position%</argument>
|
||||||
<argument type="service" id="router" on-invalid="ignore" />
|
<argument type="service" id="router" on-invalid="ignore" />
|
||||||
<argument /> <!-- paths that should be excluded from the AJAX requests shown in the toolbar -->
|
<argument /> <!-- paths that should be excluded from the AJAX requests shown in the toolbar -->
|
||||||
|
<argument type="service" id="web_profiler.csp.handler" />
|
||||||
</service>
|
</service>
|
||||||
</services>
|
</services>
|
||||||
</container>
|
</container>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<script>/*<![CDATA[*/
|
<script{% if csp_script_nonce is defined and csp_script_nonce %} nonce={{ csp_script_nonce }}{% endif %}>/*<![CDATA[*/
|
||||||
{# Caution: the contents of this file are processed by Twig before loading
|
{# Caution: the contents of this file are processed by Twig before loading
|
||||||
them as JavaScript source code. Always use '/*' comments instead
|
them as JavaScript source code. Always use '/*' comments instead
|
||||||
of '//' comments to avoid impossible-to-debug side-effects #}
|
of '//' comments to avoid impossible-to-debug side-effects #}
|
||||||
|
@ -94,7 +94,7 @@
|
|||||||
<div id="sidebar">
|
<div id="sidebar">
|
||||||
<div id="sidebar-shortcuts">
|
<div id="sidebar-shortcuts">
|
||||||
<div class="shortcuts">
|
<div class="shortcuts">
|
||||||
<a href="#" class="visible-small" onclick="Sfjs.toggleClass(document.getElementById('sidebar'), 'expanded'); return false;">
|
<a href="#" id="sidebarShortcutsMenu" class="visible-small">
|
||||||
<span class="icon">{{ include('@WebProfiler/Icon/menu.svg') }}</span>
|
<span class="icon">{{ include('@WebProfiler/Icon/menu.svg') }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@ -124,4 +124,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
Sfjs.addEventListener(document.getElementById('sidebarShortcutsMenu'), 'click', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
Sfjs.toggleClass(document.getElementById('sidebar'), 'expanded');
|
||||||
|
})
|
||||||
|
}())
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -26,6 +26,15 @@
|
|||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sf-toolbar-clearer {
|
||||||
|
clear: both;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-display-none {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.sf-toolbarreset * {
|
.sf-toolbarreset * {
|
||||||
-webkit-box-sizing: content-box;
|
-webkit-box-sizing: content-box;
|
||||||
-moz-box-sizing: content-box;
|
-moz-box-sizing: content-box;
|
||||||
|
@ -1,27 +1,14 @@
|
|||||||
<!-- START of Symfony Web Debug Toolbar -->
|
<!-- START of Symfony Web Debug Toolbar -->
|
||||||
{% if 'normal' != position %}
|
{% if 'normal' != position %}
|
||||||
<div id="sfMiniToolbar-{{ token }}" class="sf-minitoolbar" data-no-turbolink>
|
<div id="sfMiniToolbar-{{ token }}" class="sf-minitoolbar" data-no-turbolink>
|
||||||
<a href="javascript:void(0);" title="Show Symfony toolbar" tabindex="-1" accesskey="D" onclick="
|
<a href="#" title="Show Symfony toolbar" tabindex="-1" id="sfToolbarMiniToggler-{{ token }}" accesskey="D">
|
||||||
var elem = this.parentNode;
|
|
||||||
if (elem.style.display == 'none') {
|
|
||||||
document.getElementById('sfToolbarMainContent-{{ token }}').style.display = 'none';
|
|
||||||
document.getElementById('sfToolbarClearer-{{ token }}').style.display = 'none';
|
|
||||||
elem.style.display = 'block';
|
|
||||||
} else {
|
|
||||||
document.getElementById('sfToolbarMainContent-{{ token }}').style.display = 'block';
|
|
||||||
document.getElementById('sfToolbarClearer-{{ token }}').style.display = 'block';
|
|
||||||
elem.style.display = 'none'
|
|
||||||
}
|
|
||||||
|
|
||||||
Sfjs.setPreference('toolbar/displayState', 'block');
|
|
||||||
">
|
|
||||||
{{ include('@WebProfiler/Icon/symfony.svg') }}
|
{{ include('@WebProfiler/Icon/symfony.svg') }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style{% if csp_style_nonce %} nonce="{{ csp_style_nonce }}"{% endif %}>
|
||||||
{{ include('@WebProfiler/Profiler/toolbar.css.twig', { 'position': position, 'floatable': true }) }}
|
{{ include('@WebProfiler/Profiler/toolbar.css.twig', { 'position': position, 'floatable': true }) }}
|
||||||
</style>
|
</style>
|
||||||
<div id="sfToolbarClearer-{{ token }}" style="clear: both; height: 36px;"></div>
|
<div id="sfToolbarClearer-{{ token }}" class="sf-toolbar-clearer"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div id="sfToolbarMainContent-{{ token }}" class="sf-toolbarreset clear-fix" data-no-turbolink>
|
<div id="sfToolbarMainContent-{{ token }}" class="sf-toolbarreset clear-fix" data-no-turbolink>
|
||||||
@ -31,19 +18,15 @@
|
|||||||
'profiler_url': profiler_url,
|
'profiler_url': profiler_url,
|
||||||
'token': profile.token,
|
'token': profile.token,
|
||||||
'name': name,
|
'name': name,
|
||||||
'profiler_markup_version': profiler_markup_version
|
'profiler_markup_version': profiler_markup_version,
|
||||||
|
'csp_script_nonce': csp_script_nonce,
|
||||||
|
'csp_style_nonce': csp_style_nonce
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if 'normal' != position %}
|
{% if 'normal' != position %}
|
||||||
<a class="hide-button" title="Close Toolbar" tabindex="-1" accesskey="D" onclick="
|
<a class="hide-button" id="sfToolbarHideButton-{{ token }}" title="Close Toolbar" tabindex="-1" accesskey="D">
|
||||||
var p = this.parentNode;
|
|
||||||
p.style.display = 'none';
|
|
||||||
(p.previousElementSibling || p.previousSibling).style.display = 'none';
|
|
||||||
document.getElementById('sfMiniToolbar-{{ token }}').style.display = 'block';
|
|
||||||
Sfjs.setPreference('toolbar/displayState', 'none');
|
|
||||||
">
|
|
||||||
{{ include('@WebProfiler/Icon/close.svg') }}
|
{{ include('@WebProfiler/Icon/close.svg') }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div id="sfwdt{{ token }}" class="sf-toolbar" style="display: none"></div>
|
<div id="sfwdt{{ token }}" class="sf-toolbar sf-display-none"></div>
|
||||||
{{ include('@WebProfiler/Profiler/base_js.html.twig') }}
|
{{ include('@WebProfiler/Profiler/base_js.html.twig') }}
|
||||||
<script>/*<![CDATA[*/
|
<script{% if csp_script_nonce %} nonce={{ csp_script_nonce }}{% endif %}>/*<![CDATA[*/
|
||||||
(function () {
|
(function () {
|
||||||
{% if 'top' == position %}
|
{% if 'top' == position %}
|
||||||
var sfwdt = document.getElementById('sfwdt{{ token }}');
|
var sfwdt = document.getElementById('sfwdt{{ token }}');
|
||||||
@ -14,6 +14,11 @@
|
|||||||
'sfwdt{{ token }}',
|
'sfwdt{{ token }}',
|
||||||
'{{ path("_wdt", { "token": token }) }}',
|
'{{ path("_wdt", { "token": token }) }}',
|
||||||
function(xhr, el) {
|
function(xhr, el) {
|
||||||
|
|
||||||
|
/* Evaluate embedded scripts inside the toolbar */
|
||||||
|
var rx=/<script>(.*?)<\/script>/g, s;
|
||||||
|
while (s = rx.exec(el.innerHTML)) {eval(s[1]);};
|
||||||
|
|
||||||
el.style.display = -1 !== xhr.responseText.indexOf('sf-toolbarreset') ? 'block' : 'none';
|
el.style.display = -1 !== xhr.responseText.indexOf('sf-toolbarreset') ? 'block' : 'none';
|
||||||
|
|
||||||
if (el.style.display == 'none') {
|
if (el.style.display == 'none') {
|
||||||
@ -58,13 +63,38 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
Sfjs.addEventListener(document.getElementById('sfToolbarHideButton-{{ token }}'), 'click', function () {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
var p = this.parentNode;
|
||||||
|
p.style.display = 'none';
|
||||||
|
(p.previousElementSibling || p.previousSibling).style.display = 'none';
|
||||||
|
document.getElementById('sfMiniToolbar-{{ token }}').style.display = 'block';
|
||||||
|
Sfjs.setPreference('toolbar/displayState', 'none');
|
||||||
|
});
|
||||||
|
Sfjs.addEventListener(document.getElementById('sfToolbarMiniToggler-{{ token }}'), 'click', function (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
var elem = this.parentNode;
|
||||||
|
if (elem.style.display == 'none') {
|
||||||
|
document.getElementById('sfToolbarMainContent-{{ token }}').style.display = 'none';
|
||||||
|
document.getElementById('sfToolbarClearer-{{ token }}').style.display = 'none';
|
||||||
|
elem.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
document.getElementById('sfToolbarMainContent-{{ token }}').style.display = 'block';
|
||||||
|
document.getElementById('sfToolbarClearer-{{ token }}').style.display = 'block';
|
||||||
|
elem.style.display = 'none'
|
||||||
|
}
|
||||||
|
|
||||||
|
Sfjs.setPreference('toolbar/displayState', 'block');
|
||||||
|
})
|
||||||
},
|
},
|
||||||
function(xhr) {
|
function(xhr) {
|
||||||
if (xhr.status !== 0) {
|
if (xhr.status !== 0) {
|
||||||
confirm('An error occurred while loading the web debug toolbar (' + xhr.status + ': ' + xhr.statusText + ').\n\nDo you want to open the profiler?') && (window.location = '{{ path("_profiler", { "token": token }) }}');
|
confirm('An error occurred while loading the web debug toolbar (' + xhr.status + ': ' + xhr.statusText + ').\n\nDo you want to open the profiler?') && (window.location = '{{ path("_profiler", { "token": token }) }}');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{'maxTries': 5}
|
{ maxTries: 5 }
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
/*]]>*/</script>
|
/*]]>*/</script>
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
namespace Symfony\Bundle\WebProfilerBundle\Tests\Controller;
|
namespace Symfony\Bundle\WebProfilerBundle\Tests\Controller;
|
||||||
|
|
||||||
use Symfony\Bundle\WebProfilerBundle\Controller\ProfilerController;
|
use Symfony\Bundle\WebProfilerBundle\Controller\ProfilerController;
|
||||||
|
use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler;
|
||||||
use Symfony\Component\HttpKernel\Profiler\Profile;
|
use Symfony\Component\HttpKernel\Profiler\Profile;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
@ -44,17 +45,17 @@ class ProfilerControllerTest extends \PHPUnit_Framework_TestCase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testReturns404onTokenNotFound()
|
/**
|
||||||
|
* @dataProvider provideCspVariants
|
||||||
|
*/
|
||||||
|
public function testReturns404onTokenNotFound($withCsp)
|
||||||
{
|
{
|
||||||
$urlGenerator = $this->getMock('Symfony\Component\Routing\Generator\UrlGeneratorInterface');
|
|
||||||
$twig = $this->getMockBuilder('Twig_Environment')->disableOriginalConstructor()->getMock();
|
$twig = $this->getMockBuilder('Twig_Environment')->disableOriginalConstructor()->getMock();
|
||||||
$profiler = $this
|
$profiler = $this
|
||||||
->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler')
|
->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler')
|
||||||
->disableOriginalConstructor()
|
->disableOriginalConstructor()
|
||||||
->getMock();
|
->getMock();
|
||||||
|
|
||||||
$controller = new ProfilerController($urlGenerator, $profiler, $twig, array());
|
|
||||||
|
|
||||||
$profiler
|
$profiler
|
||||||
->expects($this->exactly(2))
|
->expects($this->exactly(2))
|
||||||
->method('loadProfile')
|
->method('loadProfile')
|
||||||
@ -65,6 +66,8 @@ class ProfilerControllerTest extends \PHPUnit_Framework_TestCase
|
|||||||
}))
|
}))
|
||||||
;
|
;
|
||||||
|
|
||||||
|
$controller = $this->createController($profiler, $twig, $withCsp);
|
||||||
|
|
||||||
$response = $controller->toolbarAction(Request::create('/_wdt/found'), 'found');
|
$response = $controller->toolbarAction(Request::create('/_wdt/found'), 'found');
|
||||||
$this->assertEquals(200, $response->getStatusCode());
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
|
||||||
@ -72,16 +75,18 @@ class ProfilerControllerTest extends \PHPUnit_Framework_TestCase
|
|||||||
$this->assertEquals(404, $response->getStatusCode());
|
$this->assertEquals(404, $response->getStatusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSearchResult()
|
/**
|
||||||
|
* @dataProvider provideCspVariants
|
||||||
|
*/
|
||||||
|
public function testSearchResult($withCsp)
|
||||||
{
|
{
|
||||||
$urlGenerator = $this->getMock('Symfony\Component\Routing\Generator\UrlGeneratorInterface');
|
|
||||||
$twig = $this->getMockBuilder('Twig_Environment')->disableOriginalConstructor()->getMock();
|
$twig = $this->getMockBuilder('Twig_Environment')->disableOriginalConstructor()->getMock();
|
||||||
$profiler = $this
|
$profiler = $this
|
||||||
->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler')
|
->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler')
|
||||||
->disableOriginalConstructor()
|
->disableOriginalConstructor()
|
||||||
->getMock();
|
->getMock();
|
||||||
|
|
||||||
$controller = new ProfilerController($urlGenerator, $profiler, $twig, array());
|
$controller = $this->createController($profiler, $twig, $withCsp);
|
||||||
|
|
||||||
$tokens = array(
|
$tokens = array(
|
||||||
array(
|
array(
|
||||||
@ -135,4 +140,25 @@ class ProfilerControllerTest extends \PHPUnit_Framework_TestCase
|
|||||||
$response = $controller->searchResultsAction($request, 'empty');
|
$response = $controller->searchResultsAction($request, 'empty');
|
||||||
$this->assertEquals(200, $response->getStatusCode());
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function provideCspVariants()
|
||||||
|
{
|
||||||
|
return array(
|
||||||
|
array(true),
|
||||||
|
array(false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createController($profiler, $twig, $withCSP)
|
||||||
|
{
|
||||||
|
$urlGenerator = $this->getMock('Symfony\Component\Routing\Generator\UrlGeneratorInterface');
|
||||||
|
|
||||||
|
if ($withCSP) {
|
||||||
|
$nonceGenerator = $this->getMock('Symfony\Bundle\WebProfilerBundle\Csp\NonceGenerator');
|
||||||
|
|
||||||
|
return new ProfilerController($urlGenerator, $profiler, $twig, array(), 'normal', new ContentSecurityPolicyHandler($nonceGenerator));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ProfilerController($urlGenerator, $profiler, $twig, array(), 'normal');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,199 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Bundle\WebProfilerBundle\Tests\Csp;
|
||||||
|
|
||||||
|
use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class ContentSecurityPolicyHandlerTest extends \PHPUnit_Framework_TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @dataProvider provideRequestAndResponses
|
||||||
|
*/
|
||||||
|
public function testGetNonces($nonce, $expectedNonce, Request $request, Response $response)
|
||||||
|
{
|
||||||
|
$cspHandler = new ContentSecurityPolicyHandler($this->mockNonceGenerator($nonce));
|
||||||
|
|
||||||
|
$this->assertSame($expectedNonce, $cspHandler->getNonces($request, $response));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideRequestAndResponsesForOnKernelResponse
|
||||||
|
*/
|
||||||
|
public function testOnKernelResponse($nonce, $expectedNonce, Request $request, Response $response, array $expectedCsp)
|
||||||
|
{
|
||||||
|
$cspHandler = new ContentSecurityPolicyHandler($this->mockNonceGenerator($nonce));
|
||||||
|
|
||||||
|
$this->assertSame($expectedNonce, $cspHandler->updateResponseHeaders($request, $response));
|
||||||
|
|
||||||
|
$this->assertFalse($response->headers->has('X-SymfonyProfiler-Script-Nonce'));
|
||||||
|
$this->assertFalse($response->headers->has('X-SymfonyProfiler-Style-Nonce'));
|
||||||
|
|
||||||
|
foreach ($expectedCsp as $header => $value) {
|
||||||
|
$this->assertSame($value, $response->headers->get($header));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideRequestAndResponses()
|
||||||
|
{
|
||||||
|
$nonce = bin2hex(random_bytes(16));
|
||||||
|
|
||||||
|
$requestScriptNonce = 'request-with-headers-script-nonce';
|
||||||
|
$requestStyleNonce = 'request-with-headers-style-nonce';
|
||||||
|
|
||||||
|
$responseScriptNonce = 'response-with-headers-script-nonce';
|
||||||
|
$responseStyleNonce = 'response-with-headers-style-nonce';
|
||||||
|
|
||||||
|
$requestNonceHeaders = array(
|
||||||
|
'X-SymfonyProfiler-Script-Nonce' => $requestScriptNonce,
|
||||||
|
'X-SymfonyProfiler-Style-Nonce' => $requestStyleNonce,
|
||||||
|
);
|
||||||
|
$responseNonceHeaders = array(
|
||||||
|
'X-SymfonyProfiler-Script-Nonce' => $responseScriptNonce,
|
||||||
|
'X-SymfonyProfiler-Style-Nonce' => $responseStyleNonce,
|
||||||
|
);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
array($nonce, array('csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce), $this->createRequest(), $this->createResponse()),
|
||||||
|
array($nonce, array('csp_script_nonce' => $requestScriptNonce, 'csp_style_nonce' => $requestStyleNonce), $this->createRequest($requestNonceHeaders), $this->createResponse($responseNonceHeaders)),
|
||||||
|
array($nonce, array('csp_script_nonce' => $requestScriptNonce, 'csp_style_nonce' => $requestStyleNonce), $this->createRequest($requestNonceHeaders), $this->createResponse()),
|
||||||
|
array($nonce, array('csp_script_nonce' => $responseScriptNonce, 'csp_style_nonce' => $responseStyleNonce), $this->createRequest(), $this->createResponse($responseNonceHeaders)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideRequestAndResponsesForOnKernelResponse()
|
||||||
|
{
|
||||||
|
$nonce = bin2hex(random_bytes(16));
|
||||||
|
|
||||||
|
$requestScriptNonce = 'request-with-headers-script-nonce';
|
||||||
|
$requestStyleNonce = 'request-with-headers-style-nonce';
|
||||||
|
|
||||||
|
$responseScriptNonce = 'response-with-headers-script-nonce';
|
||||||
|
$responseStyleNonce = 'response-with-headers-style-nonce';
|
||||||
|
|
||||||
|
$requestNonceHeaders = array(
|
||||||
|
'X-SymfonyProfiler-Script-Nonce' => $requestScriptNonce,
|
||||||
|
'X-SymfonyProfiler-Style-Nonce' => $requestStyleNonce,
|
||||||
|
);
|
||||||
|
$responseNonceHeaders = array(
|
||||||
|
'X-SymfonyProfiler-Script-Nonce' => $responseScriptNonce,
|
||||||
|
'X-SymfonyProfiler-Style-Nonce' => $responseStyleNonce,
|
||||||
|
);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
array(
|
||||||
|
$nonce,
|
||||||
|
array('csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce),
|
||||||
|
$this->createRequest(),
|
||||||
|
$this->createResponse(),
|
||||||
|
array('Content-Security-Policy' => null, 'X-Content-Security-Policy' => null),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
$nonce, array('csp_script_nonce' => $requestScriptNonce, 'csp_style_nonce' => $requestStyleNonce),
|
||||||
|
$this->createRequest($requestNonceHeaders),
|
||||||
|
$this->createResponse($responseNonceHeaders),
|
||||||
|
array('Content-Security-Policy' => null, 'X-Content-Security-Policy' => null),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
$nonce,
|
||||||
|
array('csp_script_nonce' => $requestScriptNonce, 'csp_style_nonce' => $requestStyleNonce),
|
||||||
|
$this->createRequest($requestNonceHeaders),
|
||||||
|
$this->createResponse(),
|
||||||
|
array('Content-Security-Policy' => null, 'X-Content-Security-Policy' => null),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
$nonce,
|
||||||
|
array('csp_script_nonce' => $responseScriptNonce, 'csp_style_nonce' => $responseStyleNonce),
|
||||||
|
$this->createRequest(),
|
||||||
|
$this->createResponse($responseNonceHeaders),
|
||||||
|
array('Content-Security-Policy' => null, 'X-Content-Security-Policy' => null),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
$nonce,
|
||||||
|
array('csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce),
|
||||||
|
$this->createRequest(),
|
||||||
|
$this->createResponse(array('Content-Security-Policy' => 'default-src \'self\' domain.com; script-src \'self\' \'unsafe-inline\'')),
|
||||||
|
array('Content-Security-Policy' => 'default-src \'self\' domain.com; script-src \'self\' \'unsafe-inline\'; style-src \'self\' domain.com \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'X-Content-Security-Policy' => null),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
$nonce,
|
||||||
|
array('csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce),
|
||||||
|
$this->createRequest(),
|
||||||
|
$this->createResponse(array('Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\'')),
|
||||||
|
array('Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\'; style-src \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'X-Content-Security-Policy' => null),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
$nonce,
|
||||||
|
array('csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce),
|
||||||
|
$this->createRequest(),
|
||||||
|
$this->createResponse(array('Content-Security-Policy' => 'script-src \'self\'; style-src \'self\'')),
|
||||||
|
array('Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'; style-src \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'X-Content-Security-Policy' => null),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
$nonce,
|
||||||
|
array('csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce),
|
||||||
|
$this->createRequest(),
|
||||||
|
$this->createResponse(array('X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\'')),
|
||||||
|
array('X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\'; style-src \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'Content-Security-Policy' => null),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
$nonce,
|
||||||
|
array('csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce),
|
||||||
|
$this->createRequest(),
|
||||||
|
$this->createResponse(array('X-Content-Security-Policy' => 'script-src \'self\'')),
|
||||||
|
array('X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'; style-src \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'Content-Security-Policy' => null),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
$nonce,
|
||||||
|
array('csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce),
|
||||||
|
$this->createRequest(),
|
||||||
|
$this->createResponse(array('X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\' \'sha384-LALALALALAAL\'')),
|
||||||
|
array('X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\' \'sha384-LALALALALAAL\' \'nonce-'.$nonce.'\'; style-src \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'Content-Security-Policy' => null),
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
$nonce,
|
||||||
|
array('csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce),
|
||||||
|
$this->createRequest(),
|
||||||
|
$this->createResponse(array('Content-Security-Policy' => 'script-src \'self\'; style-src \'self\'', 'X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\'; style-src \'self\'')),
|
||||||
|
array('Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'; style-src \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'X-Content-Security-Policy' => 'script-src \'self\' \'unsafe-inline\'; style-src \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\''),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createRequest(array $headers = array())
|
||||||
|
{
|
||||||
|
$request = new Request();
|
||||||
|
$request->headers->add($headers);
|
||||||
|
|
||||||
|
return $request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createResponse(array $headers = array())
|
||||||
|
{
|
||||||
|
$response = new Response();
|
||||||
|
$response->headers->add($headers);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mockNonceGenerator($value)
|
||||||
|
{
|
||||||
|
$generator = $this->getMock('Symfony\Bundle\WebProfilerBundle\Csp\NonceGenerator');
|
||||||
|
|
||||||
|
$generator->expects($this->any())
|
||||||
|
->method('generate')
|
||||||
|
->will($this->returnValue($value));
|
||||||
|
|
||||||
|
return $generator;
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,7 @@
|
|||||||
namespace Symfony\Bundle\WebProfilerBundle\Tests\EventListener;
|
namespace Symfony\Bundle\WebProfilerBundle\Tests\EventListener;
|
||||||
|
|
||||||
use Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener;
|
use Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener;
|
||||||
|
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\HttpKernel\Event\FilterResponseEvent;
|
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
|
||||||
@ -31,7 +32,7 @@ class WebDebugToolbarListenerTest extends \PHPUnit_Framework_TestCase
|
|||||||
|
|
||||||
$response = new Response($content);
|
$response = new Response($content);
|
||||||
|
|
||||||
$m->invoke($listener, $response, Request::create('/'));
|
$m->invoke($listener, $response, Request::create('/'), array('csp_script_nonce' => 'scripto', 'csp_style_nonce' => 'stylo'));
|
||||||
$this->assertEquals($expected, $response->getContent());
|
$this->assertEquals($expected, $response->getContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,6 +244,8 @@ class WebDebugToolbarListenerTest extends \PHPUnit_Framework_TestCase
|
|||||||
->method('getRequestFormat')
|
->method('getRequestFormat')
|
||||||
->will($this->returnValue($requestFormat));
|
->will($this->returnValue($requestFormat));
|
||||||
|
|
||||||
|
$request->headers = new HeaderBag();
|
||||||
|
|
||||||
if ($hasSession) {
|
if ($hasSession) {
|
||||||
$session = $this->getMock('Symfony\Component\HttpFoundation\Session\Session', array(), array(), '', false);
|
$session = $this->getMock('Symfony\Component\HttpFoundation\Session\Session', array(), array(), '', false);
|
||||||
$request->expects($this->any())
|
$request->expects($this->any())
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
"require": {
|
"require": {
|
||||||
"php": ">=5.5.9",
|
"php": ">=5.5.9",
|
||||||
"symfony/http-kernel": "~2.8|~3.0",
|
"symfony/http-kernel": "~2.8|~3.0",
|
||||||
|
"symfony/polyfill-php70": "~1.0",
|
||||||
"symfony/routing": "~2.8|~3.0",
|
"symfony/routing": "~2.8|~3.0",
|
||||||
"symfony/twig-bridge": "~2.8|~3.0"
|
"symfony/twig-bridge": "~2.8|~3.0"
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user