Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- Upgraded gems:
- rails-html-sanitizer, sqlite3, websocket-driver
- Bugs fixes:
- Models: scrub invalid UTF-8 byte sequences from every :string and :text column before saving
- [entity]:
- [future tense verb] [bug fix]
- Bug tracker items:
Expand Down
19 changes: 19 additions & 0 deletions config/initializers/scrub_invalid_encoding.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Content can contain invalid UTF-8 byte sequences. MySQL can be configured
# to reject these at the DB layer, but CE runs on SQLite, which has no
# charset enforcement at all, so DB-level rejection can't be the shared fix
# across editions. Scrub instead, so an unscrubbed value never reaches the
# DB and never raises an unhandled ArgumentError later, wherever it happens
# to be read first rather than where it was written.
#
# ActiveModel::Type::String is the shared base type behind every :string and
# :text column (ActiveRecord::Type::String and ActiveRecord::Type::Text both
# inherit from it), so scrubbing here covers every column of both types, on
# every model and every adapter, without registering per model or column.
module ScrubsInvalidEncoding
def cast(value)
value = value.scrub if value.is_a?(String)
super
end
end

ActiveModel::Type::String.prepend(ScrubsInvalidEncoding)
45 changes: 45 additions & 0 deletions spec/initializers/scrub_invalid_encoding_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
require 'rails_helper'

describe 'scrubbing invalid encoding' do
let(:bad_bytes) { "before \xC3\x28 after" }

it 'scrubs invalid byte sequences on cast' do
type = ActiveModel::Type::String.new

expect(type.cast(bad_bytes).valid_encoding?).to eq(true)
end

it 'leaves valid strings untouched' do
type = ActiveModel::Type::String.new

expect(type.cast('valid string')).to eq('valid string')
end

it 'passes non-string values through to the wrapped type' do
type = ActiveModel::Type::String.new

expect(type.cast(42)).to eq('42')
end

it 'covers :string columns' do
node = build(:node, label: bad_bytes)

node.save!

expect(node.reload.label.valid_encoding?).to eq(true)
end

it 'covers :text columns' do
note = build(:note, text: bad_bytes)

note.save!

expect(note.reload.text.valid_encoding?).to eq(true)
end

it 'covers :string columns without per-model opt-in' do
card = create(:card, name: bad_bytes)

expect(card.reload.name.valid_encoding?).to eq(true)
end
end
Loading