Skip to content

Commit

Permalink
Adjust how we handle reporting directives
Browse files Browse the repository at this point in the history
This should be backwards compatible with any previous config, but provide the correct `Report-To` header to go with the `report-to` directive
  • Loading branch information
edwilde committed Oct 19, 2023
1 parent b48447c commit cff9a02
Showing 1 changed file with 104 additions and 32 deletions.
136 changes: 104 additions & 32 deletions src/Policies/Policy.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,16 +88,25 @@ public function enforce(): self
* Add reporting directives to the policy, so that violations can be sent to
* the uri defined as CSP_REPORT_TO in the environment.
*
* @param string $uri
* @param string $uri - the uri to send the reports to, or empty to remove reporting
* @return self
*/
public function reportTo(string $uri): self
{
// if the string is empty, we can assume we need to _remove_ reporting
if (empty($uri)) {
unset($this->directives[Directive::REPORT]);
unset($this->directives[Directive::REPORT_TO]);

return $this;
}

// Add the report-uri directive - this is deprecated, but still supported by most browsers
$this->directives[Directive::REPORT] = [$uri];

// Add the report-to directive - this is the new standard, but not yet supported by all browsers
$this->directives[Directive::REPORT_TO] = ['csp-endpoint'];
// the syntax for this will be fixed when the header is added
$this->directives[Directive::REPORT_TO] = [$uri];

return $this;
}
Expand Down Expand Up @@ -138,23 +147,8 @@ public function applyTo(HTTPResponse $response)
return;
}

$reportTo = Environment::getEnv('CSP_REPORT_TO');
if (!array_key_exists(Directive::REPORT, $this->directives) && $reportTo) {
$this->reportTo($reportTo);
}

// Add the Report-To header, used by the report-to directive
if (array_key_exists(Directive::REPORT_TO, $this->directives) && $reportTo) {
$response->addHeader('Report-To', json_encode([
'group' => 'csp-endpoint',
'max_age' => 10886400,
'endpoints' => [
[
'url' => $reportTo,
],
],
]));
}
// optionally add reporting directives
$this->applyReporting($response);

$response->addHeader($headerName, (string) $this);
$response->addHeader('csp-name', ClassInfo::shortName(static::class));
Expand All @@ -175,6 +169,33 @@ public function __toString()
return implode('; ', $directives);
}

/*
* Takes an array of `Fragment` implementations and adds them to the policy
*/
public function addFragments(array $fragments): self
{
foreach ($fragments as $fragment) {
call_user_func_array([$fragment, 'addTo'], [$this]);
}

return $this;
}

/**
* If the given value is not an array and not null, wrap it in one.
*
* @param mixed $value
* @return array
*/
public static function wrap($value): array
{
if (is_null($value)) {
return [];
}

return is_array($value) ? $value : [$value];
}

protected function guardAgainstInvalidDirectives(string $directive)
{
if (!Directive::isValid($directive)) {
Expand Down Expand Up @@ -225,30 +246,81 @@ protected function sanitizeValue(string $value): string
return $value;
}

/*
* Takes an array of `Fragment` implementations and adds them to the policy
/**
* Add the reporting directives to the policy if the address is set
* as an environment variable.
*
* @param HTTPResponse $response - the response to add the header to
* @return void
*/
public function addFragments(array $fragments): self
private function applyReporting(HTTPResponse $response): void
{
foreach ($fragments as $fragment) {
call_user_func_array([$fragment, 'addTo'], [$this]);
$reportTo = Environment::getEnv('CSP_REPORT_TO');

$hasEnvironmentVariable = !is_null($reportTo) && $reportTo !== false;

// if we have the environment variable, assume we want both directives
if ($hasEnvironmentVariable) {
$this->reportTo($reportTo);
$this->applyReportTo($response);
return;
}

return $this;
// if we don't have the environment variable,
// check if we have the directives manually set
$hasReportDirective = array_key_exists(Directive::REPORT, $this->directives);
$hasReportToDirective = array_key_exists(Directive::REPORT_TO, $this->directives);

// no directives, no further processing needed
if (!$hasReportDirective && !$hasReportToDirective) {
return;
}

// if the report-to directive is set, we need to add the header and process the value
if ($hasReportToDirective) {
$this->applyReportTo($response);
return;
}
}

/**
* If the given value is not an array and not null, wrap it in one.
* Add the necessary extras for the report-to directive
*
* @param mixed $value
* @return array
* @param HTTPResponse $response - the response to add the header to
* @return void
*/
public static function wrap($value): array
private function applyReportTo(HTTPResponse $response): void
{
if (is_null($value)) {
return [];
$hasReportToDirective = array_key_exists(Directive::REPORT_TO, $this->directives);

// if the environment variable is not set, and the directive is not set, we can't add the header
if (!$hasReportToDirective) {
return;
}

return is_array($value) ? $value : [$value];
// set a standard group name to use
$groupName = 'csp-endpoint';

// 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];

// and set it correctly
$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,
],
],
], JSON_UNESCAPED_SLASHES));
}
}

0 comments on commit cff9a02

Please sign in to comment.