View | Details | Raw Unified | Return to bug 38365
Collapse All | Expand All

(-)a/Koha/ContentSecurityPolicy.pm (-26 / +34 lines)
Lines 57-63 sub new { Link Here
57
    my ( $class, $params ) = @_;
57
    my ( $class, $params ) = @_;
58
    my $self = bless $params // {}, $class;
58
    my $self = bless $params // {}, $class;
59
59
60
    $self->nonce( $params->{nonce} );
60
    $self->set_nonce( $params->{nonce} ) if $params->{nonce};
61
    $self->{config} = Koha::Config->get_instance;
61
    $self->{config} = Koha::Config->get_instance;
62
    return $self;
62
    return $self;
63
}
63
}
Lines 127-133 sub header_value { Link Here
127
        unless exists $conf_csp->{$interface}->{csp_header_value};
127
        unless exists $conf_csp->{$interface}->{csp_header_value};
128
128
129
    my $csp_header_value = $conf_csp->{$interface}->{csp_header_value};
129
    my $csp_header_value = $conf_csp->{$interface}->{csp_header_value};
130
    my $csp_nonce        = $self->nonce;
130
    my $csp_nonce        = $self->get_nonce;
131
131
132
    $csp_header_value =~ s/_CSP_NONCE_/$csp_nonce/g;
132
    $csp_header_value =~ s/_CSP_NONCE_/$csp_nonce/g;
133
133
Lines 166-209 sub is_enabled { Link Here
166
    return 0;
166
    return 0;
167
}
167
}
168
168
169
=head2 nonce
169
=head2 get_nonce
170
170
171
    $csp->nonce();
171
    $csp->get_nonce();
172
172
173
    Generates and returns the nonce.
173
    Returns the previously set nonce.
174
174
175
    $csp->nonce($nonce);
175
    A CSP nonce is a random token that is used both in the inline scripts
176
    and the Content-Security-Policy[-Report-Only] response header.
177
178
=cut
179
180
sub get_nonce {
181
    my ($self) = @_;
182
183
    #Koha::Middleware::ContentSecurityPolicy sets an environmental variable
184
    #We cannot use the L1 cache since it is cleared after the middleware is applied
185
    my $env_value = $ENV{CSP_NONCE};
186
187
    return $env_value;
188
}
189
190
=head2 set_nonce
176
191
177
    Sets and returns the nonce.
192
    $csp->set_nonce($nonce);
193
194
    Set the nonce value.
195
196
    If value is provided, that value is used. Otherwise, a value is generated.
178
197
179
    A CSP nonce is a random token that is used both in the inline scripts
198
    A CSP nonce is a random token that is used both in the inline scripts
180
    and the Content-Security-Policy[-Report-Only] response header.
199
    and the Content-Security-Policy[-Report-Only] response header.
