Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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);
Comment thread
paulbalandan marked this conversation as resolved.

$this->sendHeaders();
$this->sendCookies();
Expand Down
153 changes: 153 additions & 0 deletions tests/system/HTTP/ResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use DateTimeZone;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use ReflectionClass;

/**
* @internal
Expand Down Expand Up @@ -577,4 +578,156 @@ 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}';

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

// Inject the custom CSP config
$reflection = new ReflectionClass($response);
$cspProperty = $reflection->getProperty('CSP');
$cspProperty->setValue($response, new ContentSecurityPolicy($cspConfig));
Comment thread
patel-vansh marked this conversation as resolved.
Outdated

$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;

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

// Inject the custom CSP config
$reflection = new ReflectionClass($response);
$cspProperty = $reflection->getProperty('CSP');
$cspProperty->setValue($response, new ContentSecurityPolicy($cspConfig));
Comment thread
patel-vansh marked this conversation as resolved.
Outdated

$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