Skip to content

Commit

Permalink
Allow multiple endpoint urls to be set for report-to
Browse files Browse the repository at this point in the history
  • Loading branch information
edwilde committed Oct 19, 2023
1 parent cceb2c4 commit e115b93
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 12 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ To set the **report to** url, we usually use an env var named `CSP_REPORT_TO`. T

You can also call `$this->reportTo()` in your policies configure func if required (perhaps you want the report URI based on the policy applied).

Reporting can be sent to multiple urls if required, `CSP_REPORT_TO` supports CSV, or the directive can be used with an array.

To add the policy to the list of applied policies you'll want to add some yaml config:
```yaml
Silverstripe\CSP\CSPMiddleware:
Expand Down
55 changes: 43 additions & 12 deletions src/Policies/Policy.php
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,17 @@ private function applyReporting(HTTPResponse $response): void

// if we have the environment variable, assume we want both directives
if ($hasEnvironmentVariable) {
$hasMultipleUrls = str_contains($reportTo, ',');

// if we are handling multiple urls we need to only add a single directive
if ($hasMultipleUrls) {
$reportToArray = explode(',', $reportTo);
$this->directives[Directive::REPORT_TO] = $reportToArray;
$this->applyReportTo($response);
return;
}

// otherwise add both
$this->reportTo($reportTo);
$this->applyReportTo($response);
return;
Expand Down Expand Up @@ -298,29 +309,49 @@ private function applyReportTo(HTTPResponse $response): void
return;
}

// set a standard group name to use
$groupName = 'csp-endpoint';
// get the directive value
$reportTo = $this->directives[Directive::REPORT_TO];

// if the directive is not set, we can't add the header
if (is_null($reportTo) || $reportTo === false || $reportTo === '') {
return;
}

$endpoints = [];
foreach ($reportTo as $uri) {
// tidy up
$uri = trim($uri);

// if the directive is set incorrectly as a url, use it for the endpoint instead
if (filter_var($this->directives[Directive::REPORT_TO][0], FILTER_VALIDATE_URL)) {
$reportTo = $this->directives[Directive::REPORT_TO][0];
// if the value is not a url, we can't add the header
if (!filter_var($uri, FILTER_VALIDATE_URL)) {
continue;
}

// if the value is a url, we can use it as the endpoint
$endpoints[] = [
'url' => $uri,
];
}

// and set it correctly
$this->directives[Directive::REPORT_TO] = [$groupName];
// if we don't have any endpoints, we can't add the header
if (count($endpoints) === 0) {
return;
}

// set a standard group name to use
$groupName = 'csp-endpoint';

// add the group name to the directive, replacing the invalid urls
$this->directives[Directive::REPORT_TO] = [$groupName];

// set the amount of time the users-browser should store the endpoint
$ttl = Environment::getEnv('CSP_REPORT_TO_TTL') ?: 10886400; // 126 days

// add the reponse header
$response->addHeader('Report-To', json_encode([
'group' => $groupName,
'max_age' => $ttl,
'endpoints' => [
[
'url' => $reportTo,
],
],
'endpoints' => $endpoints,
], JSON_UNESCAPED_SLASHES));
}
}
106 changes: 106 additions & 0 deletions tests/PolicyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,112 @@ public function testAReportToCanBeSetWithoutReportURI(): void
);
}

/**
* Check the reporting endpoint can be set from the environment variable
*/
public function testMultipleReportURICanBeSetFromEnvironmentVariable(): void
{
[$request, $response] = $this->getRequestResponse();
/** @var Policy $policy */
$policy = Injector::inst()->get(CMS::class);

$reportTo = 'https://example.com,http://example.org,https://example.net';
Environment::setEnv('CSP_REPORT_TO', $reportTo);
Environment::setEnv('CSP_REPORT_ONLY', 'enabled');

// apply the policy
$policy->applyTo($response);

// check the header
$this->assertNull($response->getHeader('Content-Security-Policy'));
$this->assertNotNull($response->getHeader('Content-Security-Policy-Report-Only'));

// the report-uri directive only supports a single address,
// so we should not expect to see it
$this->assertStringNotContainsString(
'report-uri',
$response->getHeader('Content-Security-Policy-Report-Only')
);

// check the report-to directive
$this->assertStringContainsString(
'report-to csp-endpoint',
$response->getHeader('Content-Security-Policy-Report-Only')
);

// check the Report-To header
$this->assertNotNull($response->getHeader('Report-To'));

// convert the comma separated list into an array
$urls = explode(',', $reportTo);
$endpoints = [];
foreach ($urls as $url) {
$endpoints[] = ['url' => trim($url)];
}

$this->assertStringContainsString(
sprintf(
'{"group":"csp-endpoint","max_age":10886400,"endpoints":%s}',
json_encode($endpoints, JSON_UNESCAPED_SLASHES)
),
$response->getHeader('Report-To')
);
}

/**
* Check the reporting endpoint can be set from the environment variable
*/
public function testMultipleReportURICanBeSetFromCode(): void
{
[$request, $response] = $this->getRequestResponse();
/** @var Policy $policy */
$policy = Injector::inst()->get(CMS::class);

$urls = [
'https://example.com',
'http://example.org',
'https://example.net',
];
$policy->addDirective(Directive::REPORT_TO, $urls);

// apply the policy
$policy->applyTo($response);

// check the header
$this->assertNotNull($response->getHeader('Content-Security-Policy'));
$this->assertNull($response->getHeader('Content-Security-Policy-Report-Only'));

// the report-uri directive only supports a single address,
// so we should not expect to see it
$this->assertStringNotContainsString(
'report-uri',
$response->getHeader('Content-Security-Policy')
);

// check the report-to directive
$this->assertStringContainsString(
'report-to csp-endpoint',
$response->getHeader('Content-Security-Policy')
);

// check the Report-To header
$this->assertNotNull($response->getHeader('Report-To'));

// convert the comma separated list into an array
$endpoints = [];
foreach ($urls as $url) {
$endpoints[] = ['url' => trim($url)];
}

$this->assertStringContainsString(
sprintf(
'{"group":"csp-endpoint","max_age":10886400,"endpoints":%s}',
json_encode($endpoints, JSON_UNESCAPED_SLASHES)
),
$response->getHeader('Report-To')
);
}

public function testIsCanUseMultipleValuesForTheSameDirective(): void
{
$policy = new class extends Policy {
Expand Down

0 comments on commit e115b93

Please sign in to comment.