diff --git a/composer.json b/composer.json index d231ae6..cb4ea01 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "drupal/config_update": "^2@alpha", "drupal/core-composer-scaffold": "~11.3.7", "drupal/core-recommended": "~11.3.7", + "drupal/csp": "^2.1", "drupal/devel": "^5.5.0", "drupal/diff": "^1.10", "drupal/entity_clone": "^2.1@beta", diff --git a/composer.lock b/composer.lock index 6a0d0cc..d7f926c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "aed09bf30f1da38ecf6377aeed9c3cb9", + "content-hash": "7336a80abed6f600d3e7e9d0f72271fd", "packages": [ { "name": "asm89/stack-cors", @@ -2798,6 +2798,54 @@ "issues": "https://www.drupal.org/project/issues/crop" } }, + { + "name": "drupal/csp", + "version": "2.2.3", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/csp.git", + "reference": "2.2.3" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/csp-2.2.3.zip", + "reference": "2.2.3", + "shasum": "a02939b221ecb66273530d7be3b2a287f897fa3c" + }, + "require": { + "drupal/core": "^11.2 || ^12" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "2.2.3", + "datestamp": "1776729271", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "gapple", + "homepage": "https://www.drupal.org/user/490940" + } + ], + "description": "Provide Content-Security-Policy headers", + "homepage": "https://www.drupal.org/project/csp", + "keywords": [ + "Drupal" + ], + "support": { + "source": "https://git.drupalcode.org/project/csp", + "issues": "https://www.drupal.org/project/issues/csp" + } + }, { "name": "drupal/ctools", "version": "4.1.0", diff --git a/config/default/core.extension.yml b/config/default/core.extension.yml index c7f9014..d747b3a 100644 --- a/config/default/core.extension.yml +++ b/config/default/core.extension.yml @@ -25,6 +25,7 @@ module: content_moderation: 0 contextual: 0 crop: 0 + csp: 0 ctools: 0 datetime: 0 datetime_range: 0 diff --git a/config/default/csp.settings.yml b/config/default/csp.settings.yml new file mode 100644 index 0000000..3b195f5 --- /dev/null +++ b/config/default/csp.settings.yml @@ -0,0 +1,64 @@ +report-only: + enable: false + directives: {} + reporting: + plugin: none +enforce: + enable: true + directives: + default-src: + base: self + script-src: + base: self + sources: + - https://www.googletagmanager.com + - https://www.gstatic.com + - https://www.recaptcha.net + - https://www.google.com + - https://cdnjs.cloudflare.com + - https://cdn.jsdelivr.net/gh/cferdinandi/tabby@12.0.3/dist/js/tabby.min.js + - https://unpkg.com/@popperjs/core@2.11.6/dist/umd/popper.js + - https://unpkg.com/tippy.js@6.3.7/dist/tippy.umd.js + object-src: + base: none + style-src: + base: self + flags: + - unsafe-inline + sources: + - https://cdnjs.cloudflare.com/ajax/libs/highlight.js/ + - https://fonts.googleapis.com/ + - https://cdn.jsdelivr.net/gh/cferdinandi/tabby@12.0.3/dist/css/tabby-ui.min.css + - https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.12/codemirror.css + - https://unpkg.com/tippy.js@6.3.7/dist/tippy.css + - https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css + img-src: + base: self + sources: + - 'data:' + media-src: + base: self + frame-src: + base: self + sources: + - https://www.youtube.com + - https://www.recaptcha.net + - https://www.google.com + frame-ancestors: + base: none + font-src: + base: self + sources: + - https://fonts.gstatic.com + connect-src: + base: self + sources: + - https://www.googletagmanager.com + - https://www.google-analytics.com + - https://www.recaptcha.net + - https://www.google.com + upgrade-insecure-requests: true + reporting: + plugin: uri + options: + uri: /report-csp-violation diff --git a/config/default/seckit.settings.yml b/config/default/seckit.settings.yml index 029bdb3..b01d61b 100644 --- a/config/default/seckit.settings.yml +++ b/config/default/seckit.settings.yml @@ -1,6 +1,6 @@ seckit_xss: csp: - checkbox: true + checkbox: false vendor-prefix: x: true webkit: true diff --git a/tests/behat/features/csp.feature b/tests/behat/features/csp.feature new file mode 100644 index 0000000..09d046f --- /dev/null +++ b/tests/behat/features/csp.feature @@ -0,0 +1,37 @@ +@csp @p2 +Feature: Content Security Policy + + As a site owner + I want the Content-Security-Policy header to include a per-request nonce + and match the previously enforced policy + So that BigPipe and other Drupal inline scripts are not blocked by CSP + and no source is silently widened or narrowed during the migration + + @api + Scenario: CSP header contains a nonce for anonymous users + Given I am an anonymous user + When I go to the homepage + Then the response status code should be 200 + And the response header "Content-Security-Policy" should contain the value "'nonce-" + + @api + Scenario: CSP header contains a nonce for authenticated users + Given I am logged in as a user with the "administrator" role + When I go to the homepage + Then the response status code should be 200 + And the response header "Content-Security-Policy" should contain the value "'nonce-" + + @api + Scenario: CSP policy preserves previously allowed sources + Given I am an anonymous user + When I go to the homepage + Then the response status code should be 200 + And the response header "Content-Security-Policy" should contain the value "default-src 'self'" + And the response header "Content-Security-Policy" should contain the value "object-src 'none'" + And the response header "Content-Security-Policy" should contain the value "frame-ancestors 'none'" + And the response header "Content-Security-Policy" should contain the value "report-uri /report-csp-violation" + And the response header "Content-Security-Policy" should contain the value "https://www.googletagmanager.com" + And the response header "Content-Security-Policy" should contain the value "https://www.recaptcha.net" + And the response header "Content-Security-Policy" should contain the value "https://www.youtube.com" + And the response header "Content-Security-Policy" should contain the value "https://fonts.gstatic.com" + And the response header "Content-Security-Policy" should contain the value "https://www.google-analytics.com" diff --git a/tests/behat/features/seckit.feature b/tests/behat/features/seckit.feature index 5094c44..775ca76 100644 --- a/tests/behat/features/seckit.feature +++ b/tests/behat/features/seckit.feature @@ -6,18 +6,10 @@ Feature: Seckit In order to improve security and protect against common vulnerabilities @api - Scenario: Check for HSTS and CSP headers + Scenario: Seckit emits the expected non-CSP security headers Given I am an anonymous user When I go to the homepage Then the response status code should be 200 - And the response header "Content-Security-Policy" should contain the value "connect-src 'self' https://www.googletagmanager.com https://www.google-analytics.com https://www.recaptcha.net https://www.google.com;" - And the response header "Content-Security-Policy" should contain the value "default-src 'self';" - And the response header "Content-Security-Policy" should contain the value "font-src 'self' https://fonts.gstatic.com;" - And the response header "Content-Security-Policy" should contain the value "img-src 'self' data" - And the response header "Content-Security-Policy" should contain the value "media-src 'self'" - And the response header "Content-Security-Policy" should contain the value "report-uri /report-csp-violation" - And the response header "Content-Security-Policy" should contain the value "script-src 'self' https://www.googletagmanager.com https://www.gstatic.com https://www.recaptcha.net https://www.google.com https://cdnjs.cloudflare.com https://cdn.jsdelivr.net/gh/cferdinandi/tabby@12.0.3/dist/js/tabby.min.js https://unpkg.com/@popperjs/core@2.11.6/dist/umd/popper.js https://unpkg.com/tippy.js@6.3.7/dist/tippy.umd.js;" - And the response header "Content-Security-Policy" should contain the value "style-src 'self' https://cdnjs.cloudflare.com/ajax/libs/highlight.js/ 'unsafe-inline' https://fonts.googleapis.com/ https://cdn.jsdelivr.net/gh/cferdinandi/tabby@12.0.3/dist/css/tabby-ui.min.css https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.12/codemirror.css https://unpkg.com/tippy.js@6.3.7/dist/tippy.css https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css;" And the response header "Strict-Transport-Security" should contain the value "max-age=31536000" And the response header "Strict-Transport-Security" should contain the value "includeSubDomains" And the response header "Strict-Transport-Security" should contain the value "preload" diff --git a/tests/phpunit/Drupal/EnvironmentSettingsTest.php b/tests/phpunit/Drupal/EnvironmentSettingsTest.php index adbb01e..f91c75e 100644 --- a/tests/phpunit/Drupal/EnvironmentSettingsTest.php +++ b/tests/phpunit/Drupal/EnvironmentSettingsTest.php @@ -380,6 +380,7 @@ public function testEnvironmentLocal(): void { $config['shield.settings']['shield_enable'] = FALSE; $config['system.logging']['error_level'] = 'all'; $config['system.performance']['cache']['page']['max_age'] = 900; + $config['csp.settings']['enforce']['directives']['upgrade-insecure-requests'] = FALSE; $config['purge_control.settings']['disable_purge'] = TRUE; $config['purge_control.settings']['purge_auto_control'] = FALSE; $config['seckit.settings']['seckit_xss']['csp']['upgrade-req'] = FALSE; @@ -431,6 +432,7 @@ public function testEnvironmentLocalContainer(): void { $config['shield.settings']['shield_enable'] = FALSE; $config['system.logging']['error_level'] = 'all'; $config['system.performance']['cache']['page']['max_age'] = 900; + $config['csp.settings']['enforce']['directives']['upgrade-insecure-requests'] = FALSE; $config['purge_control.settings']['disable_purge'] = TRUE; $config['purge_control.settings']['purge_auto_control'] = FALSE; $config['seckit.settings']['seckit_xss']['csp']['upgrade-req'] = FALSE; @@ -484,6 +486,7 @@ public function testEnvironmentGha(): void { $config['shield.settings']['shield_enable'] = FALSE; $config['system.logging']['error_level'] = 'all'; $config['system.performance']['cache']['page']['max_age'] = 900; + $config['csp.settings']['enforce']['directives']['upgrade-insecure-requests'] = FALSE; $config['purge_control.settings']['disable_purge'] = TRUE; $config['purge_control.settings']['purge_auto_control'] = FALSE; $config['seckit.settings']['seckit_xss']['csp']['upgrade-req'] = FALSE; diff --git a/web/modules/custom/do_base/do_base.module b/web/modules/custom/do_base/do_base.module index cba1f4f..d2892f1 100644 --- a/web/modules/custom/do_base/do_base.module +++ b/web/modules/custom/do_base/do_base.module @@ -7,6 +7,7 @@ declare(strict_types=1); +use Drupal\csp\Csp; use Drupal\Core\Site\Settings; /** @@ -20,3 +21,25 @@ function do_base_mail_alter(array &$message): void { $message['send'] = FALSE; } } + +/** + * Implements hook_page_attachments(). + */ +function do_base_page_attachments(array &$attachments): void { + // Attach a CSP nonce to script-src on every page so that Drupal core's + // inline scripts (BigPipe placeholders, drupalSettings, etc.) continue to + // run under a strict Content-Security-Policy. The fallback 'unsafe-inline' + // is only used by browsers that do not support CSP3 nonces; modern + // browsers ignore it when a nonce is present. + if (!class_exists(Csp::class)) { + return; + } + + $existing = $attachments['#attached']['csp_nonce']['script'] ?? []; + $attachments['#attached']['csp_nonce']['script'] = array_values(array_unique(array_merge($existing, [Csp::POLICY_UNSAFE_INLINE]))); + + $libraries = $attachments['#attached']['library'] ?? []; + if (!in_array('csp/nonce', $libraries, TRUE)) { + $attachments['#attached']['library'][] = 'csp/nonce'; + } +} diff --git a/web/sites/default/includes/modules/settings.csp.php b/web/sites/default/includes/modules/settings.csp.php new file mode 100644 index 0000000..8003eaf --- /dev/null +++ b/web/sites/default/includes/modules/settings.csp.php @@ -0,0 +1,15 @@ +