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

(-)a/Koha/Middleware/ContentSecurityPolicy.pm (+56 lines)
Line 0 Link Here
1
package Koha::Middleware::ContentSecurityPolicy;
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <https://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
20
use parent qw(Plack::Middleware);
21
use Plack::Request;
22
use Plack::Response;
23
24
use Koha::ContentSecurityPolicy;
25
26
=head1 METHODS
27
28
=head2 call
29
30
This method is called for each request, and adds the Content-Security-Policy or
31
the Content-Security-Policy-Report-Only header based on your CSP configuration in
32
koha-conf.xml.
33
34
=cut
35
36
sub call {
37
    my ( $self, $env ) = @_;
38
39
    my $req = Plack::Request->new($env);
40
41
    my $csp = Koha::ContentSecurityPolicy->new;
42
43
    my $res = $self->app->($env);
44
    return $res unless $csp->is_enabled;
45
46
    return Plack::Util::response_cb(
47
        $res,
48
        sub {
49
            my $res     = shift;
50
            my $headers = $res->[1];
51
            push @$headers, ( $csp->header_name => $csp->header_value );
52
        }
53
    );
54
}
55
56
1;
(-)a/t/Koha/Middleware/ContentSecurityPolicy.t (+96 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
#
4
# Copyright 2025 Hypernova Oy
5
#
6
# This file is part of Koha.
7
#
8
# Koha is free software; you can redistribute it and/or modify it
9
# under the terms of the GNU General Public License as published by
10
# the Free Software Foundation; either version 3 of the License, or
11
# (at your option) any later version.
12
#
13
# Koha is distributed in the hope that it will be useful, but
14
# WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
# GNU General Public License for more details.
17
#
18
# You should have received a copy of the GNU General Public License
19
# along with Koha; if not, see <https://www.gnu.org/licenses>.
20
21
use strict;
22
use warnings;
23
use Test::NoWarnings;
24
use Test::More tests => 3;
25
use Test::Warn;
26
27
use HTTP::Request::Common;
28
use Plack::Builder;
29
use Plack::Util;
30
use Plack::Test;
31
32
use t::lib::Mocks;
33
34
use_ok("Koha::Middleware::ContentSecurityPolicy");
35
36
my $conf_csp_section = 'content_security_policy';
37
38
subtest 'test Content-Security-Policy header' => sub {
39
    plan tests => 3;
40
41
    my $test_nonce = 'TEST_NONCE';
42
    my $csp_header_value =
43
        "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'";
44
45
    t::lib::Mocks::mock_config(
46
        $conf_csp_section,
47
        { opac => { csp_mode => 'enabled', csp_header_value => $csp_header_value } }
48
    );
49
50
    # Set nonce
51
    Koha::ContentSecurityPolicy->new->nonce($test_nonce);
52
53
    my $env = {};
54
    my $app = sub {
55
        my $resp = [
56
            200,
57
            [
58
                'Content-Type',
59
                'text/plain',
60
                'Content-Length',
61
                12
62
            ],
63
            [ Koha::ContentSecurityPolicy->new->nonce ]
64
        ];
65
        return $resp;
66
    };
67
68
    $app = builder {
69
        enable "+Koha::Middleware::ContentSecurityPolicy";
70
        $app;
71
    };
72
73
    my $test                      = Plack::Test->create($app);
74
    my $res                       = $test->request( GET "/" );
75
    my $expected_csp_header_value = $csp_header_value;
76
    $expected_csp_header_value =~ s/_CSP_NONCE_/$test_nonce/g;
77
    is(
78
        $res->header('content-security-policy'), $expected_csp_header_value,
79
        "Response contains Content-Security-Policy header with the expected value"
80
    );
81
    is(
82
        $res->content, $test_nonce,
83
        "Response contains generated nonce in the body"
84
    );
85
86
    t::lib::Mocks::mock_config(
87
        $conf_csp_section,
88
        { opac => { csp_mode => '', csp_header_value => $csp_header_value } }
89
    );
90
91
    $res = $test->request( GET "/" );
92
    is(
93
        $res->header('content-security-policy'), undef,
94
        "Response does not contain Content-Security-Policy header when it is disabled in koha-conf.xml"
95
    );
96
};
(-)a/t/db_dependent/Koha/Middleware/ContentSecurityPolicy.t (-1 / +165 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
#
4
# Copyright 2025 Hypernova Oy
5
#
6
# This file is part of Koha.
7
#
8
# Koha is free software; you can redistribute it and/or modify it
9
# under the terms of the GNU General Public License as published by
10
# the Free Software Foundation; either version 3 of the License, or
11
# (at your option) any later version.
12
#
13
# Koha is distributed in the hope that it will be useful, but
14
# WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
# GNU General Public License for more details.
17
#
18
# You should have received a copy of the GNU General Public License
19
# along with Koha; if not, see <https://www.gnu.org/licenses>.
20
21
use strict;
22
use warnings;
23
use Test::NoWarnings;
24
use Test::More tests => 4;
25
26
use HTTP::Request::Common;
27
use Plack::App::CGIBin;
28
use Plack::Builder;
29
use Plack::Test;
30
31
use t::lib::Mocks;
32
33
use_ok("Koha::Middleware::ContentSecurityPolicy");
34
35
my $conf_csp_section = 'content_security_policy';
36
37
subtest 'test CSP in OPAC' => sub {
38
    plan tests => 5;
39
40
    my $test_nonce = 'TEST_NONCE';
41
    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'";
43
44
    t::lib::Mocks::mock_preference( 'OpacPublic', 1 );
45
    t::lib::Mocks::mock_config(
46
        $conf_csp_section,
47
        { opac => { csp_mode => 'enabled', csp_header_value => $csp_header_value } }
48
    );
49
50
    # Set nonce
51
    Koha::ContentSecurityPolicy->new->nonce($test_nonce);
52
53
    my $env  = {};
54
    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;
56
57
    $app = builder {
58
        mount '/opac' => builder {
59
            enable "+Koha::Middleware::ContentSecurityPolicy";
60
            $app;
61
        };
62
    };
63
64
    my $test                      = Plack::Test->create($app);
65
    my $res                       = $test->request( GET "/opac/opac-main.pl" );
66
    my $expected_csp_header_value = $csp_header_value;
67
    $expected_csp_header_value =~ s/_CSP_NONCE_/$test_nonce/g;
68
    is(
69
        $res->header('content-security-policy'), $expected_csp_header_value,
70
        "Response contains Content-Security-Policy header with the expected value"
71
    );
72
73
    t::lib::Mocks::mock_config(
74
        $conf_csp_section,
75
        { opac => { csp_mode => '', csp_header_value => $csp_header_value } }
76
    );
77
78
    $res = $test->request( GET "/opac/opac-main.pl" );
79
    is(
80
        $res->header('content-security-policy'), undef,
81
        "Response does not contain Content-Security-Policy header when it is disabled in koha-conf.xml"
82
    );
83
84
    like( $res->content, qr/<body ID="opac-main"/, 'opac-main.pl looks okay' );
85
86
    t::lib::Mocks::mock_config(
87
        $conf_csp_section,
88
        {}
89
    );
90
91
    $res = $test->request( GET "/opac/opac-main.pl" );
92
    is(
93
        $res->header('content-security-policy'), undef,
94
        "Response does not contain Content-Security-Policy header when it has not been defined in koha-conf.xml"
95
    );
96
97
    like( $res->content, qr/<body ID="opac-main"/, 'opac-main.pl looks okay' );
98
};
99
100
subtest 'test CSP in staff client' => sub {
101
    plan tests => 5;
102
103
    my $test_nonce = 'TEST_NONCE';
104
    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'";
106
107
    t::lib::Mocks::mock_config(
108
        $conf_csp_section,
109
        { opac => { csp_mode => 'enabled', csp_header_value => $csp_header_value } }
110
    );
111
112
    # Set nonce
113
    Koha::ContentSecurityPolicy->new->nonce($test_nonce);
114
115
    my $env  = {};
116
    my $home = $ENV{KOHA_HOME};
117
    my $app  = Plack::App::CGIBin->new( root => $ENV{GIT_INSTALL} ? $home : "$home/intranet/cgi-bin/" )->to_app;
118
119
    $app = builder {
120
        mount '/intranet' => builder {
121
            enable "+Koha::Middleware::ContentSecurityPolicy";
122
            $app;
123
        };
124
    };
125
126
    my $test                      = Plack::Test->create($app);
127
    my $res                       = $test->request( GET "/intranet/mainpage.pl" );
128
    my $expected_csp_header_value = $csp_header_value;
129
    $expected_csp_header_value =~ s/_CSP_NONCE_/$test_nonce/g;
130
    is(
131
        $res->header('content-security-policy'), undef,
132
        "Response does not Content-Security-Policy header when it has not been defined in koha-conf.xml"
133
    );
134
135
    t::lib::Mocks::mock_config(
136
        $conf_csp_section,
137
        {
138
            intranet => {
139
                csp_mode => '', csp_header_value => $csp_header_value,
140
            }
141
        }
142
    );
143
144
    $res = $test->request( GET "/intranet/mainpage.pl" );
145
    is(
146
        $res->header('content-security-policy'), undef,
147
        "Response does not contain Content-Security-Policy header when it is explicitly disabled in koha-conf.xml"
148
    );
149
150
    like( $res->content, qr/<body id="main_auth"/, 'mainpage.pl looks okay' );
151
152
    t::lib::Mocks::mock_config(
153
        $conf_csp_section,
154
        { intranet => { csp_mode => 'enabled', csp_header_value => $csp_header_value } }
155
    );
156
157
    $res = $test->request( GET "/intranet/mainpage.pl" );
158
    is(
159
        $res->header('content-security-policy'), $expected_csp_header_value,
160
        "Response contains Content-Security-Policy header when it is enabled in koha-conf.xml"
161
    );
162
163
    like( $res->content, qr/<body id="main_auth"/, 'mainpage.pl looks okay' );
164
};
165

Return to bug 38365