Skip to content

Commit

Permalink
Merge pull request #70 from Firesphere/hans/report-to-header
Browse files Browse the repository at this point in the history
Add PoC of report-to header
  • Loading branch information
paragonie-security committed Dec 18, 2023
2 parents 9a2f733 + 5daab21 commit 17b59da
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 8 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"php": "^7.1|^8",
"ext-json": "*",
"paragonie/constant_time_encoding": "^2",
"psr/http-message": "^1|^2"
"psr/http-message": "^1|^2",
"opis/json-schema": "^2.3"
},
"require-dev": {
"phpunit/phpunit": "^7|^8|^9|^10",
Expand Down
50 changes: 50 additions & 0 deletions schema/reportto.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"$schema": "http://json-schema.org/draft-06/schema#",
"$ref": "#/definitions/ReportTo",
"definitions": {
"ReportTo": {
"type": "object",
"additionalProperties": false,
"properties": {
"group": {
"type": "string"
},
"max_age": {
"type": "integer"
},
"endpoints": {
"type": "array",
"items": {
"$ref": "#/definitions/Endpoint"
}
},
"include_subdomains": {
"type": "boolean"
}
},
"required": [
"endpoints",
"group",
"max_age"
],
"title": "ReportTo"
},
"Endpoint": {
"type": "object",
"additionalProperties": false,
"properties": {
"url": {
"type": "string",
"format": "uri",
"qt-uri-protocols": [
"https"
]
}
},
"required": [
"url"
],
"title": "Endpoint"
}
}
}
139 changes: 132 additions & 7 deletions src/CSPBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
declare(strict_types=1);
namespace ParagonIE\CSPBuilder;

use Opis\JsonSchema\Exceptions\SchemaException;
use Opis\JsonSchema\Helper;
use Opis\JsonSchema\Validator;
use ParagonIE\ConstantTime\Base64;
use Psr\Http\Message\MessageInterface;
use Exception;
Expand Down Expand Up @@ -56,6 +59,21 @@ class CSPBuilder
*/
private $compiled = '';

/**
* @var array
*/
private $reportEndpoints = [];

/**
* @var string
*/
private $compiledEndpoints = '';

/**
* @var bool
*/
private $needsCompileEndpoints = true;

/**
* @var bool
*/
Expand Down Expand Up @@ -141,23 +159,51 @@ public function compile(): string
if (!is_string($this->policies['report-uri'])) {
throw new TypeError('report-uri policy somehow not a string');
}
$compiled [] = 'report-uri ' . $this->enc($this->policies['report-uri'], 'report-uri') . '; ';
$compiled [] = sprintf('report-uri %s; ', $this->enc($this->policies['report-uri']), 'report-uri');
}
if (!empty($this->policies['report-to'])) {
if (!is_string($this->policies['report-to'])) {
throw new TypeError('report-to policy somehow not a string');
}
$compiled []= 'report-to ' . $this->policies['report-to'] . '; ';
// @todo validate this `report-to` target, is in the `report-to` header?
$compiled[] = sprintf('report-to %s; ', $this->policies['report-to']);
}
if (!empty($this->policies['upgrade-insecure-requests'])) {
$compiled []= 'upgrade-insecure-requests';
}

$this->compiled = rtrim(implode('', $compiled), '; ');
$this->needsCompile = false;

return $this->compiled;
}

public function compileReportEndpoints()
{
if (!empty($this->reportEndpoints) && $this->needsCompileEndpoints) {
// If it's a string, it's probably something like `report-to: key=endpoint
// Do nothing
if (!is_array($this->reportEndpoints)) {
throw new TypeError('Report endpoints is not an array');
}
if (is_array($this->reportEndpoints)) {
$jsonValidator = new Validator();
$reportTo = [];
$schema = file_get_contents(__DIR__ . '/../schema/reportto.json');
foreach ($this->reportEndpoints as $reportEndpoint) {
$reportEndpointAsJSON = \Opis\JsonSchema\Helper::toJSON($reportEndpoint);
$isValid = $jsonValidator->validate($reportEndpointAsJSON, $schema);
if ($isValid->isValid()) {
$reportTo[] = json_encode($reportEndpointAsJSON);
}

}
$this->compiledEndpoints = rtrim(implode(',', $reportTo));
}
$this->needsCompileEndpoints = false;
}
}

/**
* Add a source to our allow white-list
*
Expand Down Expand Up @@ -266,6 +312,17 @@ public function addDirective(string $key, $value = null): self
return $this;
}


/**
* @param array|string $reportEndpoint
* @return void
*/
public function addReportEndpoints(array|string $reportEndpoint): void
{
$this->needsCompileEndpoints = true;
$this->reportEndpoints[] = Helper::toJSON($reportEndpoint);
}

/**
* Add a plugin type to be added
*
Expand Down Expand Up @@ -432,6 +489,20 @@ public function getCompiledHeader(): string
return $this->compiled;
}

/**
* Get the formatted report-to header
*
* @return string
*/
public function getCompiledReportEndpointsHeader(): string
{
if ($this->needsCompileEndpoints) {
$this->compileReportEndpoints();
}

return $this->compiledEndpoints;
}

/**
* Get an associative array of headers to return.
*
Expand All @@ -443,7 +514,14 @@ public function getHeaderArray(bool $legacy = true): array
if ($this->needsCompile) {
$this->compile();
}
$return = [];
if ($this->needsCompileEndpoints) {
$this->compileReportEndpoints();
}
if (!empty($this->compiledEndpoints)) {
$return = [
'Report-To' => $this->compiledEndpoints
];
}
foreach ($this->getHeaderKeys($legacy) as $key) {
$return[(string) $key] = $this->compiled;
}
Expand All @@ -465,6 +543,14 @@ public function getRequireHeaders(): array
return $headers;
}

/**
* @return array|string
*/
public function getReportEndpoints(): array
{
return $this->reportEndpoints;
}

/**
* Add a new hash to the existing CSP
*
Expand Down Expand Up @@ -505,13 +591,20 @@ public function injectCSPHeader(MessageInterface $message, bool $legacy = false)
if ($this->needsCompile) {
$this->compile();
}
if ($this->needsCompileEndpoints) {
$this->compileReportEndpoints();
}
foreach ($this->getRequireHeaders() as $header) {
list ($key, $value) = $header;
$message = $message->withAddedHeader($key, $value);
}
foreach ($this->getHeaderKeys($legacy) as $key) {
$message = $message->withAddedHeader($key, $this->compiled);
}
if (!empty($this->compileReportEndpoints())) {
$message = $message->withAddedHeader('report-to', $this->reportTo);
}

return $message;
}

Expand Down Expand Up @@ -586,6 +679,7 @@ public function saveSnippet(
): bool {
if ($this->needsCompile) {
$this->compile();
$this->compileReportEndpoints();
}

// Are we doing a report-only header?
Expand Down Expand Up @@ -642,12 +736,18 @@ public function sendCSPHeader(bool $legacy = true): bool
if ($this->needsCompile) {
$this->compile();
}
if ($this->needsCompileEndpoints) {
$this->compileReportEndpoints();
}
foreach ($this->getRequireHeaders() as $header) {
list ($key, $value) = $header;
header($key.': '.$value);
header(sprintf('%s: %s', $key, $value));
}
foreach ($this->getHeaderKeys($legacy) as $key) {
header($key.': '.$this->compiled);
header(sprintf('%s: %s', $key, $this->compiled));
}
if (!empty($this->compiledEndpoints)) {
header(sprintf('report-to: %s', $this->compiledEndpoints));
}
return true;
}
Expand Down Expand Up @@ -770,6 +870,31 @@ public function removeDirective(string $key): self
return $this;
}

/**
* @param array|string $reportEndpoints
* @return void
*/
public function setReportEndpoints(array|string $reportEndpoints): void
{
$this->needsCompileEndpoints = true;
$toJSON = Helper::toJSON($reportEndpoints);
// If there's only one, wrap it in an array, so more can be added
$toJSON = is_array($toJSON) ? $toJSON : [$toJSON];
$this->reportEndpoints = $toJSON;
}


public function removeReportEndpoint(string $key)
{
foreach ($this->reportEndpoints as $idx => $endpoint) {
if ($endpoint->group === $key) {
unset($this->reportEndpoints[$idx]);
// Reset the array keys
$this->reportEndpoints = array_values($this->reportEndpoints);
break;
}
}
}
/**
* Allow/disallow filesystem: URIs for a given directive
*
Expand Down Expand Up @@ -927,10 +1052,10 @@ public function setReportUri(string $url = ''): self
/**
* Set the report-to directive to the desired string.
*
* @param string $policy
* @param string|array $policy
* @return self
*/
public function setReportTo(string $policy = ''): self
public function setReportTo($policy = ''): self
{
$this->policies['report-to'] = $policy;
return $this;
Expand Down

0 comments on commit 17b59da

Please sign in to comment.