Skip to content

Commit 7d948e9

Browse files
authored
Migrated CSP from 'seckit' to 'drupal/csp' with per-request nonce. (#166)
* Migrated CSP from 'seckit' to 'drupal/csp' with per-request nonce. * Addressed code review: tidied YAML, made nonce attachment additive, clarified settings comment.
1 parent c89dbf1 commit 7d948e9

10 files changed

Lines changed: 195 additions & 11 deletions

File tree

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"drupal/config_update": "^2@alpha",
2020
"drupal/core-composer-scaffold": "~11.3.7",
2121
"drupal/core-recommended": "~11.3.7",
22+
"drupal/csp": "^2.1",
2223
"drupal/devel": "^5.5.0",
2324
"drupal/diff": "^1.10",
2425
"drupal/entity_clone": "^2.1@beta",

composer.lock

Lines changed: 49 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/default/core.extension.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ module:
2525
content_moderation: 0
2626
contextual: 0
2727
crop: 0
28+
csp: 0
2829
ctools: 0
2930
datetime: 0
3031
datetime_range: 0

config/default/csp.settings.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
report-only:
2+
enable: false
3+
directives: {}
4+
reporting:
5+
plugin: none
6+
enforce:
7+
enable: true
8+
directives:
9+
default-src:
10+
base: self
11+
script-src:
12+
base: self
13+
sources:
14+
- https://www.googletagmanager.com
15+
- https://www.gstatic.com
16+
- https://www.recaptcha.net
17+
- https://www.google.com
18+
- https://cdnjs.cloudflare.com
19+
- https://cdn.jsdelivr.net/gh/cferdinandi/tabby@12.0.3/dist/js/tabby.min.js
20+
- https://unpkg.com/@popperjs/core@2.11.6/dist/umd/popper.js
21+
- https://unpkg.com/tippy.js@6.3.7/dist/tippy.umd.js
22+
object-src:
23+
base: none
24+
style-src:
25+
base: self
26+
flags:
27+
- unsafe-inline
28+
sources:
29+
- https://cdnjs.cloudflare.com/ajax/libs/highlight.js/
30+
- https://fonts.googleapis.com/
31+
- https://cdn.jsdelivr.net/gh/cferdinandi/tabby@12.0.3/dist/css/tabby-ui.min.css
32+
- https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.12/codemirror.css
33+
- https://unpkg.com/tippy.js@6.3.7/dist/tippy.css
34+
- https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css
35+
img-src:
36+
base: self
37+
sources:
38+
- 'data:'
39+
media-src:
40+
base: self
41+
frame-src:
42+
base: self
43+
sources:
44+
- https://www.youtube.com
45+
- https://www.recaptcha.net
46+
- https://www.google.com
47+
frame-ancestors:
48+
base: none
49+
font-src:
50+
base: self
51+
sources:
52+
- https://fonts.gstatic.com
53+
connect-src:
54+
base: self
55+
sources:
56+
- https://www.googletagmanager.com
57+
- https://www.google-analytics.com
58+
- https://www.recaptcha.net
59+
- https://www.google.com
60+
upgrade-insecure-requests: true
61+
reporting:
62+
plugin: uri
63+
options:
64+
uri: /report-csp-violation

config/default/seckit.settings.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
seckit_xss:
22
csp:
3-
checkbox: true
3+
checkbox: false
44
vendor-prefix:
55
x: true
66
webkit: true

tests/behat/features/csp.feature

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
@csp @p2
2+
Feature: Content Security Policy
3+
4+
As a site owner
5+
I want the Content-Security-Policy header to include a per-request nonce
6+
and match the previously enforced policy
7+
So that BigPipe and other Drupal inline scripts are not blocked by CSP
8+
and no source is silently widened or narrowed during the migration
9+
10+
@api
11+
Scenario: CSP header contains a nonce for anonymous users
12+
Given I am an anonymous user
13+
When I go to the homepage
14+
Then the response status code should be 200
15+
And the response header "Content-Security-Policy" should contain the value "'nonce-"
16+
17+
@api
18+
Scenario: CSP header contains a nonce for authenticated users
19+
Given I am logged in as a user with the "administrator" role
20+
When I go to the homepage
21+
Then the response status code should be 200
22+
And the response header "Content-Security-Policy" should contain the value "'nonce-"
23+
24+
@api
25+
Scenario: CSP policy preserves previously allowed sources
26+
Given I am an anonymous user
27+
When I go to the homepage
28+
Then the response status code should be 200
29+
And the response header "Content-Security-Policy" should contain the value "default-src 'self'"
30+
And the response header "Content-Security-Policy" should contain the value "object-src 'none'"
31+
And the response header "Content-Security-Policy" should contain the value "frame-ancestors 'none'"
32+
And the response header "Content-Security-Policy" should contain the value "report-uri /report-csp-violation"
33+
And the response header "Content-Security-Policy" should contain the value "https://www.googletagmanager.com"
34+
And the response header "Content-Security-Policy" should contain the value "https://www.recaptcha.net"
35+
And the response header "Content-Security-Policy" should contain the value "https://www.youtube.com"
36+
And the response header "Content-Security-Policy" should contain the value "https://fonts.gstatic.com"
37+
And the response header "Content-Security-Policy" should contain the value "https://www.google-analytics.com"

tests/behat/features/seckit.feature

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,10 @@ Feature: Seckit
66
In order to improve security and protect against common vulnerabilities
77

88
@api
9-
Scenario: Check for HSTS and CSP headers
9+
Scenario: Seckit emits the expected non-CSP security headers
1010
Given I am an anonymous user
1111
When I go to the homepage
1212
Then the response status code should be 200
13-
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;"
14-
And the response header "Content-Security-Policy" should contain the value "default-src 'self';"
15-
And the response header "Content-Security-Policy" should contain the value "font-src 'self' https://fonts.gstatic.com;"
16-
And the response header "Content-Security-Policy" should contain the value "img-src 'self' data"
17-
And the response header "Content-Security-Policy" should contain the value "media-src 'self'"
18-
And the response header "Content-Security-Policy" should contain the value "report-uri /report-csp-violation"
19-
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;"
20-
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;"
2113
And the response header "Strict-Transport-Security" should contain the value "max-age=31536000"
2214
And the response header "Strict-Transport-Security" should contain the value "includeSubDomains"
2315
And the response header "Strict-Transport-Security" should contain the value "preload"

tests/phpunit/Drupal/EnvironmentSettingsTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ public function testEnvironmentLocal(): void {
380380
$config['shield.settings']['shield_enable'] = FALSE;
381381
$config['system.logging']['error_level'] = 'all';
382382
$config['system.performance']['cache']['page']['max_age'] = 900;
383+
$config['csp.settings']['enforce']['directives']['upgrade-insecure-requests'] = FALSE;
383384
$config['purge_control.settings']['disable_purge'] = TRUE;
384385
$config['purge_control.settings']['purge_auto_control'] = FALSE;
385386
$config['seckit.settings']['seckit_xss']['csp']['upgrade-req'] = FALSE;
@@ -431,6 +432,7 @@ public function testEnvironmentLocalContainer(): void {
431432
$config['shield.settings']['shield_enable'] = FALSE;
432433
$config['system.logging']['error_level'] = 'all';
433434
$config['system.performance']['cache']['page']['max_age'] = 900;
435+
$config['csp.settings']['enforce']['directives']['upgrade-insecure-requests'] = FALSE;
434436
$config['purge_control.settings']['disable_purge'] = TRUE;
435437
$config['purge_control.settings']['purge_auto_control'] = FALSE;
436438
$config['seckit.settings']['seckit_xss']['csp']['upgrade-req'] = FALSE;
@@ -484,6 +486,7 @@ public function testEnvironmentGha(): void {
484486
$config['shield.settings']['shield_enable'] = FALSE;
485487
$config['system.logging']['error_level'] = 'all';
486488
$config['system.performance']['cache']['page']['max_age'] = 900;
489+
$config['csp.settings']['enforce']['directives']['upgrade-insecure-requests'] = FALSE;
487490
$config['purge_control.settings']['disable_purge'] = TRUE;
488491
$config['purge_control.settings']['purge_auto_control'] = FALSE;
489492
$config['seckit.settings']['seckit_xss']['csp']['upgrade-req'] = FALSE;

web/modules/custom/do_base/do_base.module

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
declare(strict_types=1);
99

10+
use Drupal\csp\Csp;
1011
use Drupal\Core\Site\Settings;
1112

1213
/**
@@ -20,3 +21,25 @@ function do_base_mail_alter(array &$message): void {
2021
$message['send'] = FALSE;
2122
}
2223
}
24+
25+
/**
26+
* Implements hook_page_attachments().
27+
*/
28+
function do_base_page_attachments(array &$attachments): void {
29+
// Attach a CSP nonce to script-src on every page so that Drupal core's
30+
// inline scripts (BigPipe placeholders, drupalSettings, etc.) continue to
31+
// run under a strict Content-Security-Policy. The fallback 'unsafe-inline'
32+
// is only used by browsers that do not support CSP3 nonces; modern
33+
// browsers ignore it when a nonce is present.
34+
if (!class_exists(Csp::class)) {
35+
return;
36+
}
37+
38+
$existing = $attachments['#attached']['csp_nonce']['script'] ?? [];
39+
$attachments['#attached']['csp_nonce']['script'] = array_values(array_unique(array_merge($existing, [Csp::POLICY_UNSAFE_INLINE])));
40+
41+
$libraries = $attachments['#attached']['library'] ?? [];
42+
if (!in_array('csp/nonce', $libraries, TRUE)) {
43+
$attachments['#attached']['library'][] = 'csp/nonce';
44+
}
45+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
/**
4+
* @file
5+
* CSP settings.
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
if ($settings['environment'] === ENVIRONMENT_CI || $settings['environment'] === ENVIRONMENT_LOCAL) {
11+
// Disable only the 'upgrade-insecure-requests' directive locally and in CI
12+
// where the site is served over HTTP. The rest of the CSP policy still
13+
// applies.
14+
$config['csp.settings']['enforce']['directives']['upgrade-insecure-requests'] = FALSE;
15+
}

0 commit comments

Comments
 (0)