Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sanitise HTML attributes in the database #12943

Merged
merged 2 commits into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions app/models/custom_tab.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@ class CustomTab < ApplicationRecord

validates :title, presence: true, length: { maximum: 20 }

# Remove any unsupported HTML.
def content
HtmlSanitizer.sanitize(super)
end

# Remove any unsupported HTML.
def content=(html)
super(HtmlSanitizer.sanitize(html))
Expand Down
5 changes: 0 additions & 5 deletions app/models/enterprise_group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,6 @@ def to_param
permalink
end

# Remove any unsupported HTML.
def long_description
HtmlSanitizer.sanitize_and_enforce_link_target_blank(super)
end

# Remove any unsupported HTML.
def long_description=(html)
super(HtmlSanitizer.sanitize_and_enforce_link_target_blank(html))
Expand Down
5 changes: 0 additions & 5 deletions app/models/spree/product.rb
Original file line number Diff line number Diff line change
Expand Up @@ -279,11 +279,6 @@ def ensure_standard_variant
end
# rubocop:enable Metrics/AbcSize

# Remove any unsupported HTML.
def description
HtmlSanitizer.sanitize(super)
end

# Remove any unsupported HTML.
def description=(html)
super(HtmlSanitizer.sanitize(html))
Expand Down
58 changes: 58 additions & 0 deletions db/migrate/20241023054951_sanitize_html_attributes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# frozen_string_literal: true

class SanitizeHtmlAttributes < ActiveRecord::Migration[7.0]
class CustomTab < ApplicationRecord
end

class EnterpriseGroup < ApplicationRecord
end

class SpreeProduct < ApplicationRecord
end

# This is a copy from our application code at the time of writing.
# We prefer to keep migrations isolated and not affected by changing
# application code in the future.
# If we need to change the sanitizer in the future we may need a new
# migration (not change the old one) to sanitise the data properly.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great documentation 🏅

class HtmlSanitizer
ALLOWED_TAGS = %w[h1 h2 h3 h4 div p br b i u a strong em del pre blockquote ul ol li hr
figure].freeze
ALLOWED_ATTRIBUTES = %w[href target].freeze
ALLOWED_TRIX_DATA_ATTRIBUTES = %w[data-trix-attachment].freeze

def self.sanitize(html)
@sanitizer ||= Rails::HTML5::SafeListSanitizer.new
@sanitizer.sanitize(
html, tags: ALLOWED_TAGS, attributes: (ALLOWED_ATTRIBUTES + ALLOWED_TRIX_DATA_ATTRIBUTES)
)
end

def self.sanitize_and_enforce_link_target_blank(html)
sanitize(enforce_link_target_blank(html))
end

def self.enforce_link_target_blank(html)
return if html.nil?

Nokogiri::HTML::DocumentFragment.parse(html).tap do |document|
document.css("a").each { |link| link["target"] = "_blank" }
end.to_s
end
end

def up
CustomTab.where.not(content: [nil, ""]).find_each do |row|
sane = HtmlSanitizer.sanitize(row.content)
row.update_column(:content, sane)
end
EnterpriseGroup.where.not(long_description: [nil, ""]).find_each do |row|
sane = HtmlSanitizer.sanitize_and_enforce_link_target_blank(row.long_description)
row.update_column(:long_description, sane)
end
SpreeProduct.where.not(description: [nil, ""]).find_each do |row|
sane = HtmlSanitizer.sanitize(row.description)
row.update_column(:description, sane)
end
end
end
2 changes: 1 addition & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.0].define(version: 2024_10_02_014059) do
ActiveRecord::Schema[7.0].define(version: 2024_10_23_054951) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
enable_extension "plpgsql"
Expand Down
61 changes: 61 additions & 0 deletions spec/migrations/20241023054951_sanitize_html_attributes_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true

require 'spec_helper'
require_relative '../../db/migrate/20241023054951_sanitize_html_attributes'

RSpec.describe SanitizeHtmlAttributes do
describe "#up" do
# Let's hack some bad data:
let!(:tab) {
create(:custom_tab).tap do |row|
row.update_columns(content: bad_html)
end
}
let!(:enterprise_group) {
create(:enterprise_group).tap do |row|
row.update_columns(long_description: bad_html)
end
}
let!(:product) {
create(:product).tap do |row|
row.update_columns(description: bad_html)
end
}
let(:bad_html) {
<<~HTML.squish
<p data-controller="load->payMe">Fred Farmer is a certified
<a href="https://example.net/">organic</a>
<script>alert("Gotcha!")</script>...</p>
HTML
}
let(:good_html) {
<<~HTML.squish
<p>Fred Farmer is a certified
<a href="https://example.net/">organic</a>
alert("Gotcha!")...</p>
HTML
}
let(:good_html_external_link) {
<<~HTML.squish
<p>Fred Farmer is a certified
<a href="https://example.net/" target="_blank">organic</a>
alert("Gotcha!")...</p>
HTML
}

it "sanitises HTML attributes" do
expect { subject.up }.to change {
tab.reload.attributes["content"]
}
.from(bad_html).to(good_html)
.and change {
enterprise_group.reload.attributes["long_description"]
}
.from(bad_html).to(good_html_external_link)
.and change {
product.reload.attributes["description"]
}
.from(bad_html).to(good_html)
end
end
end
5 changes: 0 additions & 5 deletions spec/models/custom_tab_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,5 @@
subject.content = "Hello <script>alert</script> dearest <b>monster</b>."
expect(subject.content).to eq "Hello alert dearest <b>monster</b>."
end

it "sanitises existing HTML in content" do
subject[:content] = "Hello <script>alert</script> dearest <b>monster</b>."
expect(subject.content).to eq "Hello alert dearest <b>monster</b>."
end
end
end
5 changes: 0 additions & 5 deletions spec/models/enterprise_group_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,5 @@
subject.long_description = "Hello <script>alert</script> dearest <b>monster</b>."
expect(subject.long_description).to eq "Hello alert dearest <b>monster</b>."
end

it "sanitises existing HTML in long_description" do
subject[:long_description] = "Hello <script>alert</script> dearest <b>monster</b>."
expect(subject.long_description).to eq "Hello alert dearest <b>monster</b>."
end
end
end
5 changes: 0 additions & 5 deletions spec/models/spree/product_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -707,11 +707,6 @@ module Spree
subject.description = "Hello <script>alert</script> dearest <b>monster</b>."
expect(subject.description).to eq "Hello alert dearest <b>monster</b>."
end

it "sanitises existing HTML in description" do
subject[:description] = "Hello <script>alert</script> dearest <b>monster</b>."
expect(subject.description).to eq "Hello alert dearest <b>monster</b>."
end
end
end

Expand Down
Loading