181
200
182
=cut
201
=cut
183
202
184
sub nonce {
203
sub set_nonce {
185
    my ( $self, $nonce ) = @_;
204
    my ( $self, $nonce ) = @_;
186
    my $cache = Koha::Cache::Memory::Lite->new;
187
205
206
    #Koha::Middleware::ContentSecurityPolicy sets an environmental variable
207
    #We cannot use the L1 cache since it is cleared after the middleware is applied
188
    if ($nonce) {
208
    if ($nonce) {
189
        $cache->set_in_cache( 'CSP-NONCE', $nonce );
209
        $ENV{CSP_NONCE} = $nonce;
190
        $self->{nonce} = $nonce;
210
    } else {
191
        return $self->{nonce};
211
        my $nonce = Koha::Token->new()->generate( { pattern => '\w{22}' } );
192
    }
212
        $ENV{CSP_NONCE} = $nonce;
193
194
    if ( $nonce = $cache->get_from_cache('CSP-NONCE') ) {
195
        $self->{nonce} = $nonce;
196
        return $self->{nonce};
197
    }
213
    }
198
214
    return 1;
199
    unless ( $self->{nonce} ) {
200
        $nonce = Koha::Token->new()->generate( { pattern => '\w{22}' } );
201
        $self->{nonce} = $nonce;
202
    }
203
204
    $cache->set_in_cache( 'CSP-NONCE', $self->{nonce} );
205
206
    return $self->{nonce};
207
}
215
}
208
216
209
1;
217
1;
(-)a/Koha/FrameworkPlugin.pm (-2 / +9 lines)
Lines 117-122 use Cwd qw//; Link Here
117
use base qw(Class::Accessor);
117
use base qw(Class::Accessor);
118
118
119
use C4::Context;
119
use C4::Context;
120
use Koha::ContentSecurityPolicy;
120
121
121
__PACKAGE__->mk_ro_accessors(
122
__PACKAGE__->mk_ro_accessors(
122
    qw|
123
    qw|
Lines 137-143 __PACKAGE__->mk_ro_accessors( Link Here
137
138
138
sub new {
139
sub new {
139
    my ( $class, $params ) = @_;
140
    my ( $class, $params ) = @_;
140
    my $self = $class->SUPER::new();
141
    my $self      = $class->SUPER::new();
142
    my $csp_nonce = Koha::ContentSecurityPolicy->new->get_nonce;
143
    if ($csp_nonce) {
144
        $self->{csp_nonce} = $csp_nonce;
145
    }
141
    if ( ref($params) eq 'HASH' ) {
146
    if ( ref($params) eq 'HASH' ) {
142
        foreach ( 'name', 'path', 'item_style' ) {
147
        foreach ( 'name', 'path', 'item_style' ) {
143
            $self->{$_} = $params->{$_};
148
            $self->{$_} = $params->{$_};
Lines 356-361 sub _generate_js { Link Here
356
sub _process_javascript {
361
sub _process_javascript {
357
    my ( $self, $params, $script ) = @_;
362
    my ( $self, $params, $script ) = @_;
358
363
364
    my $csp_nonce = $self->{csp_nonce};
365
359
    #remove the script tags; we add them again later
366
    #remove the script tags; we add them again later
360
    $script =~ s/\<script[^>]*\>\s*(\/\/\<!\[CDATA\[)?\s*//s;
367
    $script =~ s/\<script[^>]*\>\s*(\/\/\<!\[CDATA\[)?\s*//s;
361
    $script =~ s/(\/\/\]\]\>\s*)?\<\/script\>//s;
368
    $script =~ s/(\/\/\]\]\>\s*)?\<\/script\>//s;
Lines 372-378 sub _process_javascript { Link Here
372
    }
379
    }
373
    $self->{noclick}    = !$clickfound;
380
    $self->{noclick}    = !$clickfound;
374
    $self->{javascript} = <<JS;
381
    $self->{javascript} = <<JS;
375
<script>
382
<script nonce="$csp_nonce">
376
\$(document).ready(function () {
383
\$(document).ready(function () {
377
$script
384
$script
378
});
385
});
(-)a/Koha/Middleware/ContentSecurityPolicy.pm (+1 lines)
Lines 39-44 sub call { Link Here
39
    my $req = Plack::Request->new($env);
39
    my $req = Plack::Request->new($env);
40
40
41
    my $csp = Koha::ContentSecurityPolicy->new;
41
    my $csp = Koha::ContentSecurityPolicy->new;
42
    $csp->set_nonce();
42
43
43
    my $res = $self->app->($env);
44
    my $res = $self->app->($env);
44
    return $res unless $csp->is_enabled;
45
    return $res unless $csp->is_enabled;
(-)a/Koha/Template/Plugin/Koha.pm (-1 / +1 lines)
Lines 172-178 It should be cached by Koha::Middleware::ContentSecurityPolicy. Link Here
172
=cut
172
=cut
173
173
174
sub CSPNonce {
174
sub CSPNonce {
175
    return Koha::ContentSecurityPolicy->new->nonce;
175
    return Koha::ContentSecurityPolicy->new->get_nonce;
176
}
176
}
177
177
178
1;
178
1;
(-)a/t/Koha/ContentSecurityPolicy.t (-11 / +8 lines)
Lines 140-149 subtest 'header_value tests' => sub { Link Here
140
        $conf_csp_section,
140
        $conf_csp_section,
141
        { opac => { csp_header_value => 'begin nonce-_CSP_NONCE_ end' } }
141
        { opac => { csp_header_value => 'begin nonce-_CSP_NONCE_ end' } }
142
    );
142
    );
143
    my $cache = Koha::Cache::Memory::Lite->get_instance();
143
    $csp->set_nonce('cached value');
144
    $cache->set_in_cache( 'CSP-NONCE', 'cached value' );
145
    is( $csp->header_value, 'begin nonce-cached value end', 'Cached csp_header_value' );
144
    is( $csp->header_value, 'begin nonce-cached value end', 'Cached csp_header_value' );
146
    $cache->flush;
147
145
148
    $csp = Koha::ContentSecurityPolicy->new( { nonce => 'forced value' } );
146
    $csp = Koha::ContentSecurityPolicy->new( { nonce => 'forced value' } );
149
    is( $csp->header_value, 'begin nonce-forced value end', 'Forced csp_header_value' );
147
    is( $csp->header_value, 'begin nonce-forced value end', 'Forced csp_header_value' );
Lines 158-178 subtest 'nonce tests' => sub { Link Here
158
    my $csp = Koha::ContentSecurityPolicy->new;
156
    my $csp = Koha::ContentSecurityPolicy->new;
159
157
160
    $csp = Koha::ContentSecurityPolicy->new( { nonce => 'forced value' } );
158
    $csp = Koha::ContentSecurityPolicy->new( { nonce => 'forced value' } );
161
    is( $csp->nonce, 'forced value', 'nonce is not re-generated as it was passed to new()' );
159
    is( $csp->get_nonce, 'forced value', 'nonce is not re-generated as it was passed to new()' );
162
160
163
    $csp = Koha::ContentSecurityPolicy->new( { nonce => 'forced value' } );
161
    $csp = Koha::ContentSecurityPolicy->new( { nonce => 'forced value' } );
164
    is( $csp->nonce, 'forced value', 'nonce is not re-generated as was cached in memory' );
162
    is( $csp->get_nonce, 'forced value', 'nonce is not re-generated as was cached in memory' );
165
163
166
    my $cache = Koha::Cache::Memory::Lite->get_instance();
164
    $csp->set_nonce();
167
    $cache->flush;
168
165
169
    $csp = Koha::ContentSecurityPolicy->new();
166
    $csp = Koha::ContentSecurityPolicy->new();
170
    my $nonce = $csp->nonce;
167
    my $nonce = $csp->get_nonce;
171
    like( $nonce, qr/\w{22}/, 'nonce is a random string of 22 characters (128+ bits entropy)' );
168
    like( $nonce, qr/\w{22}/, 'nonce is a random string of 22 characters (128+ bits entropy)' );
172
    is( $nonce, $csp->nonce, 're-calling nonce() returns the same randomly generated cached value' );
169
    is( $nonce, $csp->get_nonce, 're-calling nonce() returns the same randomly generated cached value' );
173
170
174
    $cache->set_in_cache( 'CSP-NONCE', 'cached value' );
171
    $csp->set_nonce('cached value');
175
    is( $csp->nonce, 'cached value', 'nonce is not re-generated as it was previously cached' );
172
    is( $csp->get_nonce, 'cached value', 'nonce is not re-generated as it was previously cached' );
176
};
173
};
177
174
178
1;
175
1;
(-)a/t/Koha/Middleware/ContentSecurityPolicy.t (-4 / +1 lines)
Lines 41-47 my $conf_csp_section = 'content_security_policy'; Link Here
41
subtest 'test Content-Security-Policy header' => sub {
41
subtest 'test Content-Security-Policy header' => sub {
42
    plan tests => 3;
42
    plan tests => 3;
43
43
44
    my $test_nonce = 'TEST_NONCE';
45
    my $csp_header_value =
44
    my $csp_header_value =
46
        "default-src 'self'; script-src 'self' 'nonce-_CSP_NONCE_'; style-src 'self' 'nonce-_CSP_NONCE_'; img-src 'self' data:; font-src 'self'; object-src 'none'";
45
        "default-src 'self'; script-src 'self' 'nonce-_CSP_NONCE_'; style-src 'self' 'nonce-_CSP_NONCE_'; img-src 'self' data:; font-src 'self'; object-src 'none'";
47
46
Lines 50-58 subtest 'test Content-Security-Policy header' => sub { Link Here
50
        { opac => { csp_mode => 'enabled', csp_header_value => $csp_header_value } }
49
        { opac => { csp_mode => 'enabled', csp_header_value => $csp_header_value } }
51
    );
50
    );
52
51
53
    # Set nonce
54
    Koha::ContentSecurityPolicy->new->nonce($test_nonce);
55
56
    t::lib::Mocks::mock_config( 'opachtdocs', C4::Context->config('intranetdir') . '/t/mock_templates/opac-tmpl' );
52
    t::lib::Mocks::mock_config( 'opachtdocs', C4::Context->config('intranetdir') . '/t/mock_templates/opac-tmpl' );
57
53
58
    my $env = {};
54
    my $env = {};
Lines 79-84 subtest 'test Content-Security-Policy header' => sub { Link Here
79
75
80
    my $test                      = Plack::Test->create($app);
76
    my $test                      = Plack::Test->create($app);
81
    my $res                       = $test->request( GET "/" );
77
    my $res                       = $test->request( GET "/" );
78
    my $test_nonce                = Koha::ContentSecurityPolicy->new->get_nonce();
82
    my $expected_csp_header_value = $csp_header_value;
79
    my $expected_csp_header_value = $csp_header_value;
83
    $expected_csp_header_value =~ s/_CSP_NONCE_/$test_nonce/g;
80
    $expected_csp_header_value =~ s/_CSP_NONCE_/$test_nonce/g;
84
    is(
81
    is(
(-)a/t/db_dependent/Koha/Middleware/ContentSecurityPolicy.t (-9 / +6 lines)
Lines 37-43 my $conf_csp_section = 'content_security_policy'; Link Here
37
subtest 'test CSP in OPAC' => sub {
37
subtest 'test CSP in OPAC' => sub {
38
    plan tests => 5;
38
    plan tests => 5;
39
39
40
    my $test_nonce = 'TEST_NONCE';
41
    my $csp_header_value =
40
    my $csp_header_value =
42
        "default-src 'self'; script-src 'self' 'nonce-_CSP_NONCE_'; style-src 'self' 'nonce-_CSP_NONCE_'; img-src 'self' data:; font-src 'self'; object-src 'none'";
41
        "default-src 'self'; script-src 'self' 'nonce-_CSP_NONCE_'; style-src 'self' 'nonce-_CSP_NONCE_'; img-src 'self' data:; font-src 'self'; object-src 'none'";
43
42
Lines 47-55 subtest 'test CSP in OPAC' => sub { Link Here
47
        { opac => { csp_mode => 'enabled', csp_header_value => $csp_header_value } }
46
        { opac => { csp_mode => 'enabled', csp_header_value => $csp_header_value } }
48
    );
47
    );
49
48
50
    # Set nonce
51
    Koha::ContentSecurityPolicy->new->nonce($test_nonce);
52
53
    my $env  = {};
49
    my $env  = {};
54
    my $home = $ENV{KOHA_HOME};
50
    my $home = $ENV{KOHA_HOME};
55
    my $app  = Plack::App::CGIBin->new( root => $ENV{GIT_INSTALL} ? "$home/opac" : "$home/opac/cgi-bin/opac" )->to_app;
51
    my $app  = Plack::App::CGIBin->new( root => $ENV{GIT_INSTALL} ? "$home/opac" : "$home/opac/cgi-bin/opac" )->to_app;
Lines 63-68 subtest 'test CSP in OPAC' => sub { Link Here
63
59
64
    my $test                      = Plack::Test->create($app);
60
    my $test                      = Plack::Test->create($app);
65
    my $res                       = $test->request( GET "/opac/opac-main.pl" );
61
    my $res                       = $test->request( GET "/opac/opac-main.pl" );
62
    my $test_nonce                = Koha::ContentSecurityPolicy->new->get_nonce();
66
    my $expected_csp_header_value = $csp_header_value;
63
    my $expected_csp_header_value = $csp_header_value;
67
    $expected_csp_header_value =~ s/_CSP_NONCE_/$test_nonce/g;
64
    $expected_csp_header_value =~ s/_CSP_NONCE_/$test_nonce/g;
68
    is(
65
    is(
Lines 100-106 subtest 'test CSP in OPAC' => sub { Link Here
100
subtest 'test CSP in staff client' => sub {
97
subtest 'test CSP in staff client' => sub {
101
    plan tests => 5;
98
    plan tests => 5;
102
99
103
    my $test_nonce = 'TEST_NONCE';
104
    my $csp_header_value =
100
    my $csp_header_value =
105
        "default-src 'self'; script-src 'self' 'nonce-_CSP_NONCE_'; style-src 'self' 'nonce-_CSP_NONCE_'; img-src 'self' data:; font-src 'self'; object-src 'none'";
101
        "default-src 'self'; script-src 'self' 'nonce-_CSP_NONCE_'; style-src 'self' 'nonce-_CSP_NONCE_'; img-src 'self' data:; font-src 'self'; object-src 'none'";
106
102
Lines 109-117 subtest 'test CSP in staff client' => sub { Link Here
109
        { opac => { csp_mode => 'enabled', csp_header_value => $csp_header_value } }
105
        { opac => { csp_mode => 'enabled', csp_header_value => $csp_header_value } }
110
    );
106
    );
111
107
112
    # Set nonce
113
    Koha::ContentSecurityPolicy->new->nonce($test_nonce);
114
115
    my $env  = {};
108
    my $env  = {};
116
    my $home = $ENV{KOHA_HOME};
109
    my $home = $ENV{KOHA_HOME};
117
    my $app  = Plack::App::CGIBin->new( root => $ENV{GIT_INSTALL} ? $home : "$home/intranet/cgi-bin/" )->to_app;
110
    my $app  = Plack::App::CGIBin->new( root => $ENV{GIT_INSTALL} ? $home : "$home/intranet/cgi-bin/" )->to_app;
Lines 125-130 subtest 'test CSP in staff client' => sub { Link Here
125
118
126
    my $test                      = Plack::Test->create($app);
119
    my $test                      = Plack::Test->create($app);
127
    my $res                       = $test->request( GET "/intranet/mainpage.pl" );
120
    my $res                       = $test->request( GET "/intranet/mainpage.pl" );
121
    my $test_nonce                = Koha::ContentSecurityPolicy->new->get_nonce();
128
    my $expected_csp_header_value = $csp_header_value;
122
    my $expected_csp_header_value = $csp_header_value;
129
    $expected_csp_header_value =~ s/_CSP_NONCE_/$test_nonce/g;
123
    $expected_csp_header_value =~ s/_CSP_NONCE_/$test_nonce/g;
130
    is(
124
    is(
Lines 154-160 subtest 'test CSP in staff client' => sub { Link Here
154
        { intranet => { csp_mode => 'enabled', csp_header_value => $csp_header_value } }
148
        { intranet => { csp_mode => 'enabled', csp_header_value => $csp_header_value } }
155
    );
149
    );
156
150
157
    $res = $test->request( GET "/intranet/mainpage.pl" );
151
    $res                       = $test->request( GET "/intranet/mainpage.pl" );
152
    $test_nonce                = Koha::ContentSecurityPolicy->new->get_nonce();
153
    $expected_csp_header_value = $csp_header_value;
154
    $expected_csp_header_value =~ s/_CSP_NONCE_/$test_nonce/g;
158
    is(
155
    is(
159
        $res->header('content-security-policy'), $expected_csp_header_value,
156
        $res->header('content-security-policy'), $expected_csp_header_value,
160
        "Response contains Content-Security-Policy header when it is enabled in koha-conf.xml"
157
        "Response contains Content-Security-Policy header when it is enabled in koha-conf.xml"
(-)a/t/db_dependent/Koha/Template/Plugin/Koha.t (-2 / +1 lines)
Lines 91-97 subtest 'CSPNonce' => sub { Link Here
91
    my $plugin = Koha::Template::Plugin::Koha->new($context);
91
    my $plugin = Koha::Template::Plugin::Koha->new($context);
92
92
93
    my $csp   = Koha::ContentSecurityPolicy->new;
93
    my $csp   = Koha::ContentSecurityPolicy->new;
94
    my $nonce = $csp->nonce;
94
    my $nonce = $csp->get_nonce;
95
95
96
    my $token = $plugin->CSPNonce;
96
    my $token = $plugin->CSPNonce;
97
97
98
- 

Return to bug 38365