# frozen_string_literal: true
require "abstract_unit"
class ContentSecurityPolicyTest < ActiveSupport::TestCase
def setup
@policy = ActionDispatch::ContentSecurityPolicy.new
end
def test_build
assert_equal "", @policy.build
@policy.script_src :self
assert_equal "script-src 'self'", @policy.build
end
def test_dup
@policy.img_src :self
@policy.block_all_mixed_content
@policy.upgrade_insecure_requests
@policy.sandbox
copied = @policy.dup
assert_equal copied.build, @policy.build
end
def test_mappings
@policy.script_src :data
assert_equal "script-src data:", @policy.build
@policy.script_src :mediastream
assert_equal "script-src mediastream:", @policy.build
@policy.script_src :blob
assert_equal "script-src blob:", @policy.build
@policy.script_src :filesystem
assert_equal "script-src filesystem:", @policy.build
@policy.script_src :self
assert_equal "script-src 'self'", @policy.build
@policy.script_src :unsafe_inline
assert_equal "script-src 'unsafe-inline'", @policy.build
@policy.script_src :unsafe_eval
assert_equal "script-src 'unsafe-eval'", @policy.build
@policy.script_src :none
assert_equal "script-src 'none'", @policy.build
@policy.script_src :strict_dynamic
assert_equal "script-src 'strict-dynamic'", @policy.build
@policy.script_src :ws
assert_equal "script-src ws:", @policy.build
@policy.script_src :wss
assert_equal "script-src wss:", @policy.build
@policy.script_src :none, :report_sample
assert_equal "script-src 'none' 'report-sample'", @policy.build
end
def test_fetch_directives
@policy.child_src :self
assert_match %r{child-src 'self'}, @policy.build
@policy.child_src false
assert_no_match %r{child-src}, @policy.build
@policy.connect_src :self
assert_match %r{connect-src 'self'}, @policy.build
@policy.connect_src false
assert_no_match %r{connect-src}, @policy.build
@policy.default_src :self
assert_match %r{default-src 'self'}, @policy.build
@policy.default_src false
assert_no_match %r{default-src}, @policy.build
@policy.font_src :self
assert_match %r{font-src 'self'}, @policy.build
@policy.font_src false
assert_no_match %r{font-src}, @policy.build
@policy.frame_src :self
assert_match %r{frame-src 'self'}, @policy.build
@policy.frame_src false
assert_no_match %r{frame-src}, @policy.build
@policy.img_src :self
assert_match %r{img-src 'self'}, @policy.build
@policy.img_src false
assert_no_match %r{img-src}, @policy.build
@policy.manifest_src :self
assert_match %r{manifest-src 'self'}, @policy.build
@policy.manifest_src false
assert_no_match %r{manifest-src}, @policy.build
@policy.media_src :self
assert_match %r{media-src 'self'}, @policy.build
@policy.media_src false
assert_no_match %r{media-src}, @policy.build
@policy.object_src :self
assert_match %r{object-src 'self'}, @policy.build
@policy.object_src false
assert_no_match %r{object-src}, @policy.build
@policy.prefetch_src :self
assert_match %r{prefetch-src 'self'}, @policy.build
@policy.prefetch_src false
assert_no_match %r{prefetch-src}, @policy.build
@policy.script_src :self
assert_match %r{script-src 'self'}, @policy.build
@policy.script_src false
assert_no_match %r{script-src}, @policy.build
@policy.style_src :self
assert_match %r{style-src 'self'}, @policy.build
@policy.style_src false
assert_no_match %r{style-src}, @policy.build
@policy.worker_src :self
assert_match %r{worker-src 'self'}, @policy.build
@policy.worker_src false
assert_no_match %r{worker-src}, @policy.build
end
def test_document_directives
@policy.base_uri "https://example.com"
assert_match %r{base-uri https://example\.com}, @policy.build
@policy.plugin_types "application/x-shockwave-flash"
assert_match %r{plugin-types application/x-shockwave-flash}, @policy.build
@policy.sandbox
assert_match %r{sandbox}, @policy.build
@policy.sandbox "allow-scripts", "allow-modals"
assert_match %r{sandbox allow-scripts allow-modals}, @policy.build
@policy.sandbox false
assert_no_match %r{sandbox}, @policy.build
end
def test_navigation_directives
@policy.form_action :self
assert_match %r{form-action 'self'}, @policy.build
@policy.frame_ancestors :self
assert_match %r{frame-ancestors 'self'}, @policy.build
end
def test_reporting_directives
@policy.report_uri "/violations"
assert_match %r{report-uri /violations}, @policy.build
end
def test_other_directives
@policy.block_all_mixed_content
assert_match %r{block-all-mixed-content}, @policy.build
@policy.block_all_mixed_content false
assert_no_match %r{block-all-mixed-content}, @policy.build
@policy.require_sri_for :script, :style
assert_match %r{require-sri-for script style}, @policy.build
@policy.require_sri_for "script", "style"
assert_match %r{require-sri-for script style}, @policy.build
@policy.require_sri_for
assert_no_match %r{require-sri-for}, @policy.build
@policy.upgrade_insecure_requests
assert_match %r{upgrade-insecure-requests}, @policy.build
@policy.upgrade_insecure_requests false
assert_no_match %r{upgrade-insecure-requests}, @policy.build
end
def test_multiple_sources
@policy.script_src :self, :https
assert_equal "script-src 'self' https:", @policy.build
end
def test_multiple_directives
@policy.script_src :self, :https
@policy.style_src :self, :https
assert_equal "script-src 'self' https:; style-src 'self' https:", @policy.build
end
def test_dynamic_directives
request = ActionDispatch::Request.new("HTTP_HOST" => "www.example.com")
controller = Struct.new(:request).new(request)
@policy.script_src -> { request.host }
assert_equal "script-src www.example.com", @policy.build(controller)
end
def test_mixed_static_and_dynamic_directives
@policy.script_src :self, -> { "foo.com" }, "bar.com"
request = ActionDispatch::Request.new({})
controller = Struct.new(:request).new(request)
assert_equal "script-src 'self' foo.com bar.com", @policy.build(controller)
end
def test_invalid_directive_source
exception = assert_raises(ArgumentError) do
@policy.script_src [:self]
end
assert_equal "Invalid content security policy source: [:self]", exception.message
end
def test_missing_context_for_dynamic_source
@policy.script_src -> { request.host }
exception = assert_raises(RuntimeError) do
@policy.build
end
assert_match %r{\AMissing context for the dynamic content security policy source:}, exception.message
end
def test_raises_runtime_error_when_unexpected_source
@policy.plugin_types [:flash]
exception = assert_raises(RuntimeError) do
@policy.build
end
assert_match %r{\AUnexpected content security policy source:}, exception.message
end
end
class DefaultContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
class PolicyController < ActionController::Base
def index
head :ok
end
end
ROUTES = ActionDispatch::Routing::RouteSet.new
ROUTES.draw do
scope module: "default_content_security_policy_integration_test" do
get "/", to: "policy#index"
get "/redirect", to: redirect("/")
end
end
POLICY = ActionDispatch::ContentSecurityPolicy.new do |p|
p.default_src -> { :self }
p.script_src -> { :https }
end
class PolicyConfigMiddleware
def initialize(app)
@app = app
end
def call(env)
env["action_dispatch.content_security_policy"] = POLICY
env["action_dispatch.content_security_policy_nonce_generator"] = proc { "iyhD0Yc0W+c=" }
env["action_dispatch.content_security_policy_report_only"] = false
env["action_dispatch.show_exceptions"] = false
@app.call(env)
end
end
APP = build_app(ROUTES) do |middleware|
middleware.use PolicyConfigMiddleware
middleware.use ActionDispatch::ContentSecurityPolicy::Middleware
end
def app
APP
end
def test_adds_nonce_to_script_src_content_security_policy_only_once
get "/"
get "/"
assert_response :success
assert_policy "default-src 'self'; script-src https: 'nonce-iyhD0Yc0W+c='"
end
def test_redirect_works_with_dynamic_sources
get "/redirect"
assert_response :redirect
assert_policy "default-src 'self'; script-src https: 'nonce-iyhD0Yc0W+c='"
end
private
def assert_policy(expected, report_only: false)
if report_only
expected_header = "Content-Security-Policy-Report-Only"
unexpected_header = "Content-Security-Policy"
else
expected_header = "Content-Security-Policy"
unexpected_header = "Content-Security-Policy-Report-Only"
end
assert_nil response.headers[unexpected_header]
assert_equal expected, response.headers[expected_header]
end
end
class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
class PolicyController < ActionController::Base
content_security_policy only: :inline do |p|
p.default_src "https://example.com"
end
content_security_policy only: :conditional, if: :condition? do |p|
p.default_src "https://true.example.com"
end
content_security_policy only: :conditional, unless: :condition? do |p|
p.default_src "https://false.example.com"
end
content_security_policy only: :report_only do |p|
p.report_uri "/violations"
end
content_security_policy only: :script_src do |p|
p.default_src false
p.script_src :self
end
content_security_policy only: :style_src do |p|
p.default_src false
p.style_src :self
end
content_security_policy(false, only: :no_policy)
content_security_policy_report_only only: :report_only
def index
head :ok
end
def inline
head :ok
end
def conditional
head :ok
end
def report_only
head :ok
end
def script_src
head :ok
end
def style_src
head :ok
end
def no_policy
head :ok
end
private
def condition?
params[:condition] == "true"
end
end
ROUTES = ActionDispatch::Routing::RouteSet.new
ROUTES.draw do
scope module: "content_security_policy_integration_test" do
get "/", to: "policy#index"
get "/inline", to: "policy#inline"
get "/conditional", to: "policy#conditional"
get "/report-only", to: "policy#report_only"
get "/script-src", to: "policy#script_src"
get "/style-src", to: "policy#style_src"
get "/no-policy", to: "policy#no_policy"
end
end
POLICY = ActionDispatch::ContentSecurityPolicy.new do |p|
p.default_src :self
end
class PolicyConfigMiddleware
def initialize(app)
@app = app
end
def call(env)
env["action_dispatch.content_security_policy"] = POLICY
env["action_dispatch.content_security_policy_nonce_generator"] = proc { "iyhD0Yc0W+c=" }
env["action_dispatch.content_security_policy_report_only"] = false
env["action_dispatch.show_exceptions"] = false
@app.call(env)
end
end
APP = build_app(ROUTES) do |middleware|
middleware.use PolicyConfigMiddleware
middleware.use ActionDispatch::ContentSecurityPolicy::Middleware
end
def app
APP
end
def test_generates_content_security_policy_header
get "/"
assert_policy "default-src 'self'"
end
def test_generates_inline_content_security_policy
get "/inline"
assert_policy "default-src https://example.com"
end
def test_generates_conditional_content_security_policy
get "/conditional", params: { condition: "true" }
assert_policy "default-src https://true.example.com"
get "/conditional", params: { condition: "false" }
assert_policy "default-src https://false.example.com"
end
def test_generates_report_only_content_security_policy
get "/report-only"
assert_policy "default-src 'self'; report-uri /violations", report_only: true
end
def test_adds_nonce_to_script_src_content_security_policy
get "/script-src"
assert_policy "script-src 'self' 'nonce-iyhD0Yc0W+c='"
end
def test_adds_nonce_to_style_src_content_security_policy
get "/style-src"
assert_policy "style-src 'self' 'nonce-iyhD0Yc0W+c='"
end
def test_generates_no_content_security_policy
get "/no-policy"
assert_nil response.headers["Content-Security-Policy"]
assert_nil response.headers["Content-Security-Policy-Report-Only"]
end
private
def assert_policy(expected, report_only: false)
assert_response :success
if report_only
expected_header = "Content-Security-Policy-Report-Only"
unexpected_header = "Content-Security-Policy"
else
expected_header = "Content-Security-Policy"
unexpected_header = "Content-Security-Policy-Report-Only"
end
assert_nil response.headers[unexpected_header]
assert_equal expected, response.headers[expected_header]
end
end
class DisabledContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
class PolicyController < ActionController::Base
content_security_policy only: :inline do |p|
p.default_src "https://example.com"
end
def index
head :ok
end
def inline
head :ok
end
end
ROUTES = ActionDispatch::Routing::RouteSet.new
ROUTES.draw do
scope module: "disabled_content_security_policy_integration_test" do
get "/", to: "policy#index"
get "/inline", to: "policy#inline"
end
end
class PolicyConfigMiddleware
def initialize(app)
@app = app
end
def call(env)
env["action_dispatch.content_security_policy"] = nil
env["action_dispatch.content_security_policy_nonce_generator"] = nil
env["action_dispatch.content_security_policy_report_only"] = false
env["action_dispatch.show_exceptions"] = false
@app.call(env)
end
end
APP = build_app(ROUTES) do |middleware|
middleware.use PolicyConfigMiddleware
middleware.use ActionDispatch::ContentSecurityPolicy::Middleware
end
def app
APP
end
def test_generates_no_content_security_policy_by_default
get "/"
assert_nil response.headers["Content-Security-Policy"]
end
def test_generates_content_security_policy_header_when_globally_disabled
get "/inline"
assert_equal "default-src https://example.com", response.headers["Content-Security-Policy"]
end
end