From 159d0eb8a5a63571995f2e8504f46be77d6535d0 Mon Sep 17 00:00:00 2001 From: David Cook Date: Tue, 10 Feb 2026 02:04:54 +0000 Subject: [PATCH] Bug 38365: Add support for both CSP report syntaxes Signed-off-by: Lari Taskula Signed-off-by: David Cook --- Koha/REST/V1/CSPReports.pm | 76 +++++++++++-------- .../swagger/definitions/csp_report_body.yaml | 56 ++++++++++++++ .../definitions/csp_report_uri_to.yaml | 22 +++--- t/db_dependent/api/v1/public/csp_reports.t | 2 +- 4 files changed, 111 insertions(+), 45 deletions(-) create mode 100644 api/v1/swagger/definitions/csp_report_body.yaml diff --git a/Koha/REST/V1/CSPReports.pm b/Koha/REST/V1/CSPReports.pm index 38ae99ed913..20568affe1f 100644 --- a/Koha/REST/V1/CSPReports.pm +++ b/Koha/REST/V1/CSPReports.pm @@ -46,43 +46,53 @@ is violated. This endpoint logs those reports for administrator review. sub add { my $c = shift->openapi->valid_input or return; - my $report = $c->req->json; - - # CSP reports come wrapped in a 'csp-report' key - my $csp_report = $report->{'csp-report'} // $report->{'body'} // $report; - my $logger = Koha::Logger->get( { interface => 'csp' } ); - # Extract key fields for logging - my $document_uri = $csp_report->{'document-uri'} // 'unknown'; - my $violated_dir = $csp_report->{'violated-directive'} // 'unknown'; - my $blocked_uri = $csp_report->{'blocked-uri'} // 'unknown'; - my $source_file = $csp_report->{'source-file'} // ''; - my $line_number = $csp_report->{'line-number'} // ''; - my $column_number = $csp_report->{'column-number'} // ''; - - # Build location string if available - my $location = ''; - if ($source_file) { - $location = " at $source_file"; - $location .= ":$line_number" if $line_number; - $location .= ":$column_number" if $column_number; - } + my @reports = (); + my $reports_from_json = $c->req->json; + if ( ref $reports_from_json eq 'ARRAY' ) { - $logger->warn( - sprintf( - "CSP violation: '%s' blocked '%s' on page '%s'%s", - $violated_dir, - $blocked_uri, - $document_uri, - $location - ) - ); + #FIXME: We could just take the top X number of reports... + push( @reports, @$reports_from_json ); + } elsif ( ref $reports_from_json eq 'HASH' ) { + push( @reports, $reports_from_json ); + } - # Log full report at debug level for detailed analysis - if ( $logger->is_debug ) { - require JSON; - $logger->debug( "CSP report details: " . JSON::encode_json($csp_report) ); + # CSP reports come wrapped in a 'csp-report' key + foreach my $report (@reports) { + my $csp_report = $report->{'csp-report'} // $report->{body}; + + # Extract key fields for logging + my $document_uri = $csp_report->{'documentURL'} // $csp_report->{'document-uri'} // 'unknown'; + my $violated_dir = $csp_report->{'effectiveDirective'} // $csp_report->{'effective-directive'} // 'unknown'; + my $blocked_uri = $csp_report->{'blockedURL'} // $csp_report->{'blocked-uri'} // 'unknown'; + my $source_file = $csp_report->{'sourceFile'} // $csp_report->{'source-file'} // ''; + my $line_number = $csp_report->{'lineNumber'} // $csp_report->{'line-number'} // ''; + my $column_number = $csp_report->{'columnNumber'} // $csp_report->{'column-number'} // ''; + + # Build location string if available + my $location = ''; + if ($source_file) { + $location = " at $source_file"; + $location .= ":$line_number" if $line_number; + $location .= ":$column_number" if $column_number; + } + + $logger->warn( + sprintf( + "CSP violation: '%s' blocked '%s' on page '%s'%s", + $violated_dir, + $blocked_uri, + $document_uri, + $location + ) + ); + + # Log full report at debug level for detailed analysis + if ( $logger->is_debug ) { + require JSON; + $logger->debug( "CSP report details: " . JSON::encode_json($csp_report) ); + } } return $c->render( diff --git a/api/v1/swagger/definitions/csp_report_body.yaml b/api/v1/swagger/definitions/csp_report_body.yaml new file mode 100644 index 00000000000..82fda7f446f --- /dev/null +++ b/api/v1/swagger/definitions/csp_report_body.yaml @@ -0,0 +1,56 @@ +--- +type: object +description: | + A Content-Security-Policy violation report as sent by browsers. + See https://www.w3.org/TR/CSP3/#violation-reports for specification. +properties: + documentURL: + type: string + description: The URI of the document where the violation occurred + maxLength: 200 + referrer: + type: string + description: The referrer of the document where the violation occurred + maxLength: 200 + effectiveDirective: + type: string + description: The effective directive that was violated + maxLength: 100 + originalPolicy: + type: string + description: The original CSP policy as specified in the header + maxLength: 400 + disposition: + type: string + description: Either "enforce" or "report" depending on CSP mode + enum: + - enforce + - report + blockedURL: + type: string + description: The URI of the resource that was blocked + maxLength: 200 + lineNumber: + type: integer + description: Line number in the document where the violation occurred + minimum: 0 + maximum: 999999 + columnNumber: + type: integer + description: Column number where the violation occurred + maximum: 999999 + minimum: 0 + sample: + type: string + description: First 40 characters of the inline script that caused the violation + maxLength: 41 + sourceFile: + type: string + description: The URI of the script where the violation occurred + maxLength: 200 + statusCode: + type: integer + description: HTTP status code of the document + minimum: 0 + maximum: 999 +additionalProperties: true diff --git a/api/v1/swagger/definitions/csp_report_uri_to.yaml b/api/v1/swagger/definitions/csp_report_uri_to.yaml index 4fb7f01779a..513f915b41a 100644 --- a/api/v1/swagger/definitions/csp_report_uri_to.yaml +++ b/api/v1/swagger/definitions/csp_report_uri_to.yaml @@ -1,27 +1,27 @@ --- -type: object +type: + - array + - object description: | A Content-Security-Policy violation report as sent by browsers. See https://www.w3.org/TR/CSP3/#violation-reports for specification. properties: csp-report: $ref: "csp_report.yaml" + age: + type: integer + minimum: 0 + maximum: 9999999999 body: - $ref: "csp_report.yaml" + $ref: "csp_report_body.yaml" user_agent: type: string maxLength: 300 - destination: - type: string - maxLength: 200 url: type: string maxLength: 200 - timestamp: + type: type: string - maxLength: 50 - attempts: - type: integer - minimum: 0 - maximum: 99 + enum: + - "csp-violation" additionalProperties: true diff --git a/t/db_dependent/api/v1/public/csp_reports.t b/t/db_dependent/api/v1/public/csp_reports.t index 951322daf08..8cbfac4a88e 100755 --- a/t/db_dependent/api/v1/public/csp_reports.t +++ b/t/db_dependent/api/v1/public/csp_reports.t @@ -50,7 +50,7 @@ subtest 'add() tests' => sub { 'csp-report' => { 'document-uri' => 'https://library.example.org/cgi-bin/koha/opac-main.pl', 'referrer' => '', - 'violated-directive' => "script-src 'self' 'nonce-abc123'", + 'violated-directive' => "script-src", 'effective-directive' => 'script-src', 'original-policy' => "default-src 'self'; script-src 'self' 'nonce-abc123'", 'disposition' => 'enforce', -- 2.39.5