Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fe7508b
(dev) update dev requirements, update ignore file, remove unnecessary…
JamesDPC Jul 28, 2025
d8d09fa
Add Github workflow
JamesDPC Jul 28, 2025
f74699c
(rector) process
JamesDPC Jul 28, 2025
7e35e1f
(php-cs-fixer) fix
JamesDPC Jul 28, 2025
40edf29
(phpstan) updates based on analysis
JamesDPC Jul 28, 2025
abb6277
[rector] Automated updates generated by rector configuration
JamesDPC Jul 28, 2025
06128a3
[php-cs-fixer] Automated updates generated by php-cs-fixer configuration
JamesDPC Jul 28, 2025
2f27f03
(phpstan) update assertion as it always returned true
JamesDPC Jul 28, 2025
d5833dc
(test) migrate abstract test
JamesDPC Jul 29, 2025
da10704
(phpstan) update tests based on analysis results
JamesDPC Jul 29, 2025
5611610
Fix: add default property value, add comparison
JamesDPC Jul 29, 2025
d0cfe70
Fix: slash in reporting url
JamesDPC Jul 29, 2025
b6c8444
Set totalSteps in setup(), only increment current step
JamesDPC Jul 29, 2025
595b870
Remove deprecation notice
JamesDPC Jul 29, 2025
b593df6
Update composer metadata
JamesDPC Jul 29, 2025
9cfa761
Improve i18n string handling in literal fields
JamesDPC Jul 29, 2025
bd1c1ed
Improve i18n strings
JamesDPC Jul 29, 2025
5093973
Improve i18n strings in form field descriptions
JamesDPC Jul 29, 2025
1168996
Add missing i18n strings
JamesDPC Jul 29, 2025
ad1bc9a
[rector] Automated updates generated by rector configuration
JamesDPC Jul 29, 2025
2a1aa6e
[php-cs-fixer] Automated updates generated by php-cs-fixer configuration
JamesDPC Jul 29, 2025
4e0609e
Use correct i18n prefix
JamesDPC Jul 29, 2025
89044c3
(docs) add a note about the use of the middleware
JamesDPC Jul 29, 2025
14507d0
Add deprecation notices for CSP middleware and metatag delivery method
JamesDPC Jul 29, 2025
a6afd74
Fix: use correct key for i18n string
JamesDPC Jul 29, 2025
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
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: CI

on:
pull_request: null

jobs:
Silverstripe:
name: 'Silverstripe (bundle)'
uses: nswdpc/ci-files/.github/workflows/silverstripe_53_83.yml@v-4
PHPStan:
name: 'PHPStan (analyse)'
uses: nswdpc/ci-files/.github/workflows/phpstan.silverstripe_83.yml@v-4
needs: Silverstripe
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
/vendor/
.DS_Store
/.php-cs-fixer.cache
/public/
/composer.lock
21 changes: 0 additions & 21 deletions .php-cs-fixer.dist.php

This file was deleted.

15 changes: 14 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"authors": [
{
"name": "James Ellis",
"homepage": "https://dpc.nsw.gov.au",
"role": "Developer"
}
],
Expand Down Expand Up @@ -40,7 +39,21 @@
"symbiote/silverstripe-multivaluefield": "^6"
},
"require-dev": {
"cambis/silverstripe-rector": "^2",
"phpunit/phpunit": "^9.5",
"cambis/silverstan": "^2",
"nswdpc/ci-files": "dev-v-4",
"phpstan/phpstan": "^2",
"rector/rector": "^2",
"phpstan/phpstan-phpunit": "^2",
"friendsofphp/php-cs-fixer": "^3"
},
"config": {
"allow-plugins": {
"composer/installers": true,
"silverstripe/vendor-plugin": true,
"silverstripe/recipe-plugin": true,
"phpstan/extension-installer": true
}
}
}
12 changes: 6 additions & 6 deletions lang/en.yml
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
en:
ContentSecurityPolicy:
ALTERNATE_REPORT_URI_DESCRIPTION: 'If not set and the sending of violation reports is enabled, reports will be directed to <code>{internal_reporting_url}</code> and will appear in the CSP/Reports screen.<br>Sending reports back to your own website may cause performance degradation.'
ALTERNATE_REPORT_URI_DESCRIPTION: 'If not set and the sending of violation reports is enabled, reports will be directed to {internal_reporting_url} and will appear in the CSP/Reports screen. Sending reports back to your own website may cause performance degradation.'
ALTERNATE_REPORT_URI_TITLE: 'Endpoint for report-uri violation reports'
ALTERNATE_REPORT_TO_TITLE: 'Endpoint for Reporting API (report-to) violation reports'
ALTERNATE_REPORT_TO_URI_DESCRIPTION: 'For services that have a separate Reporting API endpoint.<br>If not set and the sending of violation reports is enabled, reports will be directed to <code>{internal_reporting_url}</code> and will appear in the CSP/Reports screen.<br>Sending reports back to your own website may cause performance degradation.'
ALTERNATE_REPORT_TO_URI_DESCRIPTION: 'For services that have a separate Reporting API endpoint. If not set and the sending of violation reports is enabled, reports will be directed to {internal_reporting_url} and will appear in the CSP/Reports screen. Sending reports back to your own website may cause performance degradation.'
ALTERNATE_NEL_REPORT_URI_TITLE: 'NEL/Reporting API reporting URL that will accept Network Error Logging reports'
ALTERNATE_NEL_REPORT_URI_EXTERNAL: 'You must use an external reporting service.'
REPORT_VIA_META_TAG: 'Reporting violations is not supported when using the meta tag delivery method'
SEND_VIOLATION_REPORTS: "Send violation reports to a reporting system"
SEND_VIOLATION_REPORTS_REPORT_ONLY: "'Report Only' is on - it is wise to turn on sending violation reports"
USE_ON_PUBLISHED_SITE: 'When unchecked, this policy will be used on the draft site only'
USED_IN_MULTIPLE_POLICIES: 'This record is used in %d policies. Updating it will modify all linked policies'
USE_ON_PUBLISHED_SITE_DESCRIPTION: 'When unchecked, this policy will be used on the draft site only'
USED_IN_MULTIPLE_POLICIES: 'This record is used in {count} policies. Updating it will modify all linked policies'
ADD_SELF_VALUE: "Adds the 'self' value to this directive"
ADD_DATA_VALUE: "Adds the 'data:' value to this directive"
ADD_UNSAFE_INLINE_VALUE: "Adds the 'unsafe-inline' value to this directive"
SELECT_PREDEFINED_DIRECTIVE: '...or select a pre-defined directive'
PRUNE_REPORTS_JOBTITLE: 'Remove CSP violation reports older than %d hour'
REMOVED_COUNT_REPORTS: 'Removed %s reports(s)'
PRUNE_REPORTS_JOBTITLE: 'Remove CSP violation reports older than {count} hour(s)'
REMOVED_COUNT_REPORTS: 'Removed {count} reports(s)'
MINIMUM_CSP_LEVEL: "Minimum CSP Level"
MINIMUM_CSP_LEVEL_DESCRIPTION: "Setting a higher level will remove from the policy features deprecated in previous versions, such as the 'report-uri' directive. 2 is a good setting."
12 changes: 4 additions & 8 deletions src/Controllers/CspModelAdmin.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,24 @@
class CspModelAdmin extends ModelAdmin
{
/**
* @var string
* @config
*/
private static $url_segment = 'content-security-policy';
private static string $url_segment = 'content-security-policy';

/**
* @var string
* @config
*/
private static $menu_title = 'CSP';
private static string $menu_title = 'CSP';

/**
* @var string
* @config
*/
private static $menu_icon_class = 'font-icon-block';
private static string $menu_icon_class = 'font-icon-block';

/**
* @var array
* @config
*/
private static $managed_models = [
private static array $managed_models = [
Policy::class,
Directive::class,
ViolationReport::class
Expand Down
36 changes: 16 additions & 20 deletions src/Controllers/ReportingEndpoint.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,47 +16,43 @@
*/
class ReportingEndpoint extends Controller
{

/**
* Whether reports are accepted by this endpoint
* @var bool
* @config
*/
private static $accept_reports = false;
private static bool $accept_reports = false;

/**
* @var array
* @config
*/
private static $allowed_actions = [
private static array $allowed_actions = [
'report'
];

/**
* @var array
* @config
*/
private static $url_handlers = [
private static array $url_handlers = [
'v1/report' => 'report'
];

public function index(HTTPRequest $request)
public function index(HTTPRequest $request): never
{
$this->returnHeader();
}

/**
* Return appropriate response header, only
*/
private function returnHeader()
private function returnHeader(): never
{
header("HTTP/1.1 204 No Content");
exit;
}

public static function getCurrentReportingUrl($include_host = true) : string
public static function getCurrentReportingUrl($include_host = true): string
{
return ($include_host ? Director::absoluteBaseURL() : '/') . 'csp/v1/report';
return ($include_host ? rtrim(Director::absoluteBaseURL(), '/') : '') . '/csp/v1/report';
}

/**
Expand All @@ -68,7 +64,7 @@ public function report(HTTPRequest $request)
// collect the body
try {

if(!self::config()->get('accept_reports')) {
if (!self::config()->get('accept_reports')) {
throw new \Exception("This endpoint does not accept reports");
}

Expand All @@ -78,27 +74,27 @@ public function report(HTTPRequest $request)

$contentType = $request->getHeader('Content-Type');
$acceptedContentTypes = [ 'application/csp-report', 'application/reports+json' ];
if(!in_array($contentType, $acceptedContentTypes)) {
if (!in_array($contentType, $acceptedContentTypes)) {
throw new \Exception("The request does not have an accepted content type");
}

$body = $request->getBody();
if(!$body) {
if (!$body) {
throw new \Exception("The body of the request is empty");
}

$report = json_decode($body, true);
if(json_last_error() !== JSON_ERROR_NONE) {
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception("CSP report JSON decode error: " . json_last_error_msg());
}

$violationReport = ViolationReport::create_report($report , $contentType);
$violationReport = ViolationReport::create_report($report, $contentType);

} catch (\Exception $e) {
Logger::log("ReportingEndpoint: " . $e->getMessage(), "NOTICE");
} finally {
$this->returnHeader();
} catch (\Exception $exception) {
Logger::log("ReportingEndpoint: " . $exception->getMessage(), "NOTICE");
}

$this->returnHeader();

}
}
3 changes: 2 additions & 1 deletion src/Extensions/ContentSecurityPolicyEnable.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
/**
* Apply this to relevant controller types to enable CSP header delivery
* @author James
* @extends \SilverStripe\Core\Extension<(\SilverStripe\Security\Security & static)>
*/
class ContentSecurityPolicyEnable extends Extension
{
public function EnableContentSecurityPolicy()
public function EnableContentSecurityPolicy(): bool
{
return true;
}
Expand Down
53 changes: 24 additions & 29 deletions src/Extensions/ControllerExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,72 +3,67 @@
namespace NSWDPC\Utilities\ContentSecurityPolicy;

use SilverStripe\Core\Extension;
use SilverStripe\Control\Director;
use SilverStripe\Core\Config\Config;
use SilverStripe\Admin\ModelAdmin;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Versioned\Versioned;
use SilverStripe\Admin\LeftAndMain;
use SilverStripe\CMS\Controllers\ContentController;
use SilverStripe\CMS\Controllers\ModelAsController;
use SilverStripe\CMS\Model\SiteTree;

/**
* Provides an extension method so that the Controller can set the relevant CSP header
* @extends \SilverStripe\Core\Extension<(\SilverStripe\Control\Controller & static)>
*/
class ControllerExtension extends Extension
{

public function onAfterInit()
{

// No response handling
$response = $this->owner->getResponse();
$response = $this->getOwner()->getResponse();
if ($response && !($response instanceof HTTPResponse)) {
return;
}

// Don't go in a loop reporting to the Reporting Endpoint controller from the Reporting Endpoint controller!
if ($this->owner instanceof ReportingEndpoint) {
if ($this->getOwner() instanceof ReportingEndpoint) {
return;
}

// check if a policy can be applied
if (!$canApply = Policy::checkCanApply($this->owner)) {
if (!$canApply = Policy::checkCanApply($this->getOwner())) {
return;
}

// check if request on the Live stage
$stage = Versioned::get_stage();
$is_live = ($stage == Versioned::LIVE);

// only get enabled policy/directives
$enabled_policy = $enabled_directives = true;
// only get enabled directives
$enabled_directives = true;

// Set the CSP nonce for this request
Nonce::getNonce();

$policy = Policy::getDefaultBasePolicy($is_live, Policy::POLICY_DELIVERY_METHOD_HEADER);

// check for Page specific policy
if ($this->owner instanceof ContentController
&& ($data = $this->owner->data())
if ($this->getOwner() instanceof ContentController
&& ($data = $this->getOwner()->data())
&& $data instanceof SiteTree) {
$page_policy = Policy::getPagePolicy($data, $is_live, Policy::POLICY_DELIVERY_METHOD_HEADER);
if (!empty($page_policy->ID)) {
if (!empty($policy->ID)) {
/**
* HTTPResponse can't handle header names that are duplicated (which is allowed in the HTTP spec)
* Workaround is to set the page policy for merging when getPolicyData() is called
* Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#Multiple_content_security_policies
* Ref: https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
*/
$policy->setMergeFromPolicy($page_policy);
} else {
// the page policy is *the* policy
$policy = $page_policy;
}
$page_policy = Policy::getPagePolicy($data, $is_live, Policy::POLICY_DELIVERY_METHOD_HEADER);
if (!empty($page_policy->ID)) {
if (!empty($policy->ID)) {
/**
* HTTPResponse can't handle header names that are duplicated (which is allowed in the HTTP spec)
* Workaround is to set the page policy for merging when getPolicyData() is called
* Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy#Multiple_content_security_policies
* Ref: https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
*/
$policy->setMergeFromPolicy($page_policy);
} else {
// the page policy is *the* policy
$policy = $page_policy;
}
}
}

// Add the policy/reporting header values
Expand All @@ -82,6 +77,7 @@ public function onAfterInit()
Policy::getReportingEndpointsHeader($data['reporting_endpoints'])
);
}

if (!empty($data['nel'])) {
// NEL is enabled
$response->addHeader(
Expand All @@ -93,10 +89,9 @@ public function onAfterInit()
json_encode($data['nel'])
);
}

// the relevant CSP-header with its values
$response->addHeader($data['header'], $data['policy_string']);
}

return;
}
}
Loading