SECURITY: limit the number of characters in watched word replacements.

The watch words controller creation function, create_or_update_word(), doesn’t validate the size of the replacement parameter, unlike the word parameter, when creating a replace watched word. So anyone with moderator privileges can create watched words with almost unlimited characters.
This commit is contained in:
Vinoth Kannan 2024-05-30 06:32:57 +05:30 committed by Nat
parent 6ebd0c5aec
commit 7b53e610c1
No known key found for this signature in database
GPG Key ID: 4938B35D927EC773
8 changed files with 51 additions and 28 deletions

View File

@ -3,7 +3,10 @@
class WatchedWord < ActiveRecord::Base
MAX_WORDS_PER_ACTION = 2000
before_validation { self.word = WatchedWord.normalize_word(self.word) }
before_validation do
self.word = WatchedWord.normalize_word(self.word)
self.replacement = WatchedWord.normalize_word(self.replacement) if self.replacement.present?
end
before_validation do
if self.action == WatchedWord.actions[:link] && self.replacement !~ %r{\Ahttps?://}
@ -13,6 +16,7 @@ class WatchedWord < ActiveRecord::Base
end
validates :word, presence: true, uniqueness: true, length: { maximum: 100 }
validates :replacement, length: { maximum: 100 }
validates :action, presence: true
validate :replacement_is_url, if: -> { action == WatchedWord.actions[:link] }
validate :replacement_is_tag_list, if: -> { action == WatchedWord.actions[:tag] }

View File

@ -2,25 +2,25 @@
class WatchedWordGroup < ActiveRecord::Base
validates :action, presence: true
validate :watched_words_validation
has_many :watched_words, dependent: :destroy
def watched_words_validation
watched_words.each { |word| errors.merge!(word.errors) }
errors.add(:watched_words, :empty) if watched_words.empty?
end
def create_or_update_members(words, params)
WatchedWordGroup.transaction do
self.action = WatchedWord.actions[params[:action_key].to_sym]
self.save! if self.changed?
words.each do |word|
watched_word =
WatchedWord.create_or_update_word(
params.merge(word: word, watched_word_group_id: self.id),
)
if !watched_word.valid?
self.errors.merge!(watched_word.errors)
raise ActiveRecord::Rollback
end
watched_word = WatchedWord.create_or_update_word(params.merge(word: word))
self.watched_words << watched_word
end
self.save!
end
end

View File

@ -1,3 +1,6 @@
# frozen_string_literal: true
Fabricator(:watched_word_group) { action WatchedWord.actions[:block] }
Fabricator(:watched_word_group) do
action WatchedWord.actions[:block]
watched_words { [Fabricate.build(:watched_word)] }
end

View File

@ -2,7 +2,7 @@
RSpec.describe WatchedWordGroup do
fab!(:watched_word_group)
fab!(:watched_word_1) { Fabricate(:watched_word, watched_word_group_id: watched_word_group.id) }
let!(:watched_word_1) { watched_word_group.watched_words.first }
fab!(:watched_word_2) { Fabricate(:watched_word, watched_word_group_id: watched_word_group.id) }
describe "#create_or_update_members" do
@ -26,7 +26,7 @@ RSpec.describe WatchedWordGroup do
expect(watched_word_group.action).to eq(WatchedWord.actions[:censor])
end
it "leaves membership intact if update fails" do
it "raises an error if validation fails" do
words = [watched_word_1.word, watched_word_2.word, "a" * 120]
old_action = watched_word_group.action
watched_words_before_update = watched_word_group.watched_words
@ -38,19 +38,12 @@ RSpec.describe WatchedWordGroup do
)
expect(watched_words_before_update.map(&:action).uniq).to contain_exactly(old_action)
watched_word_group.create_or_update_members(
words,
action_key: WatchedWord.actions[watched_word_group.action],
)
expect(watched_word_group.reload.errors).not_to be_empty
watched_words = watched_word_group.watched_words
expect(watched_word_group.action).to eq(old_action)
expect(watched_words.size).to eq(2)
expect(watched_words.map(&:word)).to contain_exactly(watched_word_1.word, watched_word_2.word)
expect(watched_words.map(&:action).uniq).to contain_exactly(old_action)
expect {
watched_word_group.create_or_update_members(
words,
action_key: WatchedWord.actions[watched_word_group.action],
)
}.to raise_error(ActiveRecord::RecordInvalid)
end
end
end

View File

@ -26,6 +26,18 @@ RSpec.describe WatchedWord do
expect(described_class.create(word: "Jest").case_sensitive?).to eq(false)
end
it "limits the number of characters in a word" do
w = Fabricate.build(:watched_word, word: "a" * 101)
expect(w).to_not be_valid
expect(w.errors[:word]).to be_present
end
it "limits the number of characters in a replacement" do
w = Fabricate.build(:watched_word, replacement: "a" * 101)
expect(w).to_not be_valid
expect(w.errors[:replacement]).to be_present
end
describe "action_key=" do
let(:w) { WatchedWord.new(word: "troll") }

View File

@ -89,7 +89,7 @@ RSpec.describe Admin::WatchedWordsController do
it "should delete watched word group if it's the last word" do
watched_word_group = Fabricate(:watched_word_group)
watched_word.update!(watched_word_group: watched_word_group)
watched_word = watched_word_group.watched_words.first
delete "/admin/customize/watched_words/#{watched_word.id}.json"

View File

@ -17,4 +17,11 @@ describe "Admin Watched Words", type: :system, js: true do
expect(ww_page).to have_word
end
it "shows error when character limit is exceeded" do
ww_page.visit
ww_page.add_word "a" * 101
expect(ww_page).to have_error("Word is too long (maximum is 100 characters)")
end
end

View File

@ -20,6 +20,10 @@ module PageObjects
def has_word?
has_css?(".watched-words-detail .show-words-checkbox")
end
def has_error?(error)
has_css?(".dialog-container .dialog-body", text: error)
end
end
end
end