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

(-)a/t/db_dependent/Output.t (-138 / +138 lines)
Lines 162-324 subtest 'redirect_if_opac_suppressed() tests' => sub { Link Here
162
};
162
};
163
163
164
subtest 'Content-Security-Policy tests' => sub {
164
subtest 'Content-Security-Policy tests' => sub {
165
    plan tests => 1;
165
    my @interfaces = ( 'opac', 'intranet' );
166
166
    my $modes      = {
167
    subtest 'OPAC' => sub {
167
        'enabled'     => 'Content-security-policy',
168
168
        'report-only' => 'Content-security-policy-report-only',
169
        plan tests => 3;
169
    };
170
    my @conf_modes = keys %$modes;
170
171
171
        subtest 'turned off' => sub {
172
    plan tests => scalar @interfaces;
173
    foreach my $interface (@interfaces) {
172
174
173
            plan tests => 2;
175
        subtest "interface: $interface" => sub {
174
176
175
            $schema->storage->txn_begin;
177
            #         # one subtest per mode + disabled
178
            plan tests => scalar @conf_modes + 1;
176
179
177
            t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' );
180
            subtest 'disabled' => sub {
178
181
179
            my $query = CGI->new();
182
                $schema->storage->txn_begin;
180
183
181
            {
184
                t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' );
182
                local *STDOUT;
183
                my $stdout;
184
                open STDOUT, '>', \$stdout;
185
185
186
                my ( $template, $borrowernumber, $cookie ) = C4::Auth::get_template_and_user(
186
                t::lib::Mocks::mock_config(
187
                    {
187
                    'content_security_policy',
188
                        template_name   => "opac-main.tt",
188
                    { $interface => { csp_mode => '', csp_header_value => '' } }
189
                        type            => "opac",
190
                        query           => $query,
191
                        authnotrequired => 1,
192
                    }
193
                );
189
                );
194
190
195
                C4::Output::output_html_with_http_headers( $query, $cookie, $template->output );
191
                my $query = CGI->new();
196
197
                unlike( $stdout, qr{Content-Security-Policy:}, 'No header present: Content-Security-Policy' );
198
                unlike(
199
                    $stdout, qr{Content-Security-Policy-Report-Only:},
200
                    'No header present: Content-Security-Policy-Report-Only'
201
                );
202
203
                close STDOUT;
204
            };
205
206
            $schema->storage->txn_rollback;
207
208
        };
209
210
        subtest 'report-only' => sub {
211
212
            plan tests => 3;
213
192
214
            $schema->storage->txn_begin;
193
                {
194
                    local *STDOUT;
195
                    my $stdout;
196
                    open STDOUT, '>', \$stdout;
215
197
216
            t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' );
198
                    my ( $template, $borrowernumber, $cookie ) = C4::Auth::get_template_and_user(
199
                        {
200
                            template_name   => "${interface}-main.tt",
201
                            type            => $interface,
202
                            query           => $query,
203
                            authnotrequired => 1,
204
                        }
205
                    );
217
206
218
            C4::Context->interface('opac');
207
                    C4::Output::output_html_with_http_headers( $query, $cookie, $template->output );
219
208
220
            my $expected_csp_header = 'Content-security-policy-report-only';
209
                    unlike(
221
            my $expected_csp_header_value =
210
                        $stdout, qr/Content-Security-Policy:/i,
222
                "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'";
211
                        "While feature is disabled, and requesting '$interface', no header present: 'Content-Security-Policy'"
223
            t::lib::Mocks::mock_config(
212
                    );
224
                'content_security_policy',
213
                    unlike(
225
                { 'opac' => { csp_mode => 'report-only', csp_header_value => $expected_csp_header_value } }
214
                        $stdout, qr/Content-Security-Policy-Report-Only:/i,
226
            );
215
                        "While feature is disabled, and requesting '$interface', no header present: 'Content-Security-Policy-Report-Only'"
216
                    );
227
217
228
            my $query = CGI->new();
218
                    close STDOUT;
219
                };
229
220
230
            {
221
                $schema->storage->txn_rollback;
231
                local *STDOUT;
232
                my $stdout;
233
                open STDOUT, '>', \$stdout;
234
235
                my ( $template, $borrowernumber, $cookie ) = C4::Auth::get_template_and_user(
236
                    {
237
                        template_name   => "opac-main.tt",
238
                        type            => "opac",
239
                        query           => $query,
240
                        authnotrequired => 1,
241
                    }
242
                );
243
244
                my $cache = Koha::Cache::Memory::Lite->get_instance;
245
                my $nonce = $cache->get_from_cache("csp_nonce");
246
                $expected_csp_header_value =~ s/_CSP_NONCE_/nonce-$nonce/g;
247
                C4::Output::output_html_with_http_headers( $query, $cookie, $template->output );
248
249
                $stdout =~ /(Content-Security-Policy[A-Za-z\-]*): (.*)\r/im;
250
                my ( $csp_header, $csp_header_value ) = ( $1, $2 );
251
                isnt(
252
                    $csp_header, 'Content-Security-Policy',
253
                    'No header present: Content-Security-Policy'
254
                );
255
                is( $csp_header, $expected_csp_header, 'Header present: Content-Security-Policy-Report-Only' );
256
                is(
257
                    $csp_header_value, $expected_csp_header_value,
258
                    'Content-Security-Policy-Report-Only value matches expected value'
259
                );
260
222
261
                close STDOUT;
262
            };
223
            };
263
224
264
            $schema->storage->txn_rollback;
225
            foreach my $mode (@conf_modes) {
265
226
                subtest "CSP mode: $mode" => sub {
266
        };
227
267
228
                    #             # while enabled, but req..     # no header present                # header present + value matches
268
        subtest 'enabled' => sub {
229
                    plan tests => ( scalar @interfaces - 1 ) * 2 + ( scalar @conf_modes - 1 ) * 1 + 2;
269
230
270
            plan tests => 3;
231
                    $schema->storage->txn_begin;
271
232
272
            $schema->storage->txn_begin;
233
                    t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' );
273
234
274
            t::lib::Mocks::mock_preference( 'SessionStorage', 'tmp' );
235
                    C4::Context->interface($interface);
275
236
276
            C4::Context->interface('opac');
237
                    my $expected_csp_header = $modes->{$mode};
277
238
                    my $expected_csp_header_value =
278
            my $expected_csp_header = 'Content-security-policy';
239
                        "default-src 'self'; script-src 'self' 'unsafe-eval' _CSP_NONCE_; style-src 'self' 'unsafe-inline' _CSP_NONCE_; img-src 'self' data:; font-src 'self'; object-src 'none'";
279
            my $expected_csp_header_value =
240
                    t::lib::Mocks::mock_config(
280
                "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'";
241
                        'content_security_policy',
281
            t::lib::Mocks::mock_config(
242
                        { $interface => { csp_mode => $mode, csp_header_value => $expected_csp_header_value } }
282
                'content_security_policy',
243
                    );
283
                { 'opac' => { csp_mode => 'enabled', csp_header_value => $expected_csp_header_value } }
244
284
            );
245
                    my $query = CGI->new();
285
246
286
            my $query = CGI->new();
247
                    foreach my $other_interface (@interfaces) {
287
248
                        next if $other_interface eq $interface;
288
            {
249
                        {
289
                local *STDOUT;
250
                            local *STDOUT;
290
                my $stdout;
251
                            my $stdout;
291
                open STDOUT, '>', \$stdout;
252
                            open STDOUT, '>', \$stdout;
292
253
293
                my ( $template, $borrowernumber, $cookie ) = C4::Auth::get_template_and_user(
254
                            my ( $template, $borrowernumber, $cookie ) = C4::Auth::get_template_and_user(
294
                    {
255
                                {
295
                        template_name   => "opac-main.tt",
256
                                    template_name   => "${other_interface}-main.tt",
296
                        type            => "opac",
257
                                    type            => $other_interface,
297
                        query           => $query,
258
                                    query           => $query,
298
                        authnotrequired => 1,
259
                                    authnotrequired => 1,
260
                                }
261
                            );
262
263
                            C4::Output::output_html_with_http_headers( $query, $cookie, $template->output );
264
265
                            unlike(
266
                                $stdout, qr/Content-Security-Policy:/i,
267
                                "While feature is enabled on '$interface', but requesting '$other_interface', where the feature is disabled, no header present: 'Content-Security-Policy'"
268
                            );
269
                            unlike(
270
                                $stdout, qr/Content-Security-Policy-Report-Only:/i,
271
                                "While feature is enabled on '$interface', but requesting '$other_interface', where the feature is disabled, no header present: 'Content-Security-Policy-Report-Only'"
272
                            );
273
274
                            close STDOUT;
275
                        };
299
                    }
276
                    }
300
                );
301
302
                my $cache = Koha::Cache::Memory::Lite->get_instance;
303
                my $nonce = $cache->get_from_cache("csp_nonce");
304
                $expected_csp_header_value =~ s/_CSP_NONCE_/nonce-$nonce/g;
305
306
                C4::Output::output_html_with_http_headers( $query, $cookie, $template->output );
307
308
                $stdout =~ /(Content-Security-Policy[A-Za-z\-]*): (.*)\r/im;
309
                my ( $csp_header, $csp_header_value ) = ( $1, $2 );
310
                isnt(
311
                    $csp_header, 'Content-Security-Policy-Report-Only:',
312
                    'No header present: Content-Security-Policy-Report-Only'
313
                );
314
                is( $csp_header, $expected_csp_header,       'Header present: Content-Security-Policy' );
315
                is( $csp_header_value,  $expected_csp_header_value, 'Content-Security-Policy value matches expected value' );
316
277
317
                close STDOUT;
278
                    {
318
            };
279
                        local *STDOUT;
319
280
                        my $stdout;
320
            $schema->storage->txn_rollback;
281
                        open STDOUT, '>', \$stdout;
282
283
                        my ( $template, $borrowernumber, $cookie ) = C4::Auth::get_template_and_user(
284
                            {
285
                                template_name   => "${interface}-main.tt",
286
                                type            => $interface,
287
                                query           => $query,
288
                                authnotrequired => 1,
289
                            }
290
                        );
291
292
                        my $cache = Koha::Cache::Memory::Lite->get_instance;
293
                        my $nonce = $cache->get_from_cache("csp_nonce");
294
                        $expected_csp_header_value =~ s/_CSP_NONCE_/nonce-$nonce/g;
295
                        C4::Output::output_html_with_http_headers( $query, $cookie, $template->output );
296
297
                        $stdout =~ /(Content-Security-Policy[A-Za-z\-]*): (.*)\r/im;
298
                        my ( $csp_header, $csp_header_value ) = ( $1, $2 );
299
                        foreach my $other_mode (@conf_modes) {
300
                            next if $other_mode eq $mode;
301
                            isnt(
302
                                $csp_header, $modes->{$other_mode},
303
                                "While feature is in '$mode' mode, header is not present: '$modes->{$other_mode}'"
304
                            );
305
                        }
306
307
                        is(
308
                            $csp_header, $expected_csp_header,
309
                            "While feature is in '$mode' mode, header is present: '$expected_csp_header'"
310
                        );
311
                        is(
312
                            $csp_header_value, $expected_csp_header_value,
313
                            "The value of header '$csp_header' matches the expected value"
314
                        );
315
316
                        close STDOUT;
317
                    };
318
319
                    $schema->storage->txn_rollback;
320
                };
321
321
322
            }
322
        };
323
        };
323
    };
324
    }
324
};
325
};
325
- 

Return to bug 38365