# Jean-Philippe Doyle, Copyright (c) 2015 # Licence MIT # @see http://opensource.org/licenses/MIT # Logs CSP report in rails logs sent to REPORT_URI class CSPReportLogger BLOCKED_URI_KEY = 'blocked-uri'.freeze CSP_REPORT_KEY = 'csp-report'.freeze IGNORE = %w{safari-extension://}.map { |src| /\A#{src}/.freeze }.freeze LOG_HEADER = '[SECURITY] CSP Violation Report'.freeze LOG_INVALID = 'INVALID FORMAT'.freeze LOG_KEYS = %w{blocked-uri document-uri line-number source-file violated-directive}.freeze POST_BODY = 'rack.input'.freeze REPORT_METHOD = 'POST'.freeze REPORT_URI = '/errors/csp'.freeze REQUEST_UUID = 'action_dispatch.request_id'.freeze # rails-only too def initialize(app) @app = app end def call(env) if REPORT_METHOD == env['REQUEST_METHOD'] && REPORT_URI == env['REQUEST_PATH'] report!(env) [204, {}, []] else @app.call(env) end end def report!(env) report = JSON.parse(env[POST_BODY])[CSP_REPORT_KEY] return log_invalid_body(env) if report.nil? return if ignored?(report) log(JSON.pretty_generate(report.slice(*LOG_KEYS)), env) rescue JSON::ParserError log_invalid_body(env) end private def log_invalid_body(env) log("#{LOG_INVALID}\n#{env[POST_BODY].read}", env) end def log(msg, env) Rails.logger.info("[#{env[REQUEST_UUID]}}] #{LOG_HEADER}:\n#{msg}") end def ignored?(report) return unless report[BLOCKED_URI_KEY] IGNORE.any? { |d| d =~ report[BLOCKED_URI_KEY] } end end