Sanitize descriptions in the views

Sanitizing descriptions before saving a record has a few drawbacks:

1. It makes the application rely on data being safe in the database. If
somehow dangerous data enters the database, the application will be
vulnerable to XSS attacks
2. It makes the code complicated
3. It isn't backwards compatible; if we decide to disallow a certain
HTML tag in the future, we'd need to sanitize existing data.

On the other hand, sanitizing the data in the view means we don't need
to triple-check dangerous HTML has already been stripped when we see the
method `auto_link_already_sanitized_html`, since now every time we use
it we sanitize the text in the same line we call this method.

We could also sanitize the data twice, both when saving to the database
and when displaying values in the view. However, doing so wouldn't make
the application safer, since we sanitize text introduced through
textarea fields but we don't sanitize text introduced through input
fields.

Finally, we could also overwrite the `description` method so it
sanitizes the text. But we're already introducing Globalize which
overwrites that method, and overwriting it again is a bit too confusing
in my humble opinion. It can also lead to hard-to-debug behaviour.
This commit is contained in:
Javi Martín
2019-10-06 03:41:53 +02:00
parent ae2576020e
commit 7bf4e4d611
30 changed files with 47 additions and 129 deletions

View File

@@ -42,8 +42,6 @@ class Budget < ApplicationRecord
has_one :poll has_one :poll
before_validation :sanitize_descriptions
after_create :generate_phases after_create :generate_phases
scope :drafting, -> { where(phase: "drafting") } scope :drafting, -> { where(phase: "drafting") }
@@ -79,7 +77,7 @@ class Budget < ApplicationRecord
if phases.exists? && phases.send(phase).description.present? if phases.exists? && phases.send(phase).description.present?
phases.send(phase).description phases.send(phase).description
else else
send("description_#{phase}")&.html_safe send("description_#{phase}")
end end
end end
@@ -205,14 +203,6 @@ class Budget < ApplicationRecord
private private
def sanitize_descriptions
s = WYSIWYGSanitizer.new
Budget::Phase::PHASE_KINDS.each do |phase|
sanitized = s.sanitize(send("description_#{phase}"))
send("description_#{phase}=", sanitized)
end
end
def generate_phases def generate_phases
Budget::Phase::PHASE_KINDS.each do |phase| Budget::Phase::PHASE_KINDS.each do |phase|
Budget::Phase.create( Budget::Phase.create(

View File

@@ -9,17 +9,6 @@ class Budget
translates :summary, touch: true translates :summary, touch: true
translates :description, touch: true translates :description, touch: true
include Globalizable include Globalizable
class Translation
before_validation :sanitize_description
private
def sanitize_description
self.description = WYSIWYGSanitizer.new.sanitize(description)
end
end
include Sanitizable include Sanitizable
belongs_to :budget belongs_to :budget

View File

@@ -9,10 +9,6 @@ module Globalizable
validate :check_translations_number, on: :update, if: :translations_required? validate :check_translations_number, on: :update, if: :translations_required?
after_validation :copy_error_to_current_translation, on: :update after_validation :copy_error_to_current_translation, on: :update
def description
self.read_attribute(:description)&.html_safe
end
def locales_not_marked_for_destruction def locales_not_marked_for_destruction
translations.reject(&:marked_for_destruction?).map(&:locale) translations.reject(&:marked_for_destruction?).map(&:locale)
end end

View File

@@ -2,49 +2,12 @@ module Sanitizable
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
before_validation :sanitize_description
before_validation :sanitize_tag_list before_validation :sanitize_tag_list
unless included_modules.include? Globalizable
def description
super&.html_safe
end
end
end end
protected protected
def sanitize_description
if translatable_description?
sanitize_description_translations
else
self.description = WYSIWYGSanitizer.new.sanitize(description)
end
end
def sanitize_tag_list def sanitize_tag_list
self.tag_list = TagSanitizer.new.sanitize_tag_list(tag_list) if self.class.taggable? self.tag_list = TagSanitizer.new.sanitize_tag_list(tag_list) if self.class.taggable?
end end
def translatable_description?
self.class.included_modules.include?(Globalizable) &&
self.class.translated_attribute_names.include?(:description)
end
def sanitize_description_translations
# Sanitize description when using attribute accessor in place of nested translations.
# This is because Globalize gem create translations on after save callback
# https://github.com/globalize/globalize/blob/e37c471775d196cd4318e61954572c300c015467/lib/globalize/active_record/act_macro.rb#L105
if translations.empty?
Globalize.with_locale(I18n.locale) do
self.description = WYSIWYGSanitizer.new.sanitize(description)
end
end
translations.reject(&:_destroy).each do |translation|
Globalize.with_locale(translation.locale) do
self.description = WYSIWYGSanitizer.new.sanitize(description)
end
end
end
end end

View File

@@ -19,10 +19,6 @@ class Poll::Question::Answer < ApplicationRecord
scope :visibles, -> { where(hidden: false) } scope :visibles, -> { where(hidden: false) }
def description
self[:description]&.html_safe
end
def self.order_answers(ordered_array) def self.order_answers(ordered_array)
ordered_array.each_with_index do |answer_id, order| ordered_array.each_with_index do |answer_id, order|
find(answer_id).update_attribute(:given_order, (order + 1)) find(answer_id).update_attribute(:given_order, (order + 1))

View File

@@ -25,7 +25,7 @@
</span> </span>
</div> </div>
<%= auto_link_already_sanitized_html @debate.description %> <%= auto_link_already_sanitized_html wysiwyg(@debate.description) %>
<h3><%= t("votes.supports") %></h3> <h3><%= t("votes.supports") %></h3>

View File

@@ -20,7 +20,7 @@
</td> </td>
<td> <td>
<div class="moderation-description"> <div class="moderation-description">
<%= investment.description %> <%= wysiwyg(investment.description) %>
</div> </div>
</td> </td>
<td class="align-top"> <td class="align-top">

View File

@@ -20,7 +20,7 @@
</td> </td>
<td> <td>
<div class="moderation-description"> <div class="moderation-description">
<%= debate.description %> <%= wysiwyg(debate.description) %>
</div> </div>
</td> </td>
<td class="align-top"> <td class="align-top">

View File

@@ -21,7 +21,7 @@
<td> <td>
<div class="moderation-description"> <div class="moderation-description">
<p><small><%= proposal.summary %></small></p> <p><small><%= proposal.summary %></small></p>
<%= proposal.description %> <%= wysiwyg(proposal.description) %>
<% if proposal.video_url.present? %> <% if proposal.video_url.present? %>
<p><%= sanitize_and_auto_link proposal.video_url %></p> <p><%= sanitize_and_auto_link proposal.video_url %></p>
<% end %> <% end %>

View File

@@ -21,7 +21,7 @@
<tbody> <tbody>
<tr> <tr>
<td><%= @answer.title %></td> <td><%= @answer.title %></td>
<td><%= @answer.description %></td> <td><%= wysiwyg(@answer.description) %></td>
<td class="text-center"> <td class="text-center">
(<%= @answer.images.count %>)<br> (<%= @answer.images.count %>)<br>
<%= link_to t("admin.answers.show.images_list"), admin_answer_images_path(@answer) %> <%= link_to t("admin.answers.show.images_list"), admin_answer_images_path(@answer) %>

View File

@@ -82,7 +82,7 @@
<% @question.question_answers.each do |answer| %> <% @question.question_answers.each do |answer| %>
<tr id="<%= dom_id(answer) %>" class="poll_question_answer" data-answer-id="<%= answer.id %>"> <tr id="<%= dom_id(answer) %>" class="poll_question_answer" data-answer-id="<%= answer.id %>">
<td class="align-top"><%= link_to answer.title, admin_answer_path(answer) %></td> <td class="align-top"><%= link_to answer.title, admin_answer_path(answer) %></td>
<td class="align-top break"><%= answer.description %></td> <td class="align-top break"><%= wysiwyg(answer.description) %></td>
<td class="align-top text-center"> <td class="align-top text-center">
(<%= answer.images.count %>) (<%= answer.images.count %>)
<br> <br>

View File

@@ -15,7 +15,7 @@
<h1><%= current_budget.name %></h1> <h1><%= current_budget.name %></h1>
<div class="description"> <div class="description">
<%= auto_link_already_sanitized_html(current_budget.description) %> <%= auto_link_already_sanitized_html wysiwyg(current_budget.description) %>
</div> </div>
<p> <p>
<%= link_to t("budgets.index.section_header.help"), "#section_help" %> <%= link_to t("budgets.index.section_header.help"), "#section_help" %>

View File

@@ -46,7 +46,7 @@
<%= investment.heading.name %> <%= investment.heading.name %>
</p> </p>
<div class="investment-project-description"> <div class="investment-project-description">
<%= investment.description %> <%= wysiwyg(investment.description) %>
<div class="truncate"></div> <div class="truncate"></div>
</div> </div>
<%= render "shared/tags", taggable: investment, limit: 5 %> <%= render "shared/tags", taggable: investment, limit: 5 %>

View File

@@ -22,7 +22,7 @@
<%= sanitize(t("budgets.investments.show.code", code: investment.id)) %> <%= sanitize(t("budgets.investments.show.code", code: investment.id)) %>
</p> </p>
<%= auto_link_already_sanitized_html investment.description %> <%= auto_link_already_sanitized_html wysiwyg(investment.description) %>
<% if feature?(:map) && map_location_available?(@investment.map_location) %> <% if feature?(:map) && map_location_available?(@investment.map_location) %>
<div class="margin"> <div class="margin">

View File

@@ -9,7 +9,7 @@
<h1><%= @budget.name %></h1> <h1><%= @budget.name %></h1>
<%= auto_link_already_sanitized_html(@budget.description) %> <%= auto_link_already_sanitized_html wysiwyg(@budget.description) %>
</div> </div>
<div class="small-12 medium-3 column info padding" data-equalizer-watch> <div class="small-12 medium-3 column info padding" data-equalizer-watch>
<p> <p>

View File

@@ -39,7 +39,7 @@
</p> </p>
<div class="debate-description"> <div class="debate-description">
<%= debate.description %> <%= wysiwyg(debate.description) %>
<div class="truncate"></div> <div class="truncate"></div>
</div> </div>
<%= render "shared/tags", taggable: debate, limit: 5 %> <%= render "shared/tags", taggable: debate, limit: 5 %>

View File

@@ -30,7 +30,7 @@
</span> </span>
</div> </div>
<%= auto_link_already_sanitized_html @debate.description %> <%= auto_link_already_sanitized_html wysiwyg(@debate.description) %>
<%= render "shared/tags", taggable: @debate %> <%= render "shared/tags", taggable: @debate %>

View File

@@ -68,7 +68,7 @@
</div> </div>
<% end %> <% end %>
<%= auto_link_already_sanitized_html @proposal.description %> <%= auto_link_already_sanitized_html wysiwyg(@proposal.description) %>
<% if @proposal.video_url.present? %> <% if @proposal.video_url.present? %>
<div class="video-link"> <div class="video-link">

View File

@@ -40,7 +40,7 @@
<%= investment.author.username %> <%= investment.author.username %>
<br> <br>
<div class="moderation-description"> <div class="moderation-description">
<%= investment.description %> <%= wysiwyg(investment.description) %>
</div> </div>
</td> </td>
<td class="text-center"> <td class="text-center">

View File

@@ -37,7 +37,7 @@
<%= debate.author.username %> <%= debate.author.username %>
<br> <br>
<div class="moderation-description"> <div class="moderation-description">
<%= debate.description %> <%= wysiwyg(debate.description) %>
</div> </div>
</td> </td>
<td class="text-center"> <td class="text-center">

View File

@@ -37,7 +37,7 @@
<%= proposal.author.username %> <%= proposal.author.username %>
<br> <br>
<div class="moderation-description"> <div class="moderation-description">
<%= proposal.description %> <%= wysiwyg(proposal.description) %>
</div> </div>
</td> </td>
<td class="text-center"> <td class="text-center">

View File

@@ -67,7 +67,7 @@
<div id="answer_description_<%= answer.id %>" <div id="answer_description_<%= answer.id %>"
class="answer-description short answer-left-divider" data-toggler="short"> class="answer-description short answer-left-divider" data-toggler="short">
<% if answer.description.present? %> <% if answer.description.present? %>
<%= answer.description %> <%= wysiwyg(answer.description) %>
<% end %> <% end %>
<% if answer.images.any? %> <% if answer.images.any? %>

View File

@@ -39,7 +39,7 @@
</div> </div>
<% end %> <% end %>
<%= auto_link_already_sanitized_html @proposal.description %> <%= auto_link_already_sanitized_html wysiwyg(@proposal.description) %>
<% if feature?(:map) && map_location_available?(@proposal.map_location) %> <% if feature?(:map) && map_location_available?(@proposal.map_location) %>
<div class="margin"> <div class="margin">

View File

@@ -7,6 +7,6 @@
<h1><%= @investment.title %></h1> <h1><%= @investment.title %></h1>
<%= auto_link_already_sanitized_html @investment.description %> <%= auto_link_already_sanitized_html wysiwyg(@investment.description) %>
<%= render "tracking/milestones/milestones", milestoneable: @investment %> <%= render "tracking/milestones/milestones", milestoneable: @investment %>

View File

@@ -3,7 +3,7 @@
<h2><%= t("tracking.budget_investments.show.title") %> <%= @investment.id %> </h2> <h2><%= t("tracking.budget_investments.show.title") %> <%= @investment.id %> </h2>
<h1><%= @investment.title %></h1> <h1><%= @investment.title %></h1>
<%= auto_link_already_sanitized_html @investment.description %> <%= auto_link_already_sanitized_html wysiwyg(@investment.description) %>
<% if @investment.external_url.present? %> <% if @investment.external_url.present? %>
<p><%= sanitize_and_auto_link @investment.external_url %></p> <p><%= sanitize_and_auto_link @investment.external_url %></p>

View File

@@ -16,7 +16,7 @@
<%= link_to recommended_path(recommended) do %> <%= link_to recommended_path(recommended) do %>
<h4 class="truncate-horizontal-text"><%= recommended.title %></h4> <h4 class="truncate-horizontal-text"><%= recommended.title %></h4>
<% end %> <% end %>
<%= recommended.description %> <%= wysiwyg(recommended.description) %>
</div> </div>
</div> </div>
</li> </li>

View File

@@ -121,6 +121,31 @@ describe "Cross-Site Scripting protection", :js do
expect(page.text).not_to be_empty expect(page.text).not_to be_empty
end end
scenario "proposal description" do
proposal = create(:proposal, description: attack_code)
visit proposal_path(proposal)
expect(page.text).not_to be_empty
end
scenario "investment description" do
investment = create(:budget_investment, description: attack_code)
visit budget_investment_path(investment.budget, investment)
expect(page.text).not_to be_empty
end
scenario "budget phase description" do
budget = create(:budget)
budget.current_phase.update(description: attack_code)
visit budget_path(budget)
expect(page.text).not_to be_empty
end
scenario "markdown conversion" do scenario "markdown conversion" do
process = create(:legislation_process, description: attack_code) process = create(:legislation_process, description: attack_code)

View File

@@ -205,12 +205,4 @@ describe Budget::Phase do
end end
end end
end end
describe "#sanitize_description" do
it "removes not allowed html entities from the description" do
expect do
first_phase.update_attributes(description: '<p><a href="/"><b>a</b></a></p> <script>javascript</script>')
end.to change { first_phase.description }.to('<p><a href="/">a</a></p> javascript')
end
end
end end

View File

@@ -37,7 +37,6 @@ describe Budget do
Budget::Phase::PHASE_KINDS.each do |phase_kind| Budget::Phase::PHASE_KINDS.each do |phase_kind|
budget.phase = phase_kind budget.phase = phase_kind
expect(budget.description).to eq(budget.send("description_#{phase_kind}")) expect(budget.description).to eq(budget.send("description_#{phase_kind}"))
expect(budget.description).to be_html_safe
end end
end end
end end

View File

@@ -1,38 +1,6 @@
shared_examples "sanitizable" do shared_examples "sanitizable" do
let(:sanitizable) { build(model_name(described_class)) } let(:sanitizable) { build(model_name(described_class)) }
it "is sanitized" do
sanitizable.description = "<script>alert('danger');</script>"
sanitizable.valid?
expect(sanitizable.description).to eq("alert('danger');")
end
it "is html_safe" do
sanitizable.description = "<script>alert('danger');</script>"
sanitizable.valid?
expect(sanitizable.description).to be_html_safe
end
it "is sanitized using globalize accessors" do
sanitizable.description_en = "<script>alert('danger');</script>"
sanitizable.valid?
expect(sanitizable.description_en).to eq("alert('danger');")
end
it "is html_safe using globalize accessors" do
sanitizable.description_en = "<script>alert('danger');</script>"
sanitizable.valid?
expect(sanitizable.description_en).to be_html_safe
end
describe "#tag_list" do describe "#tag_list" do
before do before do
unless described_class.included_modules.include?(Taggable) unless described_class.included_modules.include?(Taggable)