From 5b20919fb3ebbde5f05b08b7facc569292bc43b3 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 6 Feb 2020 13:11:17 -0300 Subject: [PATCH] Content Security Policy --- .env.example | 5 ++- app/controllers/api/v1/base_controller.rb | 3 -- .../api/v1/csp_reports_controller.rb | 42 +++++++++++++++++++ app/controllers/api/v1/sites_controller.rb | 3 ++ app/models/csp_report.rb | 4 ++ .../initializers/content_security_policy.rb | 41 +++++++++--------- config/initializers/mime_types.rb | 5 +-- config/routes.rb | 1 + .../20200205173039_create_csp_reports.rb | 17 ++++++++ ...0200206151057_add_source_to_csp_reports.rb | 7 ++++ db/schema.rb | 9 +++- .../api/v1/csp_reports_controller_test.rb | 25 +++++++++++ 12 files changed, 133 insertions(+), 29 deletions(-) create mode 100644 app/controllers/api/v1/csp_reports_controller.rb create mode 100644 app/models/csp_report.rb create mode 100644 db/migrate/20200205173039_create_csp_reports.rb create mode 100644 db/migrate/20200206151057_add_source_to_csp_reports.rb create mode 100644 test/controllers/api/v1/csp_reports_controller_test.rb diff --git a/.env.example b/.env.example index eea8055d..ac0751b1 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,8 @@ -RAILS_ENV=production +RAILS_ENV= IMAP_SERVER= DEFAULT_FROM= SKEL_SUTTY=https://0xacab.org/sutty/skel.sutty.nl -SUTTY=sutty.nl +SUTTY=sutty.local +SUTTY_WITH_PORT=sutty.local:3000 REDIS_SERVER= REDIS_CLIENT= diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index 517fa28f..6f76e51a 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -4,9 +4,6 @@ module Api module V1 # API class BaseController < ActionController::Base - http_basic_authenticate_with name: ENV['HTTP_BASIC_USER'], - password: ENV['HTTP_BASIC_PASSWORD'] - protect_from_forgery with: :null_session respond_to :json end diff --git a/app/controllers/api/v1/csp_reports_controller.rb b/app/controllers/api/v1/csp_reports_controller.rb new file mode 100644 index 00000000..cdce92d6 --- /dev/null +++ b/app/controllers/api/v1/csp_reports_controller.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Api + module V1 + # Recibe los reportes de Content Security Policy + class CspReportsController < BaseController + # Crea un reporte de CSP intercambiando los guiones medios por + # bajos + # + # TODO: Aplicar rate_limit + def create + csp = CspReport.new(csp_report_params.to_h.map do |k, v| + { k.tr('-', '_') => v } + end.inject(&:merge)) + + csp.id = SecureRandom.uuid + csp.save + + render json: {}, status: :created + end + + private + + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only#Violation_report_syntax + def csp_report_params + params.require(:'csp-report') + .permit(:disposition, + :referrer, + :'blocked-uri', + :'document-uri', + :'effective-directive', + :'original-policy', + :'script-sample', + :'status-code', + :'violated-directive', + :'line-number', + :'column-number', + :'source-file') + end + end + end +end diff --git a/app/controllers/api/v1/sites_controller.rb b/app/controllers/api/v1/sites_controller.rb index 8bea164e..18550feb 100644 --- a/app/controllers/api/v1/sites_controller.rb +++ b/app/controllers/api/v1/sites_controller.rb @@ -4,6 +4,9 @@ module Api module V1 # API para sitios class SitesController < BaseController + http_basic_authenticate_with name: ENV['HTTP_BASIC_USER'], + password: ENV['HTTP_BASIC_PASSWORD'] + def index render json: Site.all.order(:name).pluck(:name) end diff --git a/app/models/csp_report.rb b/app/models/csp_report.rb new file mode 100644 index 00000000..e76c23bb --- /dev/null +++ b/app/models/csp_report.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +# Almacena un reporte de CSP +class CspReport < ApplicationRecord; end diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index cccb03cb..5423271b 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -6,31 +6,34 @@ # For further information see the following documentation # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy -# Rails.application.config.content_security_policy do |policy| -# policy.default_src :self, :https -# policy.font_src :self, :https, :data -# policy.img_src :self, :https, :data -# policy.object_src :none -# policy.script_src :self, :https -# policy.style_src :self, :https -# # If you are using webpack-dev-server then specify -# # webpack-dev-server host -# policy.connect_src :self, :https, "http://localhost:3035", -# "ws://localhost:3035" if Rails.env.development? +Rails.application.config.content_security_policy do |policy| + policy.default_src :self + # XXX: Varios scripts generan estilos en línea + policy.style_src :self, :unsafe_inline + # Repetimos la default para poder saber cuál es la política en falta + policy.script_src :self + policy.font_src :self + # TODO: Permitimos cargar imágenes remotas? + policy.img_src :self + # Ya no usamos applets! + policy.object_src :none + if Rails.env.development? + policy.connect_src :self, + 'http://localhost:3035', + 'ws://localhost:3035' + end -# # Specify URI for violation reports -# # policy.report_uri "/csp-violation-report-endpoint" -# end + # Specify URI for violation reports + policy.report_uri "https://api.#{ENV.fetch('SUTTY_WITH_PORT', 'sutty.nl')}/v1/csp_reports.json" +end # If you are using UJS then enable automatic nonce generation -# Rails.application.config.content_security_policy_nonce_generator = -# -> request { SecureRandom.base64(16) } +# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } # Set the nonce only to specific directives -# Rails.application.config.content_security_policy_nonce_directives = -# %w(script-src) +# Rails.application.config.content_security_policy_nonce_directives = %w(script-src) # Report CSP violations to a specified URI # For further information see the following documentation: # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only -# Rails.application.config.content_security_policy_report_only = true +Rails.application.config.content_security_policy_report_only = false diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index be6fedc5..1836ef30 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -1,6 +1,3 @@ # frozen_string_literal: true -# Be sure to restart your server when you modify this file. - -# Add new mime types for use in respond_to blocks: -# Mime::Type.register "text/richtext", :rtf +Mime::Type.register 'application/csp-report', :json diff --git a/config/routes.rb b/config/routes.rb index 8ef82cbc..abad9ea7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -18,6 +18,7 @@ Rails.application.routes.draw do constraints subdomain: 'api' do scope module: 'api' do namespace :v1 do + resources :csp_reports, only: %i[create] get 'sites/allowed', to: 'sites#allowed' resources :sites, only: %i[index] end diff --git a/db/migrate/20200205173039_create_csp_reports.rb b/db/migrate/20200205173039_create_csp_reports.rb new file mode 100644 index 00000000..2a0f4051 --- /dev/null +++ b/db/migrate/20200205173039_create_csp_reports.rb @@ -0,0 +1,17 @@ +class CreateCspReports < ActiveRecord::Migration[6.0] + def change + create_table :csp_reports, id: :uuid do |t| + t.timestamps + + t.string :disposition + t.string :referrer + t.string :blocked_uri + t.string :document_uri + t.string :effective_directive + t.string :original_policy + t.string :script_sample + t.string :status_code + t.string :violated_directive + end + end +end diff --git a/db/migrate/20200206151057_add_source_to_csp_reports.rb b/db/migrate/20200206151057_add_source_to_csp_reports.rb new file mode 100644 index 00000000..9b1dd8fb --- /dev/null +++ b/db/migrate/20200206151057_add_source_to_csp_reports.rb @@ -0,0 +1,7 @@ +class AddSourceToCspReports < ActiveRecord::Migration[6.0] + def change + add_column :csp_reports, :column_number, :integer + add_column :csp_reports, :line_number, :integer + add_column :csp_reports, :source_file, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 6d63cb91..5d0d00a8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -12,7 +12,10 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20_190_829_180_743) do +ActiveRecord::Schema.define(version: 20_200_206_151_057) do + # Could not dump table "access_logs" because of following StandardError + # Unknown type '' for column 'id' + create_table 'action_text_rich_texts', force: :cascade do |t| t.string 'name', null: false t.text 'body' @@ -55,6 +58,9 @@ ActiveRecord::Schema.define(version: 20_190_829_180_743) do t.index ['deploy_id'], name: 'index_build_stats_on_deploy_id' end + # Could not dump table "csp_reports" because of following StandardError + # Unknown type 'uuid' for column 'id' + create_table 'deploys', force: :cascade do |t| t.datetime 'created_at', null: false t.datetime 'updated_at', null: false @@ -132,6 +138,7 @@ ActiveRecord::Schema.define(version: 20_190_829_180_743) do t.string 'status', default: 'waiting' t.text 'description' t.string 'title' + t.boolean 'colaboracion_anonima', default: false t.index ['design_id'], name: 'index_sites_on_design_id' t.index ['licencia_id'], name: 'index_sites_on_licencia_id' t.index ['name'], name: 'index_sites_on_name', unique: true diff --git a/test/controllers/api/v1/csp_reports_controller_test.rb b/test/controllers/api/v1/csp_reports_controller_test.rb new file mode 100644 index 00000000..c98fd860 --- /dev/null +++ b/test/controllers/api/v1/csp_reports_controller_test.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'test_helper' + +module Api + module V1 + class CSPReportsControllerTest < ActionDispatch::IntegrationTest + test 'se puede enviar un reporte' do + post v1_csp_reports_url, + params: { + 'csp-report': { + 'document-uri': 'http://example.com/signup.html', + 'referrer': '', + 'blocked-uri': 'http://example.com/css/style.css', + 'violated-directive': 'style-src cdn.example.com', + 'original-policy': "default-src 'none'; style-src cdn.example.com; report-uri /_/csp-reports" + } + } + + assert_equal 201, response.status + assert_equal 1, CspReport.all.count + end + end + end +end