diff --git a/app/controllers/accounts/logos_controller.rb b/app/controllers/accounts/logos_controller.rb index dbd06a8..50e69df 100644 --- a/app/controllers/accounts/logos_controller.rb +++ b/app/controllers/accounts/logos_controller.rb @@ -2,6 +2,7 @@ class Accounts::LogosController < ApplicationController include ActiveStorage::Streaming, ActionView::Helpers::AssetUrlHelper allow_unauthenticated_access only: :show + content_security_policy false, only: :show before_action :ensure_can_administer, only: :destroy def show diff --git a/app/controllers/qr_code_controller.rb b/app/controllers/qr_code_controller.rb index bea6dac..a95bed8 100644 --- a/app/controllers/qr_code_controller.rb +++ b/app/controllers/qr_code_controller.rb @@ -1,5 +1,6 @@ class QrCodeController < ApplicationController allow_unauthenticated_access + content_security_policy false def show url = Base64.urlsafe_decode64(params[:id]) diff --git a/app/controllers/users/avatars_controller.rb b/app/controllers/users/avatars_controller.rb index 79f7199..bb91f90 100644 --- a/app/controllers/users/avatars_controller.rb +++ b/app/controllers/users/avatars_controller.rb @@ -1,6 +1,8 @@ class Users::AvatarsController < ApplicationController include ActiveStorage::Streaming + content_security_policy false, only: :show + rescue_from(ActiveSupport::MessageVerifier::InvalidSignature) { head :not_found } def show diff --git a/config/initializers/active_storage.rb b/config/initializers/active_storage.rb index f2d77e1..bb69fb0 100644 --- a/config/initializers/active_storage.rb +++ b/config/initializers/active_storage.rb @@ -1,4 +1,6 @@ ActiveSupport.on_load(:active_storage_blob) do + ActiveStorage::DiskController.content_security_policy false, only: :show + ActiveStorage::DiskController.after_action only: :show do response.set_header("Cache-Control", "max-age=3600, public") end diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index 81a4aca..3bd6824 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -20,6 +20,12 @@ # Campfire sets no per-session verification cookie, so the lightweight # nonce_id cookie — set on first visit, present for every session including # unauthenticated ones — is the sole identifier. +# +# The generator runs for every response that carries the policy, so the cookie +# write would otherwise land on public, shared-cacheable responses (QR codes, +# logos, avatars, disk-served blobs) where a cache could replay one client's +# identifier and nonce to others. Those endpoints opt out of the policy with +# `content_security_policy false`; the nonce is only meaningful on documents. module CSP module Nonce COOKIE = "campfire_csp_nonce_id" diff --git a/test/integration/csp_nonce_test.rb b/test/integration/csp_nonce_test.rb index 5bfd3b1..df76cf4 100644 --- a/test/integration/csp_nonce_test.rb +++ b/test/integration/csp_nonce_test.rb @@ -81,6 +81,29 @@ class CspNonceTest < ActionDispatch::IntegrationTest end end + test "public cacheable responses carry neither the policy nor the nonce cookie" do + sign_in :david + blob = ActiveStorage::Blob.create_and_upload!(io: file_fixture("moon.jpg").open, filename: "moon.jpg", content_type: "image/jpeg") + get rails_blob_path(blob) + disk_blob_url = response.location + + [ + qr_code_path(Base64.urlsafe_encode64("http://example.com")), + account_logo_path, + user_avatar_path(users(:kevin).avatar_token), + disk_blob_url + ].each do |path| + cookies.delete CSP::Nonce::COOKIE + + get path + + assert_response :success + assert response.cache_control[:public], "#{path} is expected to be publicly cacheable" + assert_nil response.headers["Content-Security-Policy-Report-Only"], "#{path} must not carry the policy" + assert_nil cookies[CSP::Nonce::COOKIE], "#{path} must not set the nonce cookie" + end + end + test "directives default to :self only when no ENV extras are set" do with_env "CSP_EXTRA_FRAME_SRC" => nil, "CSP_EXTRA_IMG_SRC" => nil do header = ActionDispatch::ContentSecurityPolicy.new { |p| CSP.apply(p) }.build