From 533360f9c5bd7daf5f067f52715edd39ff8df7eb Mon Sep 17 00:00:00 2001 From: David Cook Date: Wed, 4 Feb 2026 02:00:44 +0000 Subject: [PATCH] Bug 38365: Fix nonce caching and add to framework plugin scripts Signed-off-by: David Cook Signed-off-by: Lari Taskula Signed-off-by: David Cook --- Koha/ContentSecurityPolicy.pm | 60 +++++++++++-------- Koha/FrameworkPlugin.pm | 11 +++- Koha/Middleware/ContentSecurityPolicy.pm | 1 + Koha/Template/Plugin/Koha.pm | 2 +- t/Koha/ContentSecurityPolicy.t | 19 +++--- t/Koha/Middleware/ContentSecurityPolicy.t | 5 +- .../Koha/Middleware/ContentSecurityPolicy.t | 15 ++--- t/db_dependent/Koha/Template/Plugin/Koha.t | 2 +- 8 files changed, 61 insertions(+), 54 deletions(-) diff --git a/Koha/ContentSecurityPolicy.pm b/Koha/ContentSecurityPolicy.pm index de17672e813..d112b586154 100644 --- a/Koha/ContentSecurityPolicy.pm +++ b/Koha/ContentSecurityPolicy.pm @@ -57,7 +57,7 @@ sub new { my ( $class, $params ) = @_; my $self = bless $params // {}, $class; - $self->nonce( $params->{nonce} ); + $self->set_nonce( $params->{nonce} ) if $params->{nonce}; $self->{config} = Koha::Config->get_instance; return $self; } @@ -127,7 +127,7 @@ sub header_value { unless exists $conf_csp->{$interface}->{csp_header_value}; my $csp_header_value = $conf_csp->{$interface}->{csp_header_value}; - my $csp_nonce = $self->nonce; + my $csp_nonce = $self->get_nonce; $csp_header_value =~ s/_CSP_NONCE_/$csp_nonce/g; @@ -166,44 +166,52 @@ sub is_enabled { return 0; } -=head2 nonce +=head2 get_nonce - $csp->nonce(); + $csp->get_nonce(); - Generates and returns the nonce. + Returns the previously set nonce. - $csp->nonce($nonce); + A CSP nonce is a random token that is used both in the inline scripts + and the Content-Security-Policy[-Report-Only] response header. + +=cut + +sub get_nonce { + my ($self) = @_; + + #Koha::Middleware::ContentSecurityPolicy sets an environmental variable + #We cannot use the L1 cache since it is cleared after the middleware is applied + my $env_value = $ENV{CSP_NONCE}; + + return $env_value; +} + +=head2 set_nonce - Sets and returns the nonce. + $csp->set_nonce($nonce); + + Set the nonce value. + + If value is provided, that value is used. Otherwise, a value is generated. A CSP nonce is a random token that is used both in the inline scripts and the Content-Security-Policy[-Report-Only] response header. =cut -sub nonce { +sub set_nonce { my ( $self, $nonce ) = @_; - my $cache = Koha::Cache::Memory::Lite->new; + #Koha::Middleware::ContentSecurityPolicy sets an environmental variable + #We cannot use the L1 cache since it is cleared after the middleware is applied if ($nonce) { - $cache->set_in_cache( 'CSP-NONCE', $nonce ); - $self->{nonce} = $nonce; - return $self->{nonce}; - } - - if ( $nonce = $cache->get_from_cache('CSP-NONCE') ) { - $self->{nonce} = $nonce; - return $self->{nonce}; + $ENV{CSP_NONCE} = $nonce; + } else { + my $nonce = Koha::Token->new()->generate( { pattern => '\w{22}' } ); + $ENV{CSP_NONCE} = $nonce; } - - unless ( $self->{nonce} ) { - $nonce = Koha::Token->new()->generate( { pattern => '\w{22}' } ); - $self->{nonce} = $nonce; - } - - $cache->set_in_cache( 'CSP-NONCE', $self->{nonce} ); - - return $self->{nonce}; + return 1; } 1; diff --git a/Koha/FrameworkPlugin.pm b/Koha/FrameworkPlugin.pm index 844957bf0d1..d7d8f61920f 100644 --- a/Koha/FrameworkPlugin.pm +++ b/Koha/FrameworkPlugin.pm @@ -117,6 +117,7 @@ use Cwd qw//; use base qw(Class::Accessor); use C4::Context; +use Koha::ContentSecurityPolicy; __PACKAGE__->mk_ro_accessors( qw| @@ -137,7 +138,11 @@ __PACKAGE__->mk_ro_accessors( sub new { my ( $class, $params ) = @_; - my $self = $class->SUPER::new(); + my $self = $class->SUPER::new(); + my $csp_nonce = Koha::ContentSecurityPolicy->new->get_nonce; + if ($csp_nonce) { + $self->{csp_nonce} = $csp_nonce; + } if ( ref($params) eq 'HASH' ) { foreach ( 'name', 'path', 'item_style' ) { $self->{$_} = $params->{$_}; @@ -356,6 +361,8 @@ sub _generate_js { sub _process_javascript { my ( $self, $params, $script ) = @_; + my $csp_nonce = $self->{csp_nonce}; + #remove the script tags; we add them again later $script =~ s/\]*\>\s*(\/\/\\s*)?\<\/script\>//s; @@ -372,7 +379,7 @@ sub _process_javascript { } $self->{noclick} = !$clickfound; $self->{javascript} = < +