DEV: add validation on content_security_policy_script_src site setting (#27564)

* DEV: add validation on content_security_policy_script_src site setting
This commit is contained in:
Kelv 2024-06-21 17:00:22 +08:00 committed by GitHub
parent ca4af53be8
commit 60d5170587
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 75 additions and 11 deletions

View File

@ -1844,7 +1844,7 @@ en:
content_security_policy_report_only: "Enable Content-Security-Policy-Report-Only (CSP)"
content_security_policy_collect_reports: "Enable CSP violation report collection at /csp_reports"
content_security_policy_frame_ancestors: "Restrict who can embed this site in iframes via CSP. Control allowed hosts on <a href='%{base_path}/admin/customize/embedding'>Embedding</a>"
content_security_policy_script_src: "Additional allowlisted script sources. The current host and CDN are included by default. See <a href='https://meta.discourse.org/t/mitigate-xss-attacks-with-content-security-policy/104243' target='_blank'>Mitigate XSS Attacks with Content Security Policy.</a> (CSP). Host sources will be ignored when content_security_policy_strict_dynamic is enabled."
content_security_policy_script_src: "Additional allowlisted script sources. The current host and CDN are included by default. See <a href='https://meta.discourse.org/t/mitigate-xss-attacks-with-content-security-policy/104243' target='_blank'>Mitigate XSS Attacks with Content Security Policy.</a> (CSP). Other host sources are ignored as strict-dynamic is enabled."
invalidate_inactive_admin_email_after_days: "Admin accounts that have not visited the site in this number of days will need to re-validate their email address before logging in. Set to 0 to disable."
include_secure_categories_in_tag_counts: "When enabled, count of topics for a tag will include topics that are in read restricted categories for all users. When disabled, normal users are only shown a count of topics for a tag where all the topics are in public categories."
display_personal_messages_tag_counts: "When enabled, count of personal messages tagged with a given tag will be displayed."
@ -2689,6 +2689,7 @@ en:
invalid_reply_by_email_address: "Value must contain '%{reply_key}' and be different from the notification email."
invalid_alternative_reply_by_email_addresses: "All values must contain '%{reply_key}' and be different from the notification email."
invalid_domain_hostname: "Must not include * or ? characters."
invalid_csp_script_src: "Value must be either 'unsafe-eval' or 'wasm-unsafe-eval', or in the form '<hash algorithm>-<base64 value>' where supported hash algorithms are sha256, sha384 or sha512. Ensure that your input is wrapped in single quotation marks."
pop3_polling_host_is_empty: "You must set a 'pop3 polling host' before enabling POP3 polling."
pop3_polling_username_is_empty: "You must set a 'pop3 polling username' before enabling POP3 polling."
pop3_polling_password_is_empty: "You must set a 'pop3 polling password' before enabling POP3 polling."

View File

@ -2025,6 +2025,7 @@ security:
content_security_policy_script_src:
type: simple_list
default: ""
validator: "CspScriptSrcValidator"
invalidate_inactive_admin_email_after_days:
default: 365
min: 0

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
class CspScriptSrcValidator
VALID_SOURCE_REGEX =
/
(?:\A'unsafe-eval'\z)|
(?:\A'wasm-unsafe-eval'\z)|
(?:\A'sha(?:256|384|512)-[A-Za-z0-9+\/\-_]+={0,2}'\z)
/x
def initialize(opts = {})
@opts = opts
end
def valid_value?(values)
values.split("|").all? { _1.match? VALID_SOURCE_REGEX }
end
def error_message
I18n.t("site_settings.errors.invalid_csp_script_src")
end
end

View File

@ -129,16 +129,6 @@ RSpec.describe ContentSecurityPolicy do
expect(parse(policy)["script-src"]).to include("'unsafe-eval'")
end
it "strips unsupported values from setting" do
SiteSetting.content_security_policy_script_src =
"'unsafe-eval'|blob:|https://example.com/script.js"
script_src = parse(policy)["script-src"]
expect(script_src).to include("'unsafe-eval'")
expect(script_src).not_to include("blob:")
expect(script_src).not_to include("https://example.com/script.js")
end
def parse(csp_string)
csp_string
.split(";")

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
RSpec.describe CspScriptSrcValidator do
describe "#valid_value?" do
context "when values are valid" do
context "when value is an empty string" do
it { is_expected.to be_a_valid_value "" }
end
context "when there's a single value" do
%w[
'unsafe-eval'
'wasm-unsafe-eval'
'sha256-valid_h4sH'
'sha384-valid-h4sH='
'sha512-valid+h4sH=='
].each { |valid_value| it { is_expected.to be_a_valid_value valid_value } }
end
context "when there are multiple valid values" do
let(:valid_values) do
%w[
'unsafe-eval'
'wasm-unsafe-eval'
'sha384-oqVuAfXRKap7fdgcCY5-ykM6+R9GqQ8K/uxy9rx_HNQlGYl1kPzQho1wx4JwY8wC'
].join("|")
end
it { is_expected.to be_a_valid_value valid_values }
end
end
context "when values are invalid" do
context "when there's a single value" do
%w[
unsafe-eval
'unsafe-eval'!
!'unsafe-eval'
'sha256-not+a+valid+base64===='
'md5-not+a+supported+hash+algo'
'sha224-not+a+supported+hash+algo'
].each { |invalid_value| it { is_expected.not_to be_a_valid_value invalid_value } }
end
context "when there is at least 1 invalid value and 1 valid value" do
it { is_expected.not_to be_a_valid_value "'unsafe-eval'|'md5-not+a+supported+hash+algo'" }
end
end
end
end