diff --git a/apps/qubit/modules/staticpage/actions/indexAction.class.php b/apps/qubit/modules/staticpage/actions/indexAction.class.php index 402ce4ce1b..3e4a94ebd6 100644 --- a/apps/qubit/modules/staticpage/actions/indexAction.class.php +++ b/apps/qubit/modules/staticpage/actions/indexAction.class.php @@ -43,19 +43,35 @@ protected function getPurifiedStaticPageContent() $cacheKey = 'staticpage:'.$this->resource->id.':'.$culture; $cache = QubitCache::getInstance(); + // If cache is unavailable, purify the content and return it + // without caching. if (null === $cache) { - return; + return $this->addCspNonceToInlineTags( + QubitHtmlPurifier::getInstance()->purify( + $this->resource->getContent(['cultureFallback' => true]) + ) + ); } + // If the content is cached, return it with the current CSP nonce + // added to inline tags. if ($cache->has($cacheKey)) { - return $cache->get($cacheKey); + return $this->addCspNonceToInlineTags($cache->get($cacheKey)); } - $content = $this->resource->getContent(['cultureFallback' => true]); - $content = QubitHtmlPurifier::getInstance()->purify($content); + // Otherwise, purify the content, cache it, and return it with the + // current CSP nonce added to inline tags. + $content = QubitHtmlPurifier::getInstance()->purify( + $this->resource->getContent(['cultureFallback' => true]) + ); $cache->set($cacheKey, $content); - return $content; + return $this->addCspNonceToInlineTags($content); + } + + protected function addCspNonceToInlineTags($content) + { + return QubitStaticPageNonce::addNonceToInlineTags($content); } } diff --git a/lib/QubitStaticPageNonce.class.php b/lib/QubitStaticPageNonce.class.php new file mode 100644 index 0000000000..4ed393ed0d --- /dev/null +++ b/lib/QubitStaticPageNonce.class.php @@ -0,0 +1,85 @@ +. + */ + +class QubitStaticPageNonce +{ + /** + * Add the current CSP nonce to inline style and script tags in static page HTML. + */ + public static function addNonceToInlineTags(string $content): string + { + $cspNonce = sfConfig::get('csp_nonce', ''); + if (empty($cspNonce)) { + return $content; + } + + $content = self::addNonceToInlineStyleTags($content, $cspNonce); + + return self::addNonceToInlineScriptTags($content, $cspNonce); + } + + /** + * Add the current CSP nonce to inline style tags that do not already define one. + */ + protected static function addNonceToInlineStyleTags(string $content, string $cspNonce): string + { + return preg_replace_callback( + '/'; + + $this->assertSame($content, QubitStaticPageNonce::addNonceToInlineTags($content)); + } + + /** + * Confirm inline style tags receive the current nonce. + */ + public function testAddNonceToInlineTagsAddsNonceToInlineStyleTag() + { + sfConfig::set('csp_nonce', 'nonce=abc123'); + + $content = ''; + + $this->assertSame( + '', + QubitStaticPageNonce::addNonceToInlineTags($content) + ); + } + + /** + * Confirm inline script tags receive the current nonce. + */ + public function testAddNonceToInlineTagsAddsNonceToInlineScriptTag() + { + sfConfig::set('csp_nonce', 'nonce=abc123'); + + $content = ''; + + $this->assertSame( + '', + QubitStaticPageNonce::addNonceToInlineTags($content) + ); + } + + /** + * Confirm external script tags are ignored because CSP nonces are only needed for inline code. + */ + public function testAddNonceToInlineTagsDoesNotAddNonceToExternalScriptTag() + { + sfConfig::set('csp_nonce', 'nonce=abc123'); + + $content = ''; + + $this->assertSame($content, QubitStaticPageNonce::addNonceToInlineTags($content)); + } + + /** + * Confirm an existing nonce attribute is preserved. + */ + public function testAddNonceToInlineTagsDoesNotOverrideExistingNonceAttribute() + { + sfConfig::set('csp_nonce', 'nonce=abc123'); + + $content = ''; + + $this->assertSame($content, QubitStaticPageNonce::addNonceToInlineTags($content)); + } + + /** + * Confirm unrelated HTML is left unchanged. + */ + public function testAddNonceToInlineTagsLeavesContentWithoutInlineTagsUnchanged() + { + sfConfig::set('csp_nonce', 'nonce=abc123'); + + $content = '