FEATURE: Treat site settings as plain text and add a new HTML type. (#12618)

To add an extra layer of security, we sanitize settings before shipping them to the client. We don't sanitize those that have the "html" type.

The CookedPostProcessor already uses Loofah for sanitization, so I chose to also use it for this. I added it to our gemfile since we installed it as a transitive dependency.
This commit is contained in:
Roman Rizzi 2021-04-07 12:51:19 -03:00 committed by GitHub
parent 11e611f845
commit 5e4c0e2caa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 74 additions and 8 deletions

View File

@ -96,6 +96,7 @@ gem 'discourse_image_optim', require: 'image_optim'
gem 'multi_json' gem 'multi_json'
gem 'mustache' gem 'mustache'
gem 'nokogiri' gem 'nokogiri'
gem 'loofah'
gem 'css_parser', require: false gem 'css_parser', require: false
gem 'omniauth' gem 'omniauth'

View File

@ -525,6 +525,7 @@ DEPENDENCIES
logstash-event logstash-event
logstash-logger logstash-logger
logster logster
loofah
lru_redux lru_redux
lz4-ruby lz4-ruby
mail! mail!

View File

@ -0,0 +1,2 @@
{{text-field value=(html-safe value) classNames="input-setting-string"}}
<div class="desc">{{html-safe setting.description}}</div>

View File

@ -6,7 +6,10 @@ class Admin::SiteSettingsController < Admin::AdminController
end end
def index def index
render_json_dump(site_settings: SiteSetting.all_settings, diags: SiteSetting.diags) render_json_dump(
site_settings: SiteSetting.all_settings(sanitize_plain_text_settings: true),
diags: SiteSetting.diags
)
end end
def update def update

View File

@ -2,7 +2,7 @@
class SiteSettingsTask class SiteSettingsTask
def self.export_to_hash(include_defaults: false, include_hidden: false) def self.export_to_hash(include_defaults: false, include_hidden: false)
site_settings = SiteSetting.all_settings(include_hidden) site_settings = SiteSetting.all_settings(include_hidden: include_hidden)
h = {} h = {}
site_settings.each do |site_setting| site_settings.each do |site_setting|
next if site_setting[:default] == site_setting[:value] if !include_defaults next if site_setting[:default] == site_setting[:value] if !include_defaults

View File

@ -148,7 +148,7 @@ class UserMerger
def update_site_settings def update_site_settings
::MessageBus.publish '/merge_user', { message: I18n.t("admin.user.merge_user.updating_site_settings") }, user_ids: [@acting_user.id] if @acting_user ::MessageBus.publish '/merge_user', { message: I18n.t("admin.user.merge_user.updating_site_settings") }, user_ids: [@acting_user.id] if @acting_user
SiteSetting.all_settings(true).each do |setting| SiteSetting.all_settings(include_hidden: true).each do |setting|
if setting[:type] == "username" && setting[:value] == @source_user.username if setting[:type] == "username" && setting[:value] == @source_user.username
SiteSetting.set_and_log(setting[:setting], @target_user.username) SiteSetting.set_and_log(setting[:setting], @target_user.username)
end end

View File

@ -19,6 +19,7 @@
# type: username - Must match the username of an existing user. # type: username - Must match the username of an existing user.
# type: list - A list of values, chosen from a set of valid values defined in the choices option. # type: list - A list of values, chosen from a set of valid values defined in the choices option.
# type: enum - A single value, chosen from a set of valid values in the choices option. # type: enum - A single value, chosen from a set of valid values in the choices option.
# type: html - A single value. It could contain HTML.
# #
# A type:list setting with the word 'colors' in its name will make color values have a bold line of the corresponding color # A type:list setting with the word 'colors' in its name will make color values have a bold line of the corresponding color
# #

View File

@ -208,14 +208,20 @@ module SiteSettingExtension
def client_settings_json_uncached def client_settings_json_uncached
MultiJson.dump(Hash[*@client_settings.map do |name| MultiJson.dump(Hash[*@client_settings.map do |name|
value = self.public_send(name) value = self.public_send(name)
value = value.to_s if type_supervisor.get_type(name) == :upload type = type_supervisor.get_type(name)
value = value.map(&:to_s).join("|") if type_supervisor.get_type(name) == :uploaded_image_list value = value.to_s if type == :upload
value = value.map(&:to_s).join("|") if type == :uploaded_image_list
if should_sanitize?(value, type)
value = sanitize(value)
end
[name, value] [name, value]
end.flatten]) end.flatten])
end end
# Retrieve all settings # Retrieve all settings
def all_settings(include_hidden = false) def all_settings(include_hidden: false, sanitize_plain_text_settings: false)
locale_setting_hash = locale_setting_hash =
{ {
@ -244,6 +250,8 @@ module SiteSettingExtension
default.to_i < Upload::SEEDED_ID_THRESHOLD default.to_i < Upload::SEEDED_ID_THRESHOLD
default = default_uploads[default.to_i] default = default_uploads[default.to_i]
elsif sanitize_plain_text_settings && should_sanitize?(value, type_hash[:type].to_s)
value = sanitize(value)
end end
opts = { opts = {
@ -574,6 +582,14 @@ module SiteSettingExtension
end end
end end
def should_sanitize?(value, type)
value.is_a?(String) && type.to_s != 'html'
end
def sanitize(value)
CGI.unescapeHTML(Loofah.scrub_fragment(value, :strip).to_s)
end
def logger def logger
Rails.logger Rails.logger
end end

View File

@ -36,7 +36,8 @@ class SiteSettings::TypeSupervisor
tag_list: 21, tag_list: 21,
color: 22, color: 22,
simple_list: 23, simple_list: 23,
emoji_list: 24 emoji_list: 24,
html: 25
) )
end end

View File

@ -631,7 +631,7 @@ describe SiteSettingExtension do
end end
it "is present in all_settings when we ask for hidden" do it "is present in all_settings when we ask for hidden" do
expect(settings.all_settings(true).find { |s| s[:setting] == :superman_identity }).to be_present expect(settings.all_settings(include_hidden: true).find { |s| s[:setting] == :superman_identity }).to be_present
end end
end end
@ -842,6 +842,23 @@ describe SiteSettingExtension do
expect(setting[:default]).to eq(system_upload.url) expect(setting[:default]).to eq(system_upload.url)
end end
end end
it 'should sanitize html in the site settings' do
settings.setting(:with_html, '<script></script>rest')
settings.refresh!
setting = settings.all_settings(sanitize_plain_text_settings: true).last
expect(setting[:value]).to eq('rest')
end
it 'settings with html type are not sanitized' do
settings.setting(:with_html, '<script></script>rest', type: :html)
setting = settings.all_settings(sanitize_plain_text_settings: true).last
expect(setting[:value]).to eq('<script></script>rest')
end
end end
describe '.client_settings_json_uncached' do describe '.client_settings_json_uncached' do
@ -855,6 +872,27 @@ describe SiteSettingExtension do
%Q|{"default_locale":"#{SiteSetting.default_locale}","upload_type":"#{upload.url}","string_type":"haha"}| %Q|{"default_locale":"#{SiteSetting.default_locale}","upload_type":"#{upload.url}","string_type":"haha"}|
) )
end end
it 'should sanitize html in the site settings' do
settings.setting(:with_html, '<script></script>rest', client: true)
settings.setting(:with_symbols, '<>rest', client: true)
settings.setting(:with_unknown_tag, '<rest>rest', client: true)
settings.refresh!
client_settings = JSON.parse settings.client_settings_json_uncached
expect(client_settings['with_html']).to eq('rest')
expect(client_settings['with_symbols']).to eq('<>rest')
expect(client_settings['with_unknown_tag']).to eq('rest')
end
it 'settings with html type are not sanitized' do
settings.setting(:with_html, '<script></script>rest', type: :html, client: true)
client_settings = JSON.parse settings.client_settings_json_uncached
expect(client_settings['with_html']).to eq('<script></script>rest')
end
end end
describe '.setup_methods' do describe '.setup_methods' do

View File

@ -94,6 +94,9 @@ describe SiteSettings::TypeSupervisor do
it "'emoji_list' should be at the right position" do it "'emoji_list' should be at the right position" do
expect(SiteSettings::TypeSupervisor.types[:emoji_list]).to eq(24) expect(SiteSettings::TypeSupervisor.types[:emoji_list]).to eq(24)
end end
it "'html' should be at the right position" do
expect(SiteSettings::TypeSupervisor.types[:html]).to eq(25)
end
end end
end end