discourse/spec/lib/content_security_policy_spec.rb
Kelv b751742573
FIX: invalid CSP directive sources should allow site to boot with valid CSP directives (#31256)
[Security
patch](5558e72f22)
(for this [CVE](https://nvd.nist.gov/vuln/detail/CVE-2024-54133)) from
rails actionpack was backported from [Rails
8.0.0.1](https://github.com/rails/rails/blob/v8.0.1/actionpack/CHANGELOG.md#rails-8001-december-10-2024)
to previous stable versions including `7-1-stable` / `7-2-stable`.

Any previous version of Discourse upgrading to v3.4.0.beta3 and above
would have observed their sites crashing if they had invalid sources in
their CSP directive extensions.

This fix removes such invalid sources during our build of the CSP, and
logs these at a warning level so devs are able to find out why their CSP
sources were filtered out of the extendable directives.
2025-02-10 20:38:36 +08:00

147 lines
4.2 KiB
Ruby

# frozen_string_literal: true
RSpec.describe ContentSecurityPolicy do
after { DiscoursePluginRegistry.reset! }
describe "report-uri" do
it "is enabled by SiteSetting" do
SiteSetting.content_security_policy_collect_reports = true
report_uri = parse(policy)["report-uri"].first
expect(report_uri).to eq("http://test.localhost/csp_reports")
SiteSetting.content_security_policy_collect_reports = false
report_uri = parse(policy)["report-uri"]
expect(report_uri).to eq(nil)
end
end
describe "base-uri" do
it "is set to self" do
base_uri = parse(policy)["base-uri"]
expect(base_uri).to eq(["'self'"])
end
end
describe "object-src" do
it "is set to none" do
object_srcs = parse(policy)["object-src"]
expect(object_srcs).to eq(["'none'"])
end
end
describe "upgrade-insecure-requests" do
it "is not included when force_https is off" do
SiteSetting.force_https = false
expect(parse(policy)["upgrade-insecure-requests"]).to eq(nil)
end
it "is included when force_https is on" do
SiteSetting.force_https = true
expect(parse(policy)["upgrade-insecure-requests"]).to eq([])
end
end
describe "strict-dynamic script-src and worker-src" do
it "includes strict-dynamic keyword" do
script_srcs = parse(policy)["script-src"]
expect(script_srcs).to include("'strict-dynamic'")
end
it "does not set worker-src" do
worker_src = parse(policy)["worker-src"]
expect(worker_src).to eq(nil)
end
it 'includes "report-sample" when report collection is enabled' do
SiteSetting.content_security_policy_collect_reports = true
script_srcs = parse(policy)["script-src"]
expect(script_srcs).to include("'report-sample'")
end
end
describe "manifest-src" do
it "is set to self" do
expect(parse(policy)["manifest-src"]).to eq(["'self'"])
end
end
describe "frame-ancestors" do
context "with content_security_policy_frame_ancestors enabled" do
before do
SiteSetting.content_security_policy_frame_ancestors = true
Fabricate(:embeddable_host, host: "https://a.org")
Fabricate(:embeddable_host, host: "https://b.org")
end
it "always has self" do
frame_ancestors = parse(policy)["frame-ancestors"]
expect(frame_ancestors).to include("'self'")
end
it "includes all EmbeddableHost" do
frame_ancestors = parse(policy)["frame-ancestors"]
expect(frame_ancestors).to include("https://a.org")
expect(frame_ancestors).to include("https://b.org")
end
end
context "with content_security_policy_frame_ancestors disabled" do
before { SiteSetting.content_security_policy_frame_ancestors = false }
it "does not set frame-ancestors" do
frame_ancestors = parse(policy)["frame-ancestors"]
expect(frame_ancestors).to be_nil
end
end
end
context "with a plugin" do
let(:plugin_class) do
Class.new(Plugin::Instance) do
attr_accessor :enabled
def enabled?
@enabled
end
end
end
it "can extend frame_ancestors" do
SiteSetting.content_security_policy_frame_ancestors = true
plugin = plugin_class.new(nil, "#{Rails.root}/spec/fixtures/plugins/csp_extension/plugin.rb")
plugin.activate!
Discourse.plugins << plugin
plugin.enabled = true
expect(parse(policy)["frame-ancestors"]).to include("'self'")
expect(parse(policy)["frame-ancestors"]).to include("https://frame-ancestors-plugin.ext")
plugin.enabled = false
expect(parse(policy)["frame-ancestors"]).to_not include("https://frame-ancestors-plugin.ext")
Discourse.plugins.delete plugin
DiscoursePluginRegistry.reset!
end
end
it "can be extended by site setting" do
SiteSetting.content_security_policy_script_src = "'unsafe-eval'"
expect(parse(policy)["script-src"]).to include("'unsafe-eval'")
end
def parse(csp_string)
csp_string
.split(";")
.map do |policy|
directive, *sources = policy.split
[directive, sources]
end
.to_h
end
def policy(theme_id = nil, path_info: "/")
ContentSecurityPolicy.policy(theme_id, path_info: path_info)
end
end