From 4497a755a22e4d38317151ec0dfd20b912e1803a Mon Sep 17 00:00:00 2001 From: Lari Taskula Date: Mon, 27 Oct 2025 19:14:07 +0200 Subject: [PATCH] Bug 38365: Add CSP to C4::Output::output_with_http_headers() To test: 1. prove t/db_dependent/Output.t 2. prove t/Output.t --- C4/Output.pm | 7 ++ t/db_dependent/Output.t | 159 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 165 insertions(+), 1 deletion(-) diff --git a/C4/Output.pm b/C4/Output.pm index 3aaf1c05838..67fb962703c 100644 --- a/C4/Output.pm +++ b/C4/Output.pm @@ -34,6 +34,8 @@ use C4::Auth qw( get_template_and_user ); use C4::Context; use C4::Templates; +use Koha::ContentSecurityPolicy; + our ( @ISA, @EXPORT_OK ); BEGIN { @@ -270,6 +272,11 @@ sub output_with_http_headers { $options->{'Content-Script-Type'} = 'text/javascript'; } + my $csp = Koha::ContentSecurityPolicy->new; + if ( $csp->is_enabled ) { + $options->{ $csp->header_name } = $csp->header_value if $csp->header_value; + } + # We can't encode here, that will double encode our templates, and xslt # We need to fix the encoding as it comes out of the database, or when we pass the variables to templates diff --git a/t/db_dependent/Output.t b/t/db_dependent/Output.t index 5ed9b6e6a54..d3af1bd1781 100755 --- a/t/db_dependent/Output.t +++ b/t/db_dependent/Output.t @@ -17,7 +17,7 @@ use Modern::Perl; -use Test::More tests => 3; +use Test::More tests => 4; use Test::MockModule; use Test::NoWarnings; use Test::Warn; @@ -160,3 +160,160 @@ subtest 'redirect_if_opac_suppressed() tests' => sub { $schema->storage->txn_rollback; }; + +subtest 'Content-Security-Policy tests' => sub { + plan tests => 1; + + subtest 'OPAC' => sub { + + plan tests => 3; + + subtest 'turned off' => sub { + + plan tests => 2; + + $schema->storage->txn_begin; + + t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' ); + + my $query = CGI->new(); + + { + local *STDOUT; + my $stdout; + open STDOUT, '>', \$stdout; + + my ( $template, $borrowernumber, $cookie ) = C4::Auth::get_template_and_user( + { + template_name => "opac-main.tt", + type => "opac", + query => $query, + authnotrequired => 1, + } + ); + + C4::Output::output_html_with_http_headers( $query, $cookie, $template->output ); + + unlike( $stdout, qr{Content-Security-Policy:}, 'No header present: Content-Security-Policy' ); + unlike( + $stdout, qr{Content-Security-Policy-Report-Only:}, + 'No header present: Content-Security-Policy-Report-Only' + ); + + close STDOUT; + }; + + $schema->storage->txn_rollback; + + }; + + subtest 'report-only' => sub { + + plan tests => 3; + + $schema->storage->txn_begin; + + t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' ); + + C4::Context->interface('opac'); + + my $expected_csp_header = 'Content-security-policy-report-only'; + my $expected_csp_header_value = + "default-src 'self'; script-src 'self' 'unsafe-eval' _CSP_NONCE_; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; object-src 'none'"; + t::lib::Mocks::mock_config( + 'content_security_policy', + { 'opac' => { csp_mode => 'report-only', csp_header_value => $expected_csp_header_value } } + ); + $expected_csp_header_value =~ s/_CSP_NONCE_//g; + + my $query = CGI->new(); + + { + local *STDOUT; + my $stdout; + open STDOUT, '>', \$stdout; + + my ( $template, $borrowernumber, $cookie ) = C4::Auth::get_template_and_user( + { + template_name => "opac-main.tt", + type => "opac", + query => $query, + authnotrequired => 1, + } + ); + + C4::Output::output_html_with_http_headers( $query, $cookie, $template->output ); + + $stdout =~ /(Content-Security-Policy[A-Za-z\-]*): (.*)\r/im; + my ( $csp_header, $csp_header_value ) = ( $1, $2 ); + isnt( + $csp_header, 'Content-Security-Policy', + 'No header present: Content-Security-Policy' + ); + is( $csp_header, $expected_csp_header, 'Header present: Content-Security-Policy-Report-Only' ); + is( + $csp_header_value, $expected_csp_header_value, + 'Content-Security-Policy-Report-Only value matches expected value' + ); + + close STDOUT; + }; + + $schema->storage->txn_rollback; + + }; + + subtest 'enabled' => sub { + + plan tests => 3; + + $schema->storage->txn_begin; + + t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' ); + + C4::Context->interface('opac'); + + my $expected_csp_header = 'Content-security-policy'; + my $expected_csp_header_value = + "default-src 'self'; script-src 'self' 'unsafe-eval' _CSP_NONCE_; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; object-src 'none'"; + t::lib::Mocks::mock_config( + 'content_security_policy', + { 'opac' => { csp_mode => 'enabled', csp_header_value => $expected_csp_header_value } } + ); + $expected_csp_header_value =~ s/_CSP_NONCE_//g; + + my $query = CGI->new(); + + { + local *STDOUT; + my $stdout; + open STDOUT, '>', \$stdout; + + my ( $template, $borrowernumber, $cookie ) = C4::Auth::get_template_and_user( + { + template_name => "opac-main.tt", + type => "opac", + query => $query, + authnotrequired => 1, + } + ); + + C4::Output::output_html_with_http_headers( $query, $cookie, $template->output ); + + $stdout =~ /(Content-Security-Policy[A-Za-z\-]*): (.*)\r/im; + my ( $csp_header, $csp_header_value ) = ( $1, $2 ); + isnt( + $csp_header, 'Content-Security-Policy-Report-Only:', + 'No header present: Content-Security-Policy-Report-Only' + ); + is( $csp_header, $expected_csp_header, 'Header present: Content-Security-Policy' ); + is( $csp_header_value, $expected_csp_header_value, 'Content-Security-Policy value matches expected value' ); + + close STDOUT; + }; + + $schema->storage->txn_rollback; + + }; + }; +}; -- 2.34.1