From ba05f42e3fdeba6eaa44702116506efe06445408 Mon Sep 17 00:00:00 2001 From: Soumya Ray Date: Thu, 17 Jun 2021 16:26:30 +0800 Subject: [PATCH 1/3] Sign non-authenticated requests to API --- Gemfile | 1 + Rakefile | 10 ++++++++++ app/controllers/auth.rb | 8 ++++---- app/controllers/documents.rb | 1 + app/controllers/projects.rb | 1 + app/lib/signed_message.rb | 20 +++++++++++++++++++ app/presentation/assets/css/style.css | 5 +++++ app/presentation/views/document.slim | 2 +- .../views/document_new_partial.slim | 2 +- app/presentation/views/layout.slim | 9 +++++---- app/presentation/views/login.slim | 5 +---- app/presentation/views/nav.slim | 2 +- app/presentation/views/project.slim | 2 +- app/presentation/views/projects_all.slim | 6 +++--- app/services/authenticate_account.rb | 10 +++++----- app/services/authorize_github_account.rb | 16 ++++++++------- app/services/create_account.rb | 6 ++---- app/services/verify_registration.rb | 7 +++++-- config/environments.rb | 5 +++++ config/secrets.example.yml | 5 +++++ spec/integration/service_authenticate_spec.rb | 4 ++-- 21 files changed, 88 insertions(+), 39 deletions(-) create mode 100644 app/lib/signed_message.rb diff --git a/Gemfile b/Gemfile index 3299b1b..fdae92d 100644 --- a/Gemfile +++ b/Gemfile @@ -23,6 +23,7 @@ gem 'redis', '~>5.0' gem 'dry-validation', '~>1.10' gem 'rack-ssl-enforcer' gem 'rbnacl', '~>7.1' +gem 'secure_headers' # Debugging gem 'pry' diff --git a/Rakefile b/Rakefile index 0ab65d5..f5ce16f 100644 --- a/Rakefile +++ b/Rakefile @@ -63,6 +63,16 @@ namespace :generate do end end +namespace :url do + # usage: $ rake url:integrity URL=http://example.org/script.js + desc 'Generate integrity hash for a URL' + task :integrity do + sha384 = `curl -L -s #{ENV['URL']} | openssl dgst -sha384 -binary | \ + openssl enc -base64` + puts "sha384-#{sha384}" + end +end + namespace :session do desc 'Wipe all sessions stored in Redis' task :wipe => :load_lib do diff --git a/app/controllers/auth.rb b/app/controllers/auth.rb index a307c33..5aaff3d 100644 --- a/app/controllers/auth.rb +++ b/app/controllers/auth.rb @@ -15,7 +15,6 @@ def gh_oauth_url(config) end route('auth') do |routing| - @oauth_callback = '/auth/sso_callback' @login_route = '/auth/login' routing.is 'login' do # GET /auth/login @@ -34,7 +33,8 @@ def gh_oauth_url(config) routing.redirect @login_route end - authenticated = AuthenticateAccount.new.call(**credentials.values) + authenticated = AuthenticateAccount.new(App.config) + .call(**credentials.values) current_account = Account.new( authenticated[:account], @@ -46,9 +46,9 @@ def gh_oauth_url(config) flash[:notice] = "Welcome back #{current_account.username}!" routing.redirect '/projects' rescue AuthenticateAccount::NotAuthenticatedError - flash[:error] = 'Username and password did not match our records' + flash.now[:error] = 'Username and password did not match our records' response.status = 401 - routing.redirect @login_route + view :login rescue AuthenticateAccount::ApiServerError => e App.logger.warn "API server error: #{e.inspect}\n#{e.backtrace}" flash[:error] = 'Our servers are not responding -- please try later' diff --git a/app/controllers/documents.rb b/app/controllers/documents.rb index 3a2222f..d3cb5ef 100644 --- a/app/controllers/documents.rb +++ b/app/controllers/documents.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'roda' +require_relative './app' module Credence # Web controller for Credence API diff --git a/app/controllers/projects.rb b/app/controllers/projects.rb index a77d4fe..2fc7cfa 100644 --- a/app/controllers/projects.rb +++ b/app/controllers/projects.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'roda' +require_relative './app' module Credence # Web controller for Credence API diff --git a/app/lib/signed_message.rb b/app/lib/signed_message.rb new file mode 100644 index 0000000..06a42c5 --- /dev/null +++ b/app/lib/signed_message.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'base64' +require 'rbnacl' + +# Encrypt and Decrypt from Database +class SignedMessage + # Call setup once to pass in config variable with SIGNING_KEY attribute + def self.setup(config) + @signing_key = Base64.strict_decode64(config.SIGNING_KEY) + end + + def self.sign(message) + signature = RbNaCl::SigningKey.new(@signing_key) + .sign(message.to_json) + .then { |sig| Base64.strict_encode64(sig) } + + { data: message, signature: signature } + end +end diff --git a/app/presentation/assets/css/style.css b/app/presentation/assets/css/style.css index 47424c0..2901e73 100644 --- a/app/presentation/assets/css/style.css +++ b/app/presentation/assets/css/style.css @@ -6,4 +6,9 @@ .top-buffer { margin-top:20px; +} + +.logo { + max-width:35px; + margin-top: -7px; } \ No newline at end of file diff --git a/app/presentation/views/document.slim b/app/presentation/views/document.slim index 4f370b1..cd48337 100644 --- a/app/presentation/views/document.slim +++ b/app/presentation/views/document.slim @@ -14,5 +14,5 @@ dl dd class="blockquote" a data-bs-toggle="collapse" href="#document" aria-expanded="false" aria-controls="document" show document div id="document" class="collapse" - dov class="card card-body" + div class="card card-body" pre class="force-wrap" #{document.content} diff --git a/app/presentation/views/document_new_partial.slim b/app/presentation/views/document_new_partial.slim index c7fe207..9a16dce 100644 --- a/app/presentation/views/document_new_partial.slim +++ b/app/presentation/views/document_new_partial.slim @@ -11,7 +11,7 @@ div id="newDocumentModal" class="modal fade" role="dialog" input type='text' name='filename' id='filename_input' class="form-control" placeholder="Filename is required" div class="form-group" label for='relative_path_input' Relative File Path: - span class="small" style="margin-left:10px;" (relative to project root) + span class="small ms-2" (relative to project root) input type='text' name='relative_path' id='relative_path_input' class="form-control" placeholder="(optional)" value="/" div class="form-group" label for='description_input' Description: diff --git a/app/presentation/views/layout.slim b/app/presentation/views/layout.slim index e6c9061..57f6c2a 100644 --- a/app/presentation/views/layout.slim +++ b/app/presentation/views/layout.slim @@ -5,15 +5,16 @@ html / Themed Bootstrap CSS (Cerulean Theme) - see bootswatch.com for more themes / - default Bootstrap CSS: https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/bootstrap.min.css - link rel="stylesheet" href="https://bootswatch.com/5/cerulean/bootstrap.min.css" - link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/FortAwesome/Font-Awesome@5.15.3/css/all.min.css" + link rel="shortcut icon" href="#" + link rel="stylesheet" href="https://bootswatch.com/5/cerulean/bootstrap.min.css" integrity="sha384-QIHD79FAZ7/24rhhTveEfwJuiAeoPTOQpcVTWsfuPqh3JhPaV6LW8H0cTshmT0Jn" crossorigin="anonymous" + link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/FortAwesome/Font-Awesome@5.15.3/css/all.min.css" integrity="sha384-SZXxX4whJ79/gErwcOYf+zWLeJdY/qpuqC4cAa9rOGUstPomtqpuNWT9wdPEn2fk" crossorigin="anonymous" / Custom CSS == assets(:css) body / Popper & Bootstrap Javascript (Load in this order) - script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" - script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.min.js" + script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous" + script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/js/bootstrap.min.js" integrity="sha384-lpyLfhYuitXl2zRZ5Bn2fqnhNAKOAaM/0Kr9laMspuaMiZfGmfwRNFh8HlMy49eQ" crossorigin="anonymous" == render :nav div class='container' id='html_body' diff --git a/app/presentation/views/login.slim b/app/presentation/views/login.slim index a54c2f3..fd379d8 100644 --- a/app/presentation/views/login.slim +++ b/app/presentation/views/login.slim @@ -1,7 +1,4 @@ / Fontawesome and Bootstrap-Social for Github buttons -link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.15.3/css/fontawesome.min.css" -link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-social/5.0.0/bootstrap-social.css" - div class="row" div class="col-sm-1" div class="col-sm-4" @@ -23,5 +20,5 @@ div class="row" .row a class="btn btn-block btn-social btn-github" href=gh_oauth_url span class="fab fa-github" - == "Github Sign-in" + == " Github Sign-in" .col-sm-1 \ No newline at end of file diff --git a/app/presentation/views/nav.slim b/app/presentation/views/nav.slim index f9d714a..e2f8ac1 100644 --- a/app/presentation/views/nav.slim +++ b/app/presentation/views/nav.slim @@ -1,7 +1,7 @@ nav role="navigation" class="navbar navbar-expand-sm navbar-dark bg-primary" div class="container-fluid" a class="navbar-brand" rel="home" href="/" title="Securely Share Configurations" - img alt="Credence" src="/logo.png" style="max-width:35px; margin-top: -7px;" + img alt="Credence" src="/logo.png" class="logo" a class="navbar-brand" href="/" Credence button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNavbar" aria-controls="mainNavbar" aria-expanded="false" aria-label="Toggle navigation" span class="navbar-toggler-icon" diff --git a/app/presentation/views/project.slim b/app/presentation/views/project.slim index 0be5011..8266390 100644 --- a/app/presentation/views/project.slim +++ b/app/presentation/views/project.slim @@ -28,7 +28,7 @@ div class="row" div class="col-xs-4" - if project.policies.can_add_documents button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#newDocumentModal" + New Document - div class="row" style="height:10px;" + div class="row mt-2" div class="col-lg-3" div class="panel panel-default" div class="panel-heading" Collaborators diff --git a/app/presentation/views/projects_all.slim b/app/presentation/views/projects_all.slim index 77d2286..749f22a 100644 --- a/app/presentation/views/projects_all.slim +++ b/app/presentation/views/projects_all.slim @@ -1,7 +1,7 @@ -div class="row" style="vertical-align: bottom;" - div class="col-xs-9" +div class="row mt-2" + div class="col-md-9" h3 Projects - div class="col-xs-3" + div class="col-md-3" button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#newProjectModal" + New Project table class="table" thead diff --git a/app/services/authenticate_account.rb b/app/services/authenticate_account.rb index 6ce6735..9720c21 100644 --- a/app/services/authenticate_account.rb +++ b/app/services/authenticate_account.rb @@ -10,18 +10,18 @@ class NotAuthenticatedError < StandardError; end class ApiServerError < StandardError; end def call(username:, password:) + credentials = { username: username, password: password } + response = HTTP.post("#{ENV['API_URL']}/auth/authenticate", - json: { username:, password: }) + json: SignedMessage.sign(credentials)) raise(NotAuthenticatedError) if response.code == 401 raise(ApiServerError) if response.code != 200 account_info = JSON.parse(response.to_s)['data']['attributes'] - { - account: account_info['account'], - auth_token: account_info['auth_token'] - } + { account: account_info['account'], + auth_token: account_info['auth_token'] } end end end diff --git a/app/services/authorize_github_account.rb b/app/services/authorize_github_account.rb index 22da3d8..4cee169 100644 --- a/app/services/authorize_github_account.rb +++ b/app/services/authorize_github_account.rb @@ -36,17 +36,19 @@ def get_access_token_from_github(code) end def get_sso_account_from_api(access_token) - response = - HTTP.post("#{@config.API_URL}/auth/sso", - json: { access_token: access_token }) + signed_sso_info = { access_token: access_token } + .then { |sso_info| SignedMessage.sign(sso_info) } + + response = HTTP.post( + "#{@config.API_URL}/auth/sso", + json: signed_sso_info + ) raise if response.code >= 400 account_info = JSON.parse(response)['data']['attributes'] - { - account: account_info['account'], - auth_token: account_info['auth_token'] - } + { account: account_info['account'], + auth_token: account_info['auth_token'] } end end end diff --git a/app/services/create_account.rb b/app/services/create_account.rb index c6757fd..c69345d 100644 --- a/app/services/create_account.rb +++ b/app/services/create_account.rb @@ -15,13 +15,11 @@ def initialize(config) end def call(email:, username:, password:) - message = { email:, - username:, - password: } + account = { email: , username: , password: } response = HTTP.post( "#{@config.API_URL}/accounts/", - json: message + json: SignedMessage.sign(account) ) raise InvalidAccount unless response.code == 201 diff --git a/app/services/verify_registration.rb b/app/services/verify_registration.rb index 8469e90..e2ed9a7 100644 --- a/app/services/verify_registration.rb +++ b/app/services/verify_registration.rb @@ -18,8 +18,11 @@ def call(registration_data) reg_details['verification_url'] = "#{@config.APP_URL}/auth/register/#{registration_token}" - response = HTTP.post("#{@config.API_URL}/auth/register", - json: reg_details) + response = HTTP.post( + "#{@config.API_URL}/auth/register", +json: SignedMessage.sign(reg_details) + ) + raise(VerificationError) unless response.code == 202 JSON.parse(response.to_s) diff --git a/config/environments.rb b/config/environments.rb index 3d901c0..8a947f5 100644 --- a/config/environments.rb +++ b/config/environments.rb @@ -32,6 +32,7 @@ def self.logger = LOGGER configure do SecureMessage.setup(ENV.delete('MSG_KEY')) + SignedMessage.setup(config) end configure :production do @@ -41,6 +42,8 @@ def self.logger = LOGGER use Rack::Session::Redis, expire_after: ONE_MONTH, + httponly: true, + same_site: :strict, redis_server: { url: ENV.delete('REDIS_TLS_URL'), ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE } @@ -61,6 +64,8 @@ def self.logger = LOGGER # use Rack::Session::Redis, # expire_after: ONE_MONTH, + # httponly: true, + # same_site: :strict, # redis_server: { # url: ENV.delete('REDIS_URL') # } diff --git a/config/secrets.example.yml b/config/secrets.example.yml index 732ea36..5594ada 100644 --- a/config/secrets.example.yml +++ b/config/secrets.example.yml @@ -7,6 +7,8 @@ development: SESSION_SECRET: ND3mZpWrmpAEEjke2N7eJn65IrUPLtCxwYSaql1b0L0+LgaxMPik2Eb9DEYsNSV4SWC4FCOdKtsQ8IIangRYDQ== MSG_KEY: Hb9DLQWSXyisHm8CArBXaIw9crv/wpKTVObm6u8xhxw= REDIS_URL: + SIGNING_KEY: nNuYJVHnMTPfArqFo3Rb81xvXoPDuqcdoUhjWVfGooE= + REDIS_TLS_URL: GH_OAUTH_URL: https://github.com/login/oauth/authorize GH_TOKEN_URL: https://github.com/login/oauth/access_token GH_SCOPE: user:email @@ -19,6 +21,8 @@ test: SESSION_SECRET: ND3mZpWrmpAEEjke2N7eJn65IrUPLtCxwYSaql1b0L0+LgaxMPik2Eb9DEYsNSV4SWC4FCOdKtsQ8IIangRYDQ== MSG_KEY: Hb9DLQWSXyisHm8CArBXaIw9crv/wpKTVObm6u8xhxw= REDIS_URL: + SIGNING_KEY: nNuYJVHnMTPfArqFo3Rb81xvXoPDuqcdoUhjWVfGooE= + REDIS_TLS_URL: GH_OAUTH_URL: https://github.com/login/oauth/authorize GH_TOKEN_URL: https://github.com/login/oauth/access_token GH_SCOPE: user:email @@ -30,6 +34,7 @@ production: APP_URL: http://localhost:9292 SESSION_SECRET: MSG_KEY: + SIGNING_KEY: REDIS_TLS_URL: GH_OAUTH_URL: https://github.com/login/oauth/authorize GH_TOKEN_URL: https://github.com/login/oauth/access_token diff --git a/spec/integration/service_authenticate_spec.rb b/spec/integration/service_authenticate_spec.rb index a26e209..08a6f1c 100644 --- a/spec/integration/service_authenticate_spec.rb +++ b/spec/integration/service_authenticate_spec.rb @@ -27,7 +27,7 @@ auth_return_json = File.read(auth_account_file) WebMock.stub_request(:post, "#{API_URL}/auth/authenticate") - .with(body: @credentials.to_json) + .with(body: SignedMessage.sign(@credentials).to_json) .to_return(body: auth_return_json, headers: { 'content-type' => 'application/json' }) @@ -41,7 +41,7 @@ it 'BAD: should not find a false authenticated account' do WebMock.stub_request(:post, "#{API_URL}/auth/authenticate") - .with(body: @mal_credentials.to_json) + .with(body: SignedMessage.sign(@mal_credentials).to_json) .to_return(status: 401) _(proc { Credence::AuthenticateAccount.new.call(**@mal_credentials) From ffde4809830086909663f95a88e9eed95f77154e Mon Sep 17 00:00:00 2001 From: Soumya Ray Date: Thu, 28 Apr 2022 15:21:21 +0800 Subject: [PATCH 2/3] Configure browser-side CSPs and security headers --- .rubocop.yml | 8 ++-- Gemfile.lock | 12 ++--- Rakefile | 6 +-- app/controllers/auth.rb | 5 +-- app/controllers/security.rb | 70 ++++++++++++++++++++++++++++++ app/presentation/views/layout.slim | 2 +- app/presentation/views/login.slim | 3 ++ config/environments.rb | 31 +++++++------ 8 files changed, 104 insertions(+), 33 deletions(-) create mode 100644 app/controllers/security.rb diff --git a/.rubocop.yml b/.rubocop.yml index 1691893..88ef3ae 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -20,10 +20,10 @@ Security/YAMLLoad: Exclude: - spec/**/* -# Style/HashSyntax: -# Enabled: true -# Exclude: -# - Rakefile +Style/HashSyntax: + Enabled: true + Exclude: + - Rakefile Style/SymbolArray: Enabled: true diff --git a/Gemfile.lock b/Gemfile.lock index 27f8cd0..1384885 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -22,7 +22,7 @@ GEM ast (2.4.2) coderay (1.1.3) concurrent-ruby (1.2.2) - connection_pool (2.4.0) + connection_pool (2.4.1) crack (0.4.5) rexml domain_name (0.5.20190701) @@ -39,7 +39,7 @@ GEM concurrent-ruby (~> 1.0) dry-core (~> 1.0, < 2) zeitwerk (~> 2.6) - dry-schema (1.13.1) + dry-schema (1.13.2) concurrent-ruby (~> 1.0) dry-configurable (~> 1.0, >= 1.0.1) dry-core (~> 1.0, < 2) @@ -93,7 +93,7 @@ GEM coderay (~> 1.1) method_source (~> 1.0) public_suffix (5.0.1) - puma (6.2.2) + puma (6.3.0) nio4r (~> 2.0) rack (3.0.7) rack-session (2.0.0) @@ -130,14 +130,15 @@ GEM unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.28.1) parser (>= 3.2.1.0) - rubocop-performance (1.17.1) + rubocop-performance (1.18.0) rubocop (>= 1.7.0, < 2.0) rubocop-ast (>= 0.4.0) ruby-progressbar (1.13.0) + secure_headers (6.5.0) slim (5.1.1) temple (~> 0.10.0) tilt (>= 2.1.0) - temple (0.10.1) + temple (0.10.2) thor (1.2.2) tilt (2.1.0) unf (0.1.4) @@ -174,6 +175,7 @@ DEPENDENCIES roda (~> 3.54) rubocop rubocop-performance + secure_headers slim webmock diff --git a/Rakefile b/Rakefile index f5ce16f..b19566f 100644 --- a/Rakefile +++ b/Rakefile @@ -1,11 +1,10 @@ # frozen_string_literal: true -# rubocop:disable Style/HashSyntax require 'rake/testtask' require './require_app' task :print_env do - puts "Environment: #{ENV.fetch('RACK_ENV', 'development')}" + puts "Environment: #{ENV['RACK_ENV'] || 'development'}" end desc 'Run application console (pry)' @@ -65,7 +64,7 @@ end namespace :url do # usage: $ rake url:integrity URL=http://example.org/script.js - desc 'Generate integrity hash for a URL' + desc 'Generate integrity hash for a URL (argument: URL=...)' task :integrity do sha384 = `curl -L -s #{ENV['URL']} | openssl dgst -sha384 -binary | \ openssl enc -base64` @@ -82,4 +81,3 @@ namespace :session do puts "#{wiped.count} sessions deleted" end end -# rubocop:enable Style/HashSyntax diff --git a/app/controllers/auth.rb b/app/controllers/auth.rb index 5aaff3d..1ea9c69 100644 --- a/app/controllers/auth.rb +++ b/app/controllers/auth.rb @@ -5,7 +5,7 @@ module Credence # Web controller for Credence App - class App < Roda + class App < Roda # rubocop:disable Metrics/ClassLength def gh_oauth_url(config) url = config.GH_OAUTH_URL client_id = config.GH_CLIENT_ID @@ -33,8 +33,7 @@ def gh_oauth_url(config) routing.redirect @login_route end - authenticated = AuthenticateAccount.new(App.config) - .call(**credentials.values) + authenticated = AuthenticateAccount.new.call(**credentials.values) current_account = Account.new( authenticated[:account], diff --git a/app/controllers/security.rb b/app/controllers/security.rb new file mode 100644 index 0000000..f86d72d --- /dev/null +++ b/app/controllers/security.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require_relative './app' +require 'roda' + +require 'rack/ssl-enforcer' +require 'secure_headers' + +module Credence + # Configuration for the API + class App < Roda + plugin :environments + plugin :multi_route + + FONT_SRC = %w[https://cdn.jsdelivr.net].freeze + SCRIPT_SRC = %w[https://cdn.jsdelivr.net].freeze + STYLE_SRC = %w[https://bootswatch.com https://cdn.jsdelivr.net https://cdnjs.cloudflare.com].freeze + + configure :production do + use Rack::SslEnforcer, hsts: true + end + + ## Uncomment to drop the login session in case of any violation + # use Rack::Protection, reaction: :drop_session + use SecureHeaders::Middleware + + SecureHeaders::Configuration.default do |config| + config.cookies = { + secure: true, + httponly: true, + samesite: { + strict: true + } + } + + config.x_frame_options = 'DENY' + config.x_content_type_options = 'nosniff' + config.x_xss_protection = '1' + config.x_permitted_cross_domain_policies = 'none' + config.referrer_policy = 'origin-when-cross-origin' + + # note: single-quotes needed around 'self' and 'none' in CSPs + # rubocop:disable Lint/PercentStringArray + config.csp = { + report_only: false, + preserve_schemes: true, + default_src: %w['self'], + child_src: %w['self'], + connect_src: %w[wws:], + img_src: %w['self'], + font_src: %w['self'] + FONT_SRC, + script_src: %w['self'] + SCRIPT_SRC, + style_src: %W['self'] + STYLE_SRC, + form_action: %w['self'], + frame_ancestors: %w['none'], + object_src: %w['none'], + block_all_mixed_content: true, + report_uri: %w[/security/report_csp_violation] + } + # rubocop:enable Lint/PercentStringArray + end + + route('security') do |routing| + # POST security/report_csp_violation + routing.post 'report_csp_violation' do + App.logger.warn "CSP VIOLATION: #{request.body.read}" + end + end + end +end diff --git a/app/presentation/views/layout.slim b/app/presentation/views/layout.slim index 57f6c2a..9188d78 100644 --- a/app/presentation/views/layout.slim +++ b/app/presentation/views/layout.slim @@ -6,7 +6,7 @@ html / Themed Bootstrap CSS (Cerulean Theme) - see bootswatch.com for more themes / - default Bootstrap CSS: https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/bootstrap.min.css link rel="shortcut icon" href="#" - link rel="stylesheet" href="https://bootswatch.com/5/cerulean/bootstrap.min.css" integrity="sha384-QIHD79FAZ7/24rhhTveEfwJuiAeoPTOQpcVTWsfuPqh3JhPaV6LW8H0cTshmT0Jn" crossorigin="anonymous" + link rel="stylesheet" href="https://bootswatch.com/5/cerulean/bootstrap.min.css" integrity="sha384-o4WgiBhAVOIqsqKeCjtWf1R3Geq/+/n9GUqjC5jbSYJoHOQf/G0znnM9uaG9qGMv" crossorigin="anonymous" link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/FortAwesome/Font-Awesome@5.15.3/css/all.min.css" integrity="sha384-SZXxX4whJ79/gErwcOYf+zWLeJdY/qpuqC4cAa9rOGUstPomtqpuNWT9wdPEn2fk" crossorigin="anonymous" / Custom CSS diff --git a/app/presentation/views/login.slim b/app/presentation/views/login.slim index fd379d8..ca40986 100644 --- a/app/presentation/views/login.slim +++ b/app/presentation/views/login.slim @@ -1,4 +1,7 @@ / Fontawesome and Bootstrap-Social for Github buttons +link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@5.15.3/css/fontawesome.min.css" integrity="sha384-wESLQ85D6gbsF459vf1CiZ2+rr+CsxRY0RpiF1tLlQpDnAgg6rwdsUF1+Ics2bni" crossorigin="anonymous" +link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-social/5.0.0/bootstrap-social.css" integrity="sha384-KMis1ci1uYEByUqyQ+ys92P9u9WtqmYQmUvEpXsB5BuMYyTHljPVNJ24Jl6ib3o8" crossorigin="anonymous" + div class="row" div class="col-sm-1" div class="col-sm-4" diff --git a/config/environments.rb b/config/environments.rb index 8a947f5..75af65c 100644 --- a/config/environments.rb +++ b/config/environments.rb @@ -4,7 +4,6 @@ require 'roda' require 'figaro' require 'logger' -require 'rack/ssl-enforcer' require 'rack/session' require 'rack/session/redis' require_relative '../require_app' @@ -18,28 +17,25 @@ class App < Roda # Environment variables setup Figaro.application = Figaro::Application.new( - environment:, + environment: environment, path: File.expand_path('config/secrets.yml') ) Figaro.load - def self.config = Figaro.env + def self.config() = Figaro.env # Logger setup LOGGER = Logger.new($stderr) - def self.logger = LOGGER + def self.logger() = LOGGER ONE_MONTH = 30 * 24 * 60 * 60 configure do + SecureSession.setup(ENV['REDIS_TLS_URL']) # REDIS_TLS_URL used again below SecureMessage.setup(ENV.delete('MSG_KEY')) SignedMessage.setup(config) end configure :production do - SecureSession.setup(ENV.fetch('REDIS_TLS_URL')) # REDIS_TLS_URL used again below - - use Rack::SslEnforcer, hsts: true - use Rack::Session::Redis, expire_after: ONE_MONTH, httponly: true, @@ -51,16 +47,17 @@ def self.logger = LOGGER end configure :development, :test do - require 'pry' - - # NOTE: env var REDIS_URL only used to wipe the session store (ok to be nil) - SecureSession.setup(ENV.fetch('REDIS_URL', nil)) # REDIS_URL used again below - # use Rack::Session::Cookie, - # expire_after: ONE_MONTH, secret: config.SESSION_SECRET + # expire_after: ONE_MONTH, + # secret: config.SESSION_SECRET, + # httponly: true, + # same_site: :strict + use Rack::Session::Pool, - expire_after: ONE_MONTH + expire_after: ONE_MONTH, + httponly: true, + same_site: :strict # use Rack::Session::Redis, # expire_after: ONE_MONTH, @@ -75,7 +72,9 @@ def self.logger = LOGGER require 'pry' # Allows running reload! in pry to restart entire app - def self.reload! = exec 'pry -r ./spec/test_load_all' + def self.reload! + exec 'pry -r ./spec/test_load_all' + end end end end From a14865ee8460f7c2f39de24f297b4219e7a5719e Mon Sep 17 00:00:00 2001 From: Soumya Ray Date: Thu, 15 Jun 2023 21:40:45 +0800 Subject: [PATCH 3/3] Move cookie header protections to environments.rb --- app/controllers/security.rb | 15 ++++++++------- config/environments.rb | 12 ++++++------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/controllers/security.rb b/app/controllers/security.rb index f86d72d..eec079d 100644 --- a/app/controllers/security.rb +++ b/app/controllers/security.rb @@ -25,13 +25,14 @@ class App < Roda use SecureHeaders::Middleware SecureHeaders::Configuration.default do |config| - config.cookies = { - secure: true, - httponly: true, - samesite: { - strict: true - } - } + ## Cookie security headers in config/environments.rb + # config.cookies = { + # secure: true, + # httponly: true, + # samesite: { + # lax: true + # } + # } config.x_frame_options = 'DENY' config.x_content_type_options = 'nosniff' diff --git a/config/environments.rb b/config/environments.rb index 75af65c..398b918 100644 --- a/config/environments.rb +++ b/config/environments.rb @@ -38,8 +38,9 @@ def self.logger() = LOGGER configure :production do use Rack::Session::Redis, expire_after: ONE_MONTH, + secure: true, # only possible on https:// requests httponly: true, - same_site: :strict, + same_site: :lax, redis_server: { url: ENV.delete('REDIS_TLS_URL'), ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE } @@ -48,21 +49,20 @@ def self.logger() = LOGGER configure :development, :test do # use Rack::Session::Cookie, - # expire_after: ONE_MONTH, # secret: config.SESSION_SECRET, + # expire_after: ONE_MONTH, # httponly: true, - # same_site: :strict - + # same_site: :lax use Rack::Session::Pool, expire_after: ONE_MONTH, httponly: true, - same_site: :strict + same_site: :lax # use Rack::Session::Redis, # expire_after: ONE_MONTH, # httponly: true, - # same_site: :strict, + # same_site: :lax, # redis_server: { # url: ENV.delete('REDIS_URL') # }