module Api class CspController < ApplicationController # CSP violation reports don't need authentication skip_before_action :verify_authenticity_token allow_unauthenticated_access # POST /api/csp-violation-report def violation_report # Parse CSP violation report report_data = JSON.parse(request.body.read) csp_report = report_data["csp-report"] # Validate that we have a proper CSP report unless csp_report.is_a?(Hash) && csp_report.present? Rails.logger.warn "Received empty or invalid CSP violation report" head :bad_request return end # Log the violation for security monitoring Rails.logger.warn "CSP Violation Report:" Rails.logger.warn " Blocked URI: #{csp_report["blocked-uri"]}" Rails.logger.warn " Document URI: #{csp_report["document-uri"]}" Rails.logger.warn " Referrer: #{csp_report["referrer"]}" Rails.logger.warn " Violated Directive: #{csp_report["violated-directive"]}" Rails.logger.warn " Original Policy: #{csp_report["original-policy"]}" Rails.logger.warn " User Agent: #{request.user_agent}" Rails.logger.warn " IP Address: #{request.remote_ip}" # Emit structured event for CSP violation # This allows multiple subscribers to process the event (Sentry, local logging, etc.) Rails.event.notify("csp.violation", { blocked_uri: csp_report["blocked-uri"], document_uri: csp_report["document-uri"], referrer: csp_report["referrer"], violated_directive: csp_report["violated-directive"], original_policy: csp_report["original-policy"], disposition: csp_report["disposition"], effective_directive: csp_report["effective-directive"], source_file: csp_report["source-file"], line_number: csp_report["line-number"], column_number: csp_report["column-number"], status_code: csp_report["status-code"], user_agent: request.user_agent, ip_address: request.remote_ip, current_user_id: Current.user&.id, timestamp: Time.current, session_id: Current.session&.id }) head :no_content rescue JSON::ParserError => e Rails.logger.error "Invalid CSP violation report: #{e.message}" head :bad_request end end end