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

(-)a/Koha/REST/V1/CSPReports.pm (-1 / +1 lines)
Lines 49-55 sub add { Link Here
49
    my $report = $c->req->json;
49
    my $report = $c->req->json;
50
50
51
    # CSP reports come wrapped in a 'csp-report' key
51
    # CSP reports come wrapped in a 'csp-report' key
52
    my $csp_report = $report->{'csp-report'} // $report;
52
    my $csp_report = $report->{'csp-report'} // $report->{'body'} // $report;
53
53
54
    my $logger = Koha::Logger->get( { interface => 'csp' } );
54
    my $logger = Koha::Logger->get( { interface => 'csp' } );
55
55
(-)a/api/v1/swagger/definitions/csp_report.yaml (-58 / +54 lines)
Lines 4-64 description: | Link Here
4
  A Content-Security-Policy violation report as sent by browsers.
4
  A Content-Security-Policy violation report as sent by browsers.
5
  See https://www.w3.org/TR/CSP3/#violation-reports for specification.
5
  See https://www.w3.org/TR/CSP3/#violation-reports for specification.
6
properties:
6
properties:
7
  csp-report:
7
  document-uri:
8
    type: object
8
    type: string
9
    description: The CSP violation report object
9
    description: The URI of the document where the violation occurred
10
    properties:
10
    maxLength: 200
11
      document-uri:
11
  referrer:
12
        type: string
12
    type: string
13
        description: The URI of the document where the violation occurred
13
    description: The referrer of the document where the violation occurred
14
        maxLength: 200
14
    maxLength: 200
15
      referrer:
15
  violated-directive:
16
        type: string
16
    type: string
17
        description: The referrer of the document where the violation occurred
17
    description: The directive that was violated (e.g., "script-src 'self'")
18
        maxLength: 200
18
    maxLength: 100
19
      violated-directive:
19
  effective-directive:
20
        type: string
20
    type: string
21
        description: The directive that was violated (e.g., "script-src 'self'")
21
    description: The effective directive that was violated
22
        maxLength: 100
22
    maxLength: 100
23
      effective-directive:
23
  original-policy:
24
        type: string
24
    type: string
25
        description: The effective directive that was violated
25
    description: The original CSP policy as specified in the header
26
        maxLength: 100
26
    maxLength: 400
27
      original-policy:
27
  disposition:
28
        type: string
28
    type: string
29
        description: The original CSP policy as specified in the header
29
    description: Either "enforce" or "report" depending on CSP mode
30
        maxLength: 400
30
    enum:
31
      disposition:
31
      - enforce
32
        type: string
32
      - report
33
        description: Either "enforce" or "report" depending on CSP mode
33
  blocked-uri:
34
        enum:
34
    type: string
35
          - enforce
35
    description: The URI of the resource that was blocked
36
          - report
36
    maxLength: 200
37
      blocked-uri:
37
  line-number:
38
        type: string
38
    type: integer
39
        description: The URI of the resource that was blocked
39
    description: Line number in the document where the violation occurred
40
        maxLength: 200
40
    minimum: 0
41
      line-number:
41
    maximum: 999999
42
        type: integer
42
  column-number:
43
        description: Line number in the document where the violation occurred
43
    type: integer
44
        minimum: 0
44
    description: Column number where the violation occurred
45
        maximum: 999999
45
    minimum: 0
46
      column-number:
46
    maximum: 999999
47
        type: integer
47
  source-file:
48
        description: Column number where the violation occurred
48
    type: string
49
        minimum: 0
49
    description: The URI of the script where the violation occurred
50
        maximum: 999999
50
    maxLength: 200
51
      source-file:
51
  status-code:
52
        type: string
52
    type: integer
53
        description: The URI of the script where the violation occurred
53
    description: HTTP status code of the document
54
        maxLength: 200
54
    minimum: 0
55
      status-code:
55
    maximum: 999
56
        type: integer
56
  script-sample:
57
        description: HTTP status code of the document
57
    type: string
58
        minimum: 0
58
    description: First 40 characters of the inline script that caused the violation
59
        maximum: 999
59
    maxLength: 41
60
      script-sample:
60
additionalProperties: true
61
        type: string
62
        description: First 40 characters of the inline script that caused the violation
63
        maxLength: 41
64
additionalProperties: true
(-)a/api/v1/swagger/definitions/csp_report_uri_to.yaml (+27 lines)
Line 0 Link Here
1
---
2
type: object
3
description: |
4
  A Content-Security-Policy violation report as sent by browsers.
5
  See https://www.w3.org/TR/CSP3/#violation-reports for specification.
6
properties:
7
  csp-report:
8
    $ref: "csp_report.yaml"
9
  body:
10
    $ref: "csp_report.yaml"
11
  user_agent:
12
    type: string
13
    maxLength: 300
14
  destination:
15
    type: string
16
    maxLength: 200
17
  url:
18
    type: string
19
    maxLength: 200
20
  timestamp:
21
    type: string
22
    maxLength: 50
23
  attempts:
24
    type: integer
25
    minimum: 0
26
    maximum: 99
27
additionalProperties: true
(-)a/api/v1/swagger/paths/public_csp_reports.yaml (-1 / +2 lines)
Lines 17-22 Link Here
17
    consumes:
17
    consumes:
18
      - application/csp-report
18
      - application/csp-report
19
      - application/json
19
      - application/json
20
      - application/reports+json
20
    produces:
21
    produces:
21
      - application/json
22
      - application/json
22
    parameters:
23
    parameters:
Lines 25-31 Link Here
25
        description: CSP violation report
26
        description: CSP violation report
26
        required: true
27
        required: true
27
        schema:
28
        schema:
28
          $ref: "../swagger.yaml#/definitions/csp_report"
29
          $ref: "../swagger.yaml#/definitions/csp_report_uri_to"
29
    responses:
30
    responses:
30
      "204":
31
      "204":
31
        description: Report received successfully
32
        description: Report received successfully
(-)a/api/v1/swagger/swagger.yaml (+2 lines)
Lines 44-49 definitions: Link Here
44
    $ref: ./definitions/city.yaml
44
    $ref: ./definitions/city.yaml
45
  csp_report:
45
  csp_report:
46
    $ref: ./definitions/csp_report.yaml
46
    $ref: ./definitions/csp_report.yaml
47
  csp_report_uri_to:
48
    $ref: ./definitions/csp_report_uri_to.yaml
47
  credit:
49
  credit:
48
    $ref: ./definitions/credit.yaml
50
    $ref: ./definitions/credit.yaml
49
  debit:
51
  debit:
(-)a/t/db_dependent/api/v1/public/csp_reports.t (-146 / +176 lines)
Lines 36-181 t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 ); Link Here
36
36
37
subtest 'add() tests' => sub {
37
subtest 'add() tests' => sub {
38
38
39
    plan tests => 15;
39
    plan tests => 2;
40
40
41
    $schema->storage->txn_begin;
41
    $schema->storage->txn_begin;
42
42
43
    # Test with standard CSP report format
43
    foreach my $csp_report_type ( 'report-uri', 'report-to' ) {
44
    my $csp_report = {
44
        subtest "Test $csp_report_type" => sub {
45
        'csp-report' => {
45
            plan tests => 15;
46
            'document-uri'        => 'https://library.example.org/cgi-bin/koha/opac-main.pl',
46
            my $content_type = $csp_report_type eq 'report-to' ? 'application/csp-report' : 'application/reports+json';
47
            'referrer'            => '',
47
48
            'violated-directive'  => "script-src 'self' 'nonce-abc123'",
48
            # Test with standard CSP report-uri format
49
            'effective-directive' => 'script-src',
49
            my $csp_report = {
50
            'original-policy'     => "default-src 'self'; script-src 'self' 'nonce-abc123'",
50
                'csp-report' => {
51
            'disposition'         => 'enforce',
51
                    'document-uri'        => 'https://library.example.org/cgi-bin/koha/opac-main.pl',
52
            'blocked-uri'         => 'inline',
52
                    'referrer'            => '',
53
            'line-number'         => 42,
53
                    'violated-directive'  => "script-src 'self' 'nonce-abc123'",
54
            'column-number'       => 10,
54
                    'effective-directive' => 'script-src',
55
            'source-file'         => 'https://library.example.org/cgi-bin/koah/opac-main.pl',
55
                    'original-policy'     => "default-src 'self'; script-src 'self' 'nonce-abc123'",
56
            'status-code'         => 200,
56
                    'disposition'         => 'enforce',
57
        }
57
                    'blocked-uri'         => 'inline',
58
    };
58
                    'line-number'         => 42,
59
59
                    'column-number'       => 10,
60
    $csp_report->{'csp-report'}->{'line-number'} = 99999999999999999;
60
                    'source-file'         => 'https://library.example.org/cgi-bin/koah/opac-main.pl',
61
61
                    'status-code'         => 200,
62
    # Too large integers should be rejected
62
                }
63
    $t->post_ok( '/api/v1/public/csp-reports' => { 'Content-Type' => 'application/csp-report' } => json => $csp_report )
63
            };
64
        ->status_is( 400, 'CSP report rejected (400) because of line-number exceeding maximum value' );
64
            if ( $csp_report_type eq 'report-to' ) {
65
65
                $csp_report->{'body'}       = delete $csp_report->{'csp-report'};
66
    $csp_report->{'csp-report'}->{'line-number'} = -1;
66
                $csp_report->{'age'}        = 1;
67
67
                $csp_report->{'type'}       = 'csp-violation';
68
    # Too small integers should be rejected
68
                $csp_report->{'url'}        = 'https://library.example.org/cgi-bin/koha/opac-main.pl';
69
    $t->post_ok( '/api/v1/public/csp-reports' => { 'Content-Type' => 'application/csp-report' } => json => $csp_report )
69
                $csp_report->{'user_agent'} = 'Test::Mojo';
70
        ->status_is( 400, 'CSP report rejected (400) because of line-number not reaching minimum value' );
70
            }
71
    $csp_report->{'csp-report'}->{'line-number'} = 42;
71
72
72
            my $csp_report_body_key = $csp_report_type eq 'report-uri' ? 'csp-report' : 'body';
73
    $csp_report->{'csp-report'}->{'disposition'} = 'this is not okay';
73
74
74
            $csp_report->{$csp_report_body_key}->{'line-number'} = 99999999999999999;
75
    # Enum values should be confirmed
75
76
    $t->post_ok( '/api/v1/public/csp-reports' => { 'Content-Type' => 'application/csp-report' } => json => $csp_report )
76
            # Too large integers should be rejected
77
        ->status_is( 400, 'CSP report rejected (400) because of disposition is not either "enforce" nor "report"' );
77
            $t->post_ok( '/api/v1/public/csp-reports' => { 'Content-Type' => $content_type } => json => $csp_report )
78
    $csp_report->{'csp-report'}->{'disposition'} = 'enforce';
78
                ->status_is( 400, 'CSP report rejected (400) because of line-number exceeding maximum value' );
79
79
80
    $csp_report->{'csp-report'}->{'script-sample'} =
80
            $csp_report->{$csp_report_body_key}->{'line-number'} = -1;
81
        'this is way too long script sample. a maximum of only 40 characters is allowed';
81
82
82
            # Too small integers should be rejected
83
    # Too long strings should be rejected
83
            $t->post_ok( '/api/v1/public/csp-reports' => { 'Content-Type' => $content_type } => json => $csp_report )
84
    $t->post_ok( '/api/v1/public/csp-reports' => { 'Content-Type' => 'application/csp-report' } => json => $csp_report )
84
                ->status_is( 400, 'CSP report rejected (400) because of line-number not reaching minimum value' );
85
        ->status_is( 400, 'CSP report rejected (400) because of script-sample exceeding maximum length' );
85
            $csp_report->{$csp_report_body_key}->{'line-number'} = 42;
86
    $csp_report->{'csp-report'}->{'script-sample'} = 'console.log("hi");';
86
87
87
            $csp_report->{$csp_report_body_key}->{'disposition'} = 'this is not okay';
88
    # Anonymous request should work (browsers send these without auth)
88
89
    $t->post_ok( '/api/v1/public/csp-reports' => { 'Content-Type' => 'application/csp-report' } => json => $csp_report )
89
            # Enum values should be confirmed
90
        ->status_is( 204, 'CSP report accepted' );
90
            $t->post_ok( '/api/v1/public/csp-reports' => { 'Content-Type' => $content_type } => json => $csp_report )
91
91
                ->status_is(
92
    # Test with application/json content type (also valid)
92
                400,
93
    $t->post_ok( '/api/v1/public/csp-reports' => { 'Content-Type' => 'application/json' } => json => $csp_report )
93
                'CSP report rejected (400) because of disposition is not either "enforce" nor "report"'
94
        ->status_is( 204, 'CSP report accepted with application/json content type' );
94
                );
95
95
            $csp_report->{$csp_report_body_key}->{'disposition'} = 'enforce';
96
    # Test with minimal report
96
97
    my $minimal_report = {
97
            $csp_report->{$csp_report_body_key}->{'script-sample'} =
98
        'csp-report' => {
98
                'this is way too long script sample. a maximum of only 40 characters is allowed';
99
            'document-uri'       => 'https://library.example.org/',
99
100
            'violated-directive' => 'script-src',
100
            # Too long strings should be rejected
101
            'blocked-uri'        => 'https://evil.example.com/script.js',
101
            $t->post_ok( '/api/v1/public/csp-reports' => { 'Content-Type' => $content_type } => json => $csp_report )
102
        }
102
                ->status_is( 400, 'CSP report rejected (400) because of script-sample exceeding maximum length' );
103
    };
103
            $csp_report->{$csp_report_body_key}->{'script-sample'} = 'console.log("hi");';
104
104
105
    $t->post_ok( '/api/v1/public/csp-reports' => json => $minimal_report )
105
            # Anonymous request should work (browsers send these without auth)
106
        ->status_is( 204, 'Minimal CSP report accepted' );
106
            $t->post_ok( '/api/v1/public/csp-reports' => { 'Content-Type' => $content_type } => json => $csp_report )
107
107
                ->status_is( 204, 'CSP report accepted' );
108
    subtest 'make sure log entries are being written' => sub {
108
109
        plan tests => 17;
109
            # Test with application/json content type (also valid)
110
110
            $t->post_ok( '/api/v1/public/csp-reports' => { 'Content-Type' => $content_type } => json => $csp_report )
111
        my $log4perl_conf_file = C4::Context->config('intranetdir') . '/etc/log4perl.conf';
111
                ->status_is( 204, 'CSP report accepted with application/json content type' );
112
        open my $fh, '<:encoding(UTF-8)', $log4perl_conf_file or do {
112
113
            carp "Cannot read $log4perl_conf_file: $!";
113
            # Test with minimal report
114
            return;
114
            my $minimal_report = {
115
        };
115
                'csp-report' => {
116
        my $log4perl_conf_content = "";
116
                    'document-uri'       => 'https://library.example.org/',
117
        while (<$fh>) {
117
                    'violated-directive' => 'script-src',
118
            my $line = $_;
118
                    'blocked-uri'        => 'https://evil.example.com/script.js',
119
            next unless $line =~ /^(log4perl.logger.(plack-)?csp)|(log4perl.appender.(PLACK)?CSP)/;
119
                }
120
            $log4perl_conf_content .= "$line";
120
            };
121
        }
121
            if ( $csp_report_type eq 'report-to' ) {
122
        close $fh;
122
                $minimal_report->{'body'}       = delete $minimal_report->{'csp-report'};
123
        like( $log4perl_conf_content, qr/__LOG_DIR__\/csp-violations\.log/, 'csp-violations.log is defined' );
123
                $minimal_report->{'age'}        = 1;
124
        like(
124
                $minimal_report->{'type'}       = 'csp-violation';
125
            $log4perl_conf_content, qr/__LOG_DIR__\/plack-csp-violations\.log/,
125
                $minimal_report->{'url'}        = 'https://library.example.org/cgi-bin/koha/opac-main.pl';
126
            'plack-csp-violations.log is defined'
126
                $minimal_report->{'user_agent'} = 'Test::Mojo';
127
        );
127
            }
128
        like(
128
129
            $log4perl_conf_content, qr/log4perl.appender.CSP=Log::Log4perl::Appender::File/,
129
            $t->post_ok(
130
            'CSP: File appender exists'
130
                '/api/v1/public/csp-reports' => { 'Content-Type' => $content_type } => json => $minimal_report )
131
        );
131
                ->status_is( 204, 'Minimal CSP report accepted' );
132
        like(
132
133
            $log4perl_conf_content, qr/log4perl.appender.PLACKCSP=Log::Log4perl::Appender::File/,
133
            subtest 'make sure log entries are being written' => sub {
134
            'PLACKCSP: File appender exists'
134
                plan tests => 17;
135
        );
135
136
        $log4perl_conf_content =~ s/Log::Log4perl::Appender::File/Log::Log4perl::Appender::TestBuffer/g;
136
                my $log4perl_conf_file = C4::Context->config('intranetdir') . '/etc/log4perl.conf';
137
        Log::Log4perl::init( \$log4perl_conf_content );
137
                open my $fh, '<:encoding(UTF-8)', $log4perl_conf_file or do {
138
        my $appenders     = Log::Log4perl->appenders;
138
                    carp "Cannot read $log4perl_conf_file: $!";
139
        my $appender      = Log::Log4perl->appenders->{CSP};
139
                    return;
140
        my $appenderplack = Log::Log4perl->appenders->{PLACKCSP};
140
                };
141
141
                my $log4perl_conf_content = "";
142
        is( $appender->buffer,      '', 'Nothing in log buffer yet' );
142
                while (<$fh>) {
143
        is( $appenderplack->buffer, '', 'Nothing in plack log buffer yet' );
143
                    my $line = $_;
144
144
                    next unless $line =~ /^(log4perl.logger.(plack-)?csp)|(log4perl.appender.(PLACK)?CSP)/;
145
        $t->post_ok(
145
                    $log4perl_conf_content .= "$line";
146
            '/api/v1/public/csp-reports' => { 'Content-Type' => 'application/csp-report' } => json => $csp_report )
146
                }
147
            ->status_is( 204, 'CSP report accepted' );
147
                close $fh;
148
148
                like( $log4perl_conf_content, qr/__LOG_DIR__\/csp-violations\.log/, 'csp-violations.log is defined' );
149
        my $expected_log_entry = sprintf(
149
                like(
150
            "CSP violation: '%s' blocked '%s' on page '%s'%s",
150
                    $log4perl_conf_content, qr/__LOG_DIR__\/plack-csp-violations\.log/,
151
            $csp_report->{'csp-report'}->{'violated-directive'},
151
                    'plack-csp-violations.log is defined'
152
            $csp_report->{'csp-report'}->{'blocked-uri'},
152
                );
153
            $csp_report->{'csp-report'}->{'document-uri'},
153
                like(
154
            ' at '
154
                    $log4perl_conf_content, qr/log4perl.appender.CSP=Log::Log4perl::Appender::File/,
155
                . $csp_report->{'csp-report'}->{'source-file'} . ':'
155
                    'CSP: File appender exists'
156
                . $csp_report->{'csp-report'}->{'line-number'} . ':'
156
                );
157
                . $csp_report->{'csp-report'}->{'column-number'}
157
                like(
158
        );
158
                    $log4perl_conf_content, qr/log4perl.appender.PLACKCSP=Log::Log4perl::Appender::File/,
159
159
                    'PLACKCSP: File appender exists'
160
        like(
160
                );
161
            $appender->buffer, qr/$expected_log_entry/
161
                $log4perl_conf_content =~ s/Log::Log4perl::Appender::File/Log::Log4perl::Appender::TestBuffer/g;
162
            ,                  'Log contains expected log entry'
162
                Log::Log4perl::init( \$log4perl_conf_content );
163
        );
163
                my $appenders     = Log::Log4perl->appenders;
164
        is( $appenderplack->buffer, '', 'Nothing in plack log buffer yet' );
164
                my $appender      = Log::Log4perl->appenders->{CSP};
165
        $appender->clear();
165
                my $appenderplack = Log::Log4perl->appenders->{PLACKCSP};
166
166
167
        $ENV{'plack.is.enabled.for.this.test'} = 1;    # tricking C4::Context->psgi_env
167
                is( $appender->buffer,      '', 'Nothing in log buffer yet' );
168
        $t->post_ok(
168
                is( $appenderplack->buffer, '', 'Nothing in plack log buffer yet' );
169
            '/api/v1/public/csp-reports' => { 'Content-Type' => 'application/csp-report' } => json => $csp_report )
169
170
            ->status_is( 204, 'CSP report accepted' );
170
                $t->post_ok(
171
        like(
171
                    '/api/v1/public/csp-reports' => { 'Content-Type' => $content_type } => json => $csp_report )
172
            $appenderplack->buffer, qr/$expected_log_entry/
172
                    ->status_is( 204, 'CSP report accepted' );
173
            ,                       'Plack log contains expected log entry'
173
174
        );
174
                my $expected_log_entry = sprintf(
175
        is( $appender->buffer, '', 'Nothing in log buffer yet' );
175
                    "CSP violation: '%s' blocked '%s' on page '%s'%s",
176
176
                    $csp_report->{$csp_report_body_key}->{'violated-directive'},
177
        Log::Log4perl::init(
177
                    $csp_report->{$csp_report_body_key}->{'blocked-uri'},
178
            \<<"HERE"
178
                    $csp_report->{$csp_report_body_key}->{'document-uri'},
179
                    ' at '
180
                        . $csp_report->{$csp_report_body_key}->{'source-file'} . ':'
181
                        . $csp_report->{$csp_report_body_key}->{'line-number'} . ':'
182
                        . $csp_report->{$csp_report_body_key}->{'column-number'}
183
                );
184
185
                like(
186
                    $appender->buffer, qr/$expected_log_entry/
187
                    ,                  'Log contains expected log entry'
188
                );
189
                is( $appenderplack->buffer, '', 'Nothing in plack log buffer yet' );
190
                $appender->clear();
191
192
                $ENV{'plack.is.enabled.for.this.test'} = 1;    # tricking C4::Context->psgi_env
193
                $t->post_ok(
194
                    '/api/v1/public/csp-reports' => { 'Content-Type' => $content_type } => json => $csp_report )
195
                    ->status_is( 204, 'CSP report accepted' );
196
                like(
197
                    $appenderplack->buffer, qr/$expected_log_entry/
198
                    ,                       'Plack log contains expected log entry'
199
                );
200
                is( $appender->buffer, '', 'Nothing in log buffer yet' );
201
202
                Log::Log4perl::init(
203
                    \<<"HERE"
179
log4perl.logger.api = WARN, API
204
log4perl.logger.api = WARN, API
180
log4perl.appender.API=Log::Log4perl::Appender::TestBuffer
205
log4perl.appender.API=Log::Log4perl::Appender::TestBuffer
181
log4perl.appender.API.mode=append
206
log4perl.appender.API.mode=append
Lines 183-196 log4perl.appender.API.layout=PatternLayout Link Here
183
log4perl.appender.API.layout.ConversionPattern=[%d] [%p] %m %l%n
208
log4perl.appender.API.layout.ConversionPattern=[%d] [%p] %m %l%n
184
log4perl.appender.API.utf8=1
209
log4perl.appender.API.utf8=1
185
HERE
210
HERE
186
        );
211
                );
187
        $appender = Log::Log4perl->appenders()->{API};
212
                $appender = Log::Log4perl->appenders()->{API};
188
        $t->post_ok(
213
                $t->post_ok(
189
            '/api/v1/public/csp-reports' => { 'Content-Type' => 'application/csp-report' } => json => $csp_report )
214
                    '/api/v1/public/csp-reports' => { 'Content-Type' => $content_type } => json => $csp_report )
190
            ->status_is( 204, 'CSP report returns 204 even when no CSP loggers are defined' );
215
                    ->status_is( 204, 'CSP report returns 204 even when no CSP loggers are defined' );
191
        is( $appender->buffer, '', 'Nothing in the only defined log buffer, because it is unrelated to CSP' );
216
                is( $appender->buffer, '', 'Nothing in the only defined log buffer, because it is unrelated to CSP' );
192
217
193
    };
218
                # clear buffers
219
                $appender->clear();
220
                $appenderplack->clear();
221
                delete $ENV{'plack.is.enabled.for.this.test'};
222
            };
223
        };
224
    }
194
225
195
    $schema->storage->txn_rollback;
226
    $schema->storage->txn_rollback;
196
};
227
};
197
- 

Return to bug 38365