Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions apps/qubit/modules/staticpage/actions/indexAction.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
85 changes: 85 additions & 0 deletions lib/QubitStaticPageNonce.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

/*
* This file is part of the Access to Memory (AtoM) software.
*
* Access to Memory (AtoM) is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Access to Memory (AtoM) is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Access to Memory (AtoM). If not, see <http://www.gnu.org/licenses/>.
*/

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(
'/<style\b([^>]*)>/i',
function (array $matches) use ($cspNonce) {
return self::addNonceToTag($matches[0], $matches[1], $cspNonce);
},
$content
);
}

/**
* Add the current CSP nonce to inline script tags while leaving external scripts unchanged.
*/
protected static function addNonceToInlineScriptTags(string $content, string $cspNonce): string
{
return preg_replace_callback(
'/<script\b([^>]*)>/i',
function (array $matches) use ($cspNonce) {
if (preg_match('/\bsrc\s*=/i', $matches[1])) {
return $matches[0];
}

return self::addNonceToTag($matches[0], $matches[1], $cspNonce);
},
$content
);
}

/**
* Inject a nonce attribute into an opening tag unless one is already present.
*/
protected static function addNonceToTag(string $tag, string $attributes, string $cspNonce): string
{
if (preg_match('/\bnonce\s*=/i', $attributes)) {
return $tag;
}

$trimmedAttributes = rtrim($attributes);
if ('' !== $trimmedAttributes) {
$trimmedAttributes = ' '.$trimmedAttributes;
}

return preg_replace('/>$/', $trimmedAttributes.' '.$cspNonce.'>', $tag);
}
}
109 changes: 109 additions & 0 deletions test/phpunit/lib/QubitStaticPageNonceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

use PHPUnit\Framework\TestCase;

/**
* @internal
*
* @covers \QubitStaticPageNonce
*/
class QubitStaticPageNonceTest extends TestCase
{
protected function tearDown(): void
{
sfConfig::set('csp_nonce', null);
}

/**
* Confirm inline tags are left unchanged when no nonce is available.
*/
public function testAddNonceToInlineTagsLeavesInlineTagsUntouchedWhenNonceNotConfigured()
{
sfConfig::set('csp_nonce', '');

$content = '<style>#top-bar { color: red; }</style><script>console.log("ok");</script>';

$this->assertSame($content, QubitStaticPageNonce::addNonceToInlineTags($content));
}

/**
* Confirm inline style tags receive the current nonce.
*/
public function testAddNonceToInlineTagsAddsNonceToInlineStyleTag()
{
sfConfig::set('csp_nonce', 'nonce=abc123');

$content = '<style>#top-bar { color: red; }</style>';

$this->assertSame(
'<style nonce=abc123>#top-bar { color: red; }</style>',
QubitStaticPageNonce::addNonceToInlineTags($content)
);
}

/**
* Confirm inline script tags receive the current nonce.
*/
public function testAddNonceToInlineTagsAddsNonceToInlineScriptTag()
{
sfConfig::set('csp_nonce', 'nonce=abc123');

$content = '<script>console.log("ok");</script>';

$this->assertSame(
'<script nonce=abc123>console.log("ok");</script>',
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 = '<script src="/js/main.js"></script>';

$this->assertSame($content, QubitStaticPageNonce::addNonceToInlineTags($content));
}

/**
* Confirm an existing nonce attribute is preserved.
*/
public function testAddNonceToInlineTagsDoesNotOverrideExistingNonceAttribute()
{
sfConfig::set('csp_nonce', 'nonce=abc123');

$content = '<style nonce="existing">#top-bar { color: red; }</style>';

$this->assertSame($content, QubitStaticPageNonce::addNonceToInlineTags($content));
}

/**
* Confirm unrelated HTML is left unchanged.
*/
public function testAddNonceToInlineTagsLeavesContentWithoutInlineTagsUnchanged()
{
sfConfig::set('csp_nonce', 'nonce=abc123');

$content = '<div class="page">No inline content here.</div>';

$this->assertSame($content, QubitStaticPageNonce::addNonceToInlineTags($content));
}

/**
* Confirm the nonce is applied to multiple inline tags in the same fragment.
*/
public function testAddNonceToInlineTagsAddsNonceToMultipleInlineTags()
{
sfConfig::set('csp_nonce', 'nonce=abc123');

$content = '<style>#top-bar { color: red; }</style><script>console.log("ok");</script>';

$this->assertSame(
'<style nonce=abc123>#top-bar { color: red; }</style><script nonce=abc123>console.log("ok");</script>',
QubitStaticPageNonce::addNonceToInlineTags($content)
);
}
}
Loading