Skip to content
Closed
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
5 changes: 5 additions & 0 deletions system/HTTP/ContentSecurityPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -1052,4 +1052,9 @@ public function clearDirective(string $directive): void

$this->{$this->directives[$directive]} = [];
}

public function clearNoncePlaceholders(string $text): string
{
return str_replace([$this->styleNonceTag, $this->scriptNonceTag], '', $text);
}
}
2 changes: 1 addition & 1 deletion system/HTTP/ResponseTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ public function send()
if ($this->CSP->enabled()) {
$this->CSP->finalize($this);
} else {
$this->body = str_replace(['{csp-style-nonce}', '{csp-script-nonce}'], '', $this->body ?? '');
$this->body = $this->CSP->clearNoncePlaceholders($this->body);
}

$this->sendHeaders();
Expand Down
63 changes: 63 additions & 0 deletions tests/system/HTTP/ContentSecurityPolicyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -937,4 +937,67 @@ public function testClearDirective(): void
$this->assertNotContains('report-uri http://example.com/csp/reports', $directives);
$this->assertNotContains('report-to default', $directives);
}

public function testClearNoncePlaceholdersWithDefaultTags(): void
{
$config = new CSPConfig();
$csp = new ContentSecurityPolicy($config);

$body = 'Test {csp-script-nonce} and {csp-style-nonce} here';
$cleaned = $csp->clearNoncePlaceholders($body);

$this->assertSame('Test and here', $cleaned);
$this->assertStringNotContainsString('{csp-script-nonce}', $cleaned);
$this->assertStringNotContainsString('{csp-style-nonce}', $cleaned);
}

public function testClearNoncePlaceholdersWithCustomTags(): void
{
$config = new CSPConfig();
$config->scriptNonceTag = '{custom-script-nonce}';
$config->styleNonceTag = '{custom-style-nonce}';
$csp = new ContentSecurityPolicy($config);

$body = 'Test {custom-script-nonce} and {custom-style-nonce} here';
$cleaned = $csp->clearNoncePlaceholders($body);

$this->assertSame('Test and here', $cleaned);
$this->assertStringNotContainsString('{custom-script-nonce}', $cleaned);
$this->assertStringNotContainsString('{custom-style-nonce}', $cleaned);
}

public function testClearNoncePlaceholdersWithEmptyBody(): void
{
$config = new CSPConfig();
$csp = new ContentSecurityPolicy($config);

$body = '';
$cleaned = $csp->clearNoncePlaceholders($body);

$this->assertSame('', $cleaned);
}

public function testClearNoncePlaceholdersWithNoPlaceholders(): void
{
$config = new CSPConfig();
$csp = new ContentSecurityPolicy($config);

$body = 'Test body with no placeholders';
$cleaned = $csp->clearNoncePlaceholders($body);

$this->assertSame($body, $cleaned);
}

public function testClearNoncePlaceholdersWithMultiplePlaceholders(): void
{
$config = new CSPConfig();
$csp = new ContentSecurityPolicy($config);

$body = '<script {csp-script-nonce}>a</script><script {csp-script-nonce}>b</script><style {csp-style-nonce}>c</style>';
$cleaned = $csp->clearNoncePlaceholders($body);

$this->assertStringNotContainsString('{csp-script-nonce}', $cleaned);
$this->assertStringNotContainsString('{csp-style-nonce}', $cleaned);
$this->assertSame('<script >a</script><script >b</script><style >c</style>', $cleaned);
}
}
76 changes: 76 additions & 0 deletions tests/system/HTTP/ResponseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -577,4 +577,80 @@ 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_contents();
ob_end_clean();

// Nonce placeholders should be removed when CSP is disabled
$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));

$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_contents();
ob_end_clean();

// Custom nonce placeholders should be removed when CSP is disabled
$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 testSendWithCSPDisabledDoesNotAffectBodyWithoutNonceTags(): void
{
$config = new App();
$config->CSPEnabled = false;

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

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

ob_start();
$response->send();
$actual = ob_get_contents();
ob_end_clean();

// Body without nonce tags should remain unchanged
$this->assertSame($body, $actual);
}
}
Loading