Skip to content

Commit

Permalink
feat: support Reporting-Endpoints and NEL headers (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
bepsvpt committed Oct 13, 2024
1 parent c1b0893 commit 24e2865
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 2 deletions.
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@
],
"homepage": "https://github.com/bepsvpt/secure-headers",
"require": {
"php": "^7.0 || ^8.0"
"php": "^7.0 || ^8.0",
"ext-json": "*"
},
"require-dev": {
"ext-json": "*",
"ext-xdebug": "*",
"ergebnis/composer-normalize": "^2.42",
"laravel/pint": "^1.14",
Expand Down
35 changes: 35 additions & 0 deletions config/secure-headers.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,41 @@
'preload' => false,
],

/*
* Reporting Endpoints
*
* Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Reporting-Endpoints
*
* The array key is the endpoint name, and the value is the URL.
*/

'reporting' => [
// 'csp' => 'https://example.com/csp-reports',
// 'nel' => 'https://example.com/nel-reports',
],

/*
* Network Error Logging
*
* Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Network_Error_Logging
*/

'nel' => [
'enable' => false,

// The name of reporting API, not the endpoint URL.
// @see https://developer.mozilla.org/en-US/docs/Web/API/Reporting_API
'report-to' => '',

'max-age' => 86400,

'include-subdomains' => false,

'success-fraction' => 0.0,

'failure-fraction' => 1.0,
],

/*
* Expect-CT
*
Expand Down
60 changes: 60 additions & 0 deletions src/SecureHeaders.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,13 @@ public function send()
public function headers(): array
{
$headers = array_merge(
$this->reporting(),
$this->csp(),
$this->permissionsPolicy(),
$this->hsts(),
$this->expectCT(),
$this->clearSiteData(),
$this->networkErrorLogging(),
$this->miscellaneous()
);

Expand All @@ -116,6 +118,28 @@ public function headers(): array
return array_filter($headers);
}

/**
* Get Reporting Endpoints header.
*
* @return array<string>
*/
protected function reporting(): array
{
$config = $this->config['reporting'] ?? [];

if (empty($config)) {
return [];
}

$endpoints = [];

foreach ($config as $name => $url) {
$endpoints[] = sprintf('%s="%s"', $name, $url);
}

return ['Reporting-Endpoints' => implode(', ', $endpoints)];
}

/**
* Get CSP header.
*
Expand Down Expand Up @@ -214,6 +238,42 @@ protected function clearSiteData(): array
return ['Clear-Site-Data' => $builder->get()];
}

/**
* Generate NEL header.
*
* @return array<string>
*/
protected function networkErrorLogging(): array
{
$config = $this->config['nel'] ?? [];

if (! ($config['enable'] ?? false)) {
return [];
}

if (empty($config['report-to'])) {
return [];
}

unset($config['enable']);

$nel = [];

foreach ($config as $key => $value) {
$key = str_replace('-', '_', $key);

$nel[$key] = $value;
}

$encoded = json_encode($nel, JSON_PRESERVE_ZERO_FRACTION);

if ($encoded === false) {
return [];
}

return ['NEL' => $encoded];
}

/**
* Get miscellaneous headers.
*
Expand Down
71 changes: 71 additions & 0 deletions tests/SecureHeadersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,77 @@ public function testCrossOriginPolicy()
);
}

public function testReportingEndpoints()
{
$config = $this->config();

$config['reporting'] = [
'nel' => 'https://example.com/nel',
'csp' => 'https://example.com/csp',
];

$headers = (new SecureHeaders($config))->headers();

$this->assertArrayHasKey('Reporting-Endpoints', $headers);

$this->assertSame('nel="https://example.com/nel", csp="https://example.com/csp"', $headers['Reporting-Endpoints']);

// ensure backward compatibility

unset($config['reporting']);

$this->assertArrayNotHasKey(
'Reporting-Endpoints',
(new SecureHeaders($config))->headers()
);
}

public function testNetworkErrorLogging()
{
$config = $this->config();

$config['nel']['enable'] = true;

$this->assertArrayNotHasKey(
'NEL',
(new SecureHeaders($config))->headers()
);

$config['nel']['report-to'] = 'nel';

$headers = (new SecureHeaders($config))->headers();

$this->assertArrayHasKey('NEL', $headers);

$this->assertSame('{"report_to":"nel","max_age":86400,"include_subdomains":false,"success_fraction":0.0,"failure_fraction":1.0}', $headers['NEL']);

$config['nel']['include-subdomains'] = true;

$config['nel']['failure-fraction'] = 0.01;

$headers = (new SecureHeaders($config))->headers();

$this->assertArrayHasKey('NEL', $headers);

$this->assertSame('{"report_to":"nel","max_age":86400,"include_subdomains":true,"success_fraction":0.0,"failure_fraction":0.01}', $headers['NEL']);

$config['nel']['enable'] = false;

$this->assertArrayNotHasKey(
'NEL',
(new SecureHeaders($config))->headers()
);

// ensure backward compatibility

unset($config['reporting-endpoints']);

$this->assertArrayNotHasKey(
'NEL',
(new SecureHeaders($config))->headers()
);
}

/**
* Get secure-headers config.
*
Expand Down

0 comments on commit 24e2865

Please sign in to comment.