mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
DEV: Sanitize HTML admin inputs (#14681)
* DEV: Sanitize HTML admin inputs
This PR adds on-save HTML sanitization for:
Client site settings
translation overrides
badges descriptions
user fields descriptions
I used Rails's SafeListSanitizer, which [accepts the following HTML tags and attributes](018cf54073/lib/rails/html/sanitizer.rb (L108))
* Make sure that the sanitization logic doesn't corrupt settings with special characters
This commit is contained in:
@@ -5,6 +5,7 @@ class Badge < ActiveRecord::Base
|
||||
self.ignored_columns = %w{image}
|
||||
|
||||
include GlobalPath
|
||||
include HasSanitizableFields
|
||||
|
||||
# NOTE: These badge ids are not in order! They are grouped logically.
|
||||
# When picking an id, *search* for it.
|
||||
@@ -116,6 +117,7 @@ class Badge < ActiveRecord::Base
|
||||
scope :enabled, -> { where(enabled: true) }
|
||||
|
||||
before_create :ensure_not_system
|
||||
before_save :sanitize_description
|
||||
|
||||
after_commit do
|
||||
SvgSprite.expire_cache
|
||||
@@ -314,6 +316,12 @@ class Badge < ActiveRecord::Base
|
||||
def ensure_not_system
|
||||
self.id = [Badge.maximum(:id) + 1, 100].max unless id
|
||||
end
|
||||
|
||||
def sanitize_description
|
||||
if description_changed?
|
||||
self.description = sanitize_field(self.description)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
|
||||
23
app/models/concerns/has_sanitizable_fields.rb
Normal file
23
app/models/concerns/has_sanitizable_fields.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HasSanitizableFields
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def sanitize_field(field, additional_attributes: [])
|
||||
if field
|
||||
sanitizer = Rails::Html::SafeListSanitizer.new
|
||||
allowed_attributes = Rails::Html::SafeListSanitizer.allowed_attributes
|
||||
|
||||
if additional_attributes.present?
|
||||
allowed_attributes = allowed_attributes.merge(additional_attributes)
|
||||
end
|
||||
|
||||
field = CGI.unescape_html(sanitizer.sanitize(field, attributes: allowed_attributes))
|
||||
# Just replace the characters that our translations use for interpolation.
|
||||
# Calling CGI.unescape removes characters like '+', which will corrupt the original value.
|
||||
field = field.gsub('%7B', '{').gsub('%7D', '}')
|
||||
end
|
||||
|
||||
field
|
||||
end
|
||||
end
|
||||
@@ -39,6 +39,7 @@ class TranslationOverride < ActiveRecord::Base
|
||||
}
|
||||
}
|
||||
|
||||
include HasSanitizableFields
|
||||
include ActiveSupport::Deprecation::DeprecatedConstantAccessor
|
||||
deprecate_constant 'CUSTOM_INTERPOLATION_KEYS_WHITELIST', 'TranslationOverride::ALLOWED_CUSTOM_INTERPOLATION_KEYS'
|
||||
|
||||
@@ -50,13 +51,15 @@ class TranslationOverride < ActiveRecord::Base
|
||||
def self.upsert!(locale, key, value)
|
||||
params = { locale: locale, translation_key: key }
|
||||
|
||||
data = { value: value }
|
||||
translation_override = find_or_initialize_by(params)
|
||||
sanitized_value = translation_override.sanitize_field(value, additional_attributes: ['data-auto-route'])
|
||||
|
||||
data = { value: sanitized_value }
|
||||
if key.end_with?('_MF')
|
||||
_, filename = JsLocaleHelper.find_message_format_locale([locale], fallback_to_english: false)
|
||||
data[:compiled_js] = JsLocaleHelper.compile_message_format(filename, locale, value)
|
||||
data[:compiled_js] = JsLocaleHelper.compile_message_format(filename, locale, sanitized_value)
|
||||
end
|
||||
|
||||
translation_override = find_or_initialize_by(params)
|
||||
params.merge!(data) if translation_override.new_record?
|
||||
i18n_changed(locale, [key]) if translation_override.update(data)
|
||||
translation_override
|
||||
@@ -125,7 +128,6 @@ class TranslationOverride < ActiveRecord::Base
|
||||
if original_text
|
||||
original_interpolation_keys = I18nInterpolationKeysFinder.find(original_text)
|
||||
new_interpolation_keys = I18nInterpolationKeysFinder.find(value)
|
||||
|
||||
custom_interpolation_keys = []
|
||||
|
||||
ALLOWED_CUSTOM_INTERPOLATION_KEYS.select do |keys, value|
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
class UserField < ActiveRecord::Base
|
||||
|
||||
include AnonCacheInvalidator
|
||||
include HasSanitizableFields
|
||||
|
||||
validates_presence_of :description, :field_type
|
||||
validates_presence_of :name, unless: -> { field_type == "confirm" }
|
||||
@@ -10,6 +11,7 @@ class UserField < ActiveRecord::Base
|
||||
has_one :directory_column, dependent: :destroy
|
||||
accepts_nested_attributes_for :user_field_options
|
||||
|
||||
before_save :sanitize_description
|
||||
after_save :queue_index_search
|
||||
|
||||
def self.max_length
|
||||
@@ -19,6 +21,14 @@ class UserField < ActiveRecord::Base
|
||||
def queue_index_search
|
||||
SearchIndexer.queue_users_reindex(UserCustomField.where(name: "user_field_#{self.id}").pluck(:user_id))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sanitize_description
|
||||
if description_changed?
|
||||
self.description = sanitize_field(self.description)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
|
||||
Reference in New Issue
Block a user