Skip to content
Merged
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
16 changes: 13 additions & 3 deletions system/HTTP/ContentSecurityPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -427,9 +427,7 @@ public function getScriptNonce(): string
*/
public function finalize(ResponseInterface $response)
{
if ($this->autoNonce) {
$this->generateNonces($response);
}
$this->generateNonces($response);

$this->buildHeaders($response);
}
Expand Down Expand Up @@ -892,6 +890,10 @@ protected function addOption($options, string $target, ?bool $explicitReporting
*/
protected function generateNonces(ResponseInterface $response)
{
if ($this->enabled() && ! $this->autoNonce) {
return;
}

$body = (string) $response->getBody();

if ($body === '') {
Expand All @@ -905,6 +907,10 @@ protected function generateNonces(ResponseInterface $response)
$pattern = sprintf('/(%s|%s)/', preg_quote($this->styleNonceTag, '/'), preg_quote($this->scriptNonceTag, '/'));

$body = preg_replace_callback($pattern, function ($match) use ($jsonEscape): string {
if (! $this->enabled()) {
return '';
}

$nonce = $match[0] === $this->styleNonceTag ? $this->getStyleNonce() : $this->getScriptNonce();
$attr = 'nonce="' . $nonce . '"';

Expand All @@ -923,6 +929,10 @@ protected function generateNonces(ResponseInterface $response)
*/
protected function buildHeaders(ResponseInterface $response)
{
if (! $this->enabled()) {
return;
}

$response->setHeader('Content-Security-Policy', []);
$response->setHeader('Content-Security-Policy-Report-Only', []);
$response->setHeader('Reporting-Endpoints', []);
Expand Down
6 changes: 1 addition & 5 deletions system/HTTP/ResponseTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -367,11 +367,7 @@ public function send()
{
// If we're enforcing a Content Security Policy,
// we need to give it a chance to build out it's headers.
if ($this->CSP->enabled()) {
$this->CSP->finalize($this);
} else {
$this->body = str_replace(['{csp-style-nonce}', '{csp-script-nonce}'], '', $this->body ?? '');
}
$this->CSP->finalize($this);

$this->sendHeaders();
$this->sendCookies();
Expand Down
146 changes: 146 additions & 0 deletions tests/system/HTTP/ResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -577,4 +577,150 @@ public function testPretendOutput(): void

$this->assertSame('Happy days', $actual);
}

public function testSendRemovesDefaultNoncePlaceholdersWhenCSPDisabled(): void
{
$config = new App();
$config->CSPEnabled = false;

$response = new Response($config);
$response->pretend(true);

$body = '<html><script {csp-script-nonce}>console.log("test")</script><style {csp-style-nonce}>.test{}</style></html>';
$response->setBody($body);

ob_start();
$response->send();
$actual = ob_get_clean();

// Nonce placeholders should be removed when CSP is disabled
$this->assertIsString($actual);
$this->assertStringNotContainsString('{csp-script-nonce}', $actual);
$this->assertStringNotContainsString('{csp-style-nonce}', $actual);
$this->assertStringContainsString('<script >console.log("test")</script>', $actual);
$this->assertStringContainsString('<style >.test{}</style>', $actual);
}

public function testSendRemovesCustomNoncePlaceholdersWhenCSPDisabled(): void
{
$appConfig = new App();
$appConfig->CSPEnabled = false;

// Create custom CSP config with custom nonce tags
$cspConfig = new \Config\ContentSecurityPolicy();
$cspConfig->scriptNonceTag = '{custom-script-tag}';
$cspConfig->styleNonceTag = '{custom-style-tag}';

Services::injectMock('csp', new ContentSecurityPolicy($cspConfig));

$response = new Response($appConfig);
$response->pretend(true);

$body = '<html><script {custom-script-tag}>test()</script><style {custom-style-tag}>.x{}</style></html>';
$response->setBody($body);

ob_start();
$response->send();
$actual = ob_get_clean();

// Custom nonce placeholders should be removed when CSP is disabled
$this->assertIsString($actual);
$this->assertStringNotContainsString('{custom-script-tag}', $actual);
$this->assertStringNotContainsString('{custom-style-tag}', $actual);
$this->assertStringContainsString('<script >test()</script>', $actual);
$this->assertStringContainsString('<style >.x{}</style>', $actual);
}

public function testSendNoEffectWhenBodyEmptyAndCSPDisabled(): void
{
$config = new App();
$config->CSPEnabled = false;

$response = new Response($config);
$response->pretend(true);

$body = '';
$response->setBody($body);

ob_start();
$response->send();
$actual = ob_get_clean();

$this->assertIsString($actual);
$this->assertSame('', $actual);
}

public function testSendNoEffectWithNoPlaceholdersAndCSPDisabled(): void
{
$config = new App();
$config->CSPEnabled = false;

$response = new Response($config);
$response->pretend(true);

$body = '<html><head><title>Test</title></head><body><p>No placeholders here</p></body></html>';
$response->setBody($body);

ob_start();
$response->send();
$actual = ob_get_clean();

// Body should be unchanged when there are no placeholders and CSP is disabled
$this->assertIsString($actual);
$this->assertSame($body, $actual);
}

public function testSendRemovesMultiplePlaceholdersWhenCSPDisabled(): void
{
$config = new App();
$config->CSPEnabled = false;

$response = new Response($config);
$response->pretend(true);

$body = '<html><script {csp-script-nonce}>console.log("test")</script><script {csp-script-nonce}>console.log("test2")</script><style {csp-style-nonce}>.test{}</style><style {csp-style-nonce}>.test2{}</style></html>';
$response->setBody($body);

ob_start();
$response->send();
$actual = ob_get_clean();

// All nonce placeholders should be removed when CSP is disabled
$this->assertIsString($actual);
$this->assertStringNotContainsString('{csp-script-nonce}', $actual);
$this->assertStringNotContainsString('{csp-style-nonce}', $actual);
$this->assertStringContainsString('<script >console.log("test")</script>', $actual);
$this->assertStringContainsString('<script >console.log("test2")</script>', $actual);
$this->assertStringContainsString('<style >.test{}</style>', $actual);
$this->assertStringContainsString('<style >.test2{}</style>', $actual);
}

public function testSendRemovesPlaceholdersWhenBothCSPAndAutoNonceAreDisabled(): void
{
$appConfig = new App();
$appConfig->CSPEnabled = false;

// Create custom CSP config with custom nonce tags
$cspConfig = new \Config\ContentSecurityPolicy();
$cspConfig->autoNonce = false;

Services::injectMock('csp', new ContentSecurityPolicy($cspConfig));

$response = new Response($appConfig);
$response->pretend(true);

$body = '<html><script {csp-script-nonce}>test()</script><style {csp-style-nonce}>.x{}</style></html>';
$response->setBody($body);

ob_start();
$response->send();
$actual = ob_get_clean();

// Custom nonce placeholders should be removed when CSP is disabled
$this->assertIsString($actual);
$this->assertStringNotContainsString('{csp-script-nonce}', $actual);
$this->assertStringNotContainsString('{csp-style-nonce}', $actual);
$this->assertStringContainsString('<script >test()</script>', $actual);
$this->assertStringContainsString('<style >.x{}</style>', $actual);
}
}
1 change: 1 addition & 0 deletions user_guide_src/source/changelogs/v4.7.1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Deprecations
Bugs Fixed
**********

- **ContentSecurityPolicy:** Fixed a bug where custom CSP tags were not removed from generated HTML when CSP was disabled. The method now ensures that all custom CSP tags are removed from the generated HTML.
- **ContentSecurityPolicy:** Fixed a bug where ``generateNonces()`` produces corrupted JSON responses by replacing CSP nonce placeholders with unescaped double quotes. The method now automatically JSON-escapes nonce attributes when the response Content-Type is JSON.
- **Session:** Fixed a bug in ``MemcachedHandler`` where the constructor incorrectly threw an exception when ``savePath`` was not empty.

Expand Down
Loading