From bd780497bb4194a8172c3a4ae05000938094e418 Mon Sep 17 00:00:00 2001 From: jmax-fearless <66315527+jmax-fearless@users.noreply.github.com> Date: Fri, 19 Feb 2021 10:15:02 -0500 Subject: [PATCH] SRCH-1771 block html injection (#656) SRCH-1771 - block html injection --- app/models/routed_query_keyword.rb | 32 +++++++---- spec/models/routed_query_keyword_spec.rb | 51 ++++++++++++----- spec/models/routed_query_spec.rb | 71 +++++++++++++++++------- 3 files changed, 110 insertions(+), 44 deletions(-) diff --git a/app/models/routed_query_keyword.rb b/app/models/routed_query_keyword.rb index a31bc26c74..5bf11b0cd1 100644 --- a/app/models/routed_query_keyword.rb +++ b/app/models/routed_query_keyword.rb @@ -1,7 +1,10 @@ +# frozen_string_literal: true + class RoutedQueryKeyword < ApplicationRecord include Dupable before_validation do |record| + AttributeProcessor.sanitize_attributes record, :keyword AttributeProcessor.squish_attributes record, :keyword, assign_nil_on_blank: true @@ -11,13 +14,16 @@ class RoutedQueryKeyword < ApplicationRecord belongs_to :routed_query, inverse_of: :routed_query_keywords validates :routed_query, presence: true - validates_presence_of :keyword - validates_uniqueness_of :keyword, scope: :routed_query_id, case_sensitive: false + validates :keyword, presence: true + validates :keyword, uniqueness: { + scope: :routed_query_id, + case_sensitive: false + } validate :keyword_unique_to_affiliate def self.do_not_dup_attributes - @@do_not_dup_attributes ||= %w(routed_query_id).freeze + @do_not_dup_attributes ||= %w[routed_query_id].freeze end def label @@ -25,16 +31,22 @@ def label end def keyword_unique_to_affiliate - return unless routed_query && routed_query.affiliate + return unless routed_query&.affiliate + return unless relation.any? + + errors[:keyword] << + "The keyword '#{keyword}' is already in use for a different routed query" + end - relation = routed_query.affiliate.routed_queries - .joins(:routed_query_keywords) - .where('routed_query_keywords.keyword = ?', keyword) + def relation + relation = routed_query. + affiliate. + routed_queries. + joins(:routed_query_keywords). + where('routed_query_keywords.keyword = ?', keyword) relation = relation.where('routed_query_id != ?', routed_query_id) if routed_query_id - if relation.any? - errors[:keyword] << "The keyword '#{keyword}' is already in use for a different routed query" - end + relation end end diff --git a/spec/models/routed_query_keyword_spec.rb b/spec/models/routed_query_keyword_spec.rb index 57ef4cfae0..11ee202db5 100644 --- a/spec/models/routed_query_keyword_spec.rb +++ b/spec/models/routed_query_keyword_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +# frozen_string_literal: true describe RoutedQueryKeyword do fixtures :affiliates, :routed_queries, :routed_query_keywords @@ -10,38 +10,59 @@ it { is_expected.to validate_presence_of :keyword } it { is_expected.to validate_uniqueness_of(:keyword).scoped_to(:routed_query_id).case_insensitive } - it 'should create a new instance given valid attributes' do + it 'creates a new instance given valid attributes' do routed_query.routed_query_keywords.create!(keyword: 'route me') end - it 'should downcase, squish and strip whitespace from keyword before inserting in DB' do + it 'downcases, squish and strip whitespace from keyword before inserting in DB' do keyword = ' leading and trailing whitespaces AND CAPITALS' rqk = routed_query.routed_query_keywords.create!(keyword: keyword) expect(rqk.keyword).to eq('leading and trailing whitespaces and capitals') end - it 'should not allow the same keyword to be reused within a single affiliate' do + it 'does not allow the same keyword to be reused within a single affiliate' do routed_queries(:unclaimed_money).routed_query_keywords.create!(keyword: 'route me') - rqk = routed_queries(:moar_unclaimed_money).routed_query_keywords.build(keyword: 'route me') + rqk = routed_queries(:moar_unclaimed_money).routed_query_keywords.build( + keyword: 'route me' + ) + expected_error = + 'The keyword \'route me\' is already in use for a different routed query' + expect(rqk).not_to be_valid - expect(rqk.errors[:keyword]).to eq(["The keyword 'route me' is already in use for a different routed query"]) + expect(rqk.errors[:keyword]).to eq([expected_error]) end it 'creates a matching SaytSuggestion' do routed_query.routed_query_keywords.create!(keyword: 'route me') - sayt_suggestion = SaytSuggestion.find_by_affiliate_id_and_phrase_and_is_protected(routed_query.affiliate.id, 'route me', true) + sayt_suggestion = SaytSuggestion.find_by(affiliate_id: routed_query.affiliate.id, + phrase: 'route me', + is_protected: true) expect(sayt_suggestion).to be_present end + + context 'when a keyword contains html' do + let(:keyword) do + routed_query.routed_query_keywords.create!( + keyword: 'keyword
with html' + ) + end + + it 'strips the html from the keyword' do + expect(keyword.keyword).to eq('keyword with html') + end + end end describe 'Updating a keyword' do before do keyword = routed_query.routed_query_keywords.create!(keyword: 'initial keyword') - keyword.update_attribute(:keyword, 'updated keyword') + keyword.update(keyword: 'updated keyword') end it 'updates the matching SaytSuggestion' do - sayt_suggestion = SaytSuggestion.find_by_affiliate_id_and_phrase_and_is_protected(routed_query.affiliate.id, 'updated keyword', true) + sayt_suggestion = SaytSuggestion.find_by(affiliate_id: routed_query.affiliate.id, + phrase: 'updated keyword', + is_protected: true) expect(sayt_suggestion).to be_present end end @@ -49,13 +70,14 @@ describe 'deleting a keyword' do before do routed_query.routed_query_keywords.create!(keyword: 'some keyword') - sayt_suggestion = SaytSuggestion.find_by_affiliate_id_and_phrase_and_is_protected(routed_query.affiliate.id, 'some keyword', true) - expect(sayt_suggestion).to be_present end it 'deletes the matching SaytSuggestion' do - RoutedQueryKeyword.last.destroy - sayt_suggestion = SaytSuggestion.find_by_affiliate_id_and_phrase_and_is_protected(routed_query.affiliate.id, 'some keyword', true) + described_class.last.destroy + sayt_suggestion = SaytSuggestion.find_by(affiliate_id: routed_query.affiliate.id, + phrase: 'some keyword', + is_protected: true) + expect(sayt_suggestion).to be_nil end end @@ -68,6 +90,7 @@ describe '#dup' do subject(:original_instance) { routed_query_keywords(:one) } - include_examples 'dupable', %w(routed_query_id) + + include_examples 'dupable', %w[routed_query_id] end end diff --git a/spec/models/routed_query_spec.rb b/spec/models/routed_query_spec.rb index 6f5cc35f2a..824294cbae 100644 --- a/spec/models/routed_query_spec.rb +++ b/spec/models/routed_query_spec.rb @@ -1,75 +1,106 @@ +# frozen_string_literal: true + require 'spec_helper' describe RoutedQuery do fixtures :affiliates, :routed_queries + subject(:routed_query) { described_class.new } + describe 'Creating new instance' do let(:affiliate) { affiliates(:basic_affiliate) } let(:valid_attributes) do { description: 'Some desc', url: 'http://www.gov.gov/url.html', - routed_query_keywords_attributes: { '0' => { 'keyword' => 'some keyword phrase' } } + routed_query_keywords_attributes: { + '0' => { 'keyword' => 'some keyword phrase' } + } } end it { is_expected.to belong_to :affiliate } - it { is_expected.to have_many(:routed_query_keywords).dependent(:destroy).inverse_of(:routed_query)} + + it 'has keywords' do + expect(routed_query).to have_many(:routed_query_keywords). + dependent(:destroy). + inverse_of(:routed_query) + end + it { is_expected.to validate_presence_of :description } it { is_expected.to validate_uniqueness_of(:description).scoped_to(:affiliate_id) } it { is_expected.to validate_presence_of :affiliate } it { is_expected.to allow_value('http://www.foo.com').for(:url) } it { is_expected.not_to allow_value('www.foo.com').for(:url) } - it 'should create a new instance given valid attributes' do + it 'creates a new instance given valid attributes' do affiliate.routed_queries.create!(valid_attributes) end context 'when a keyword is duplicated' do let(:dup_attributes) do - valid_attributes.merge({ - routed_query_keywords_attributes: { - '0' => { 'keyword' => 'some keyword phrase' }, - '1' => { 'keyword' => 'Some Keyword Phrase' }, + valid_attributes.merge( + { + routed_query_keywords_attributes: + { + '0' => { 'keyword' => 'some keyword phrase' }, + '1' => { 'keyword' => 'Some Keyword Phrase' } + } } - }) + ) end - it 'should reject the save of the keywords' do + let(:expected_errors) do + 'The following keyword has been duplicated: \'some keyword phrase\'. ' \ + 'Each keyword is case-insensitive and should be added only once.' + end + + it 'rejects the save of the keywords' do rq = affiliate.routed_queries.build(dup_attributes) expect(rq.valid?).to be false - expect(rq.errors[:routed_query_keywords]).to include("The following keyword has been duplicated: 'some keyword phrase'. Each keyword is case-insensitive and should be added only once.") + expect(rq.errors[:routed_query_keywords]).to include(expected_errors) end end context 'when multiple keywords are duplicated' do let(:dup_attributes) do - valid_attributes.merge({ - routed_query_keywords_attributes: { - '0' => { 'keyword' => 'some keyword phrase' }, - '1' => { 'keyword' => 'Some Keyword Phrase' }, - '2' => { 'keyword' => 'some other keyword phrase' }, - '3' => { 'keyword' => 'Some Other Keyword Phrase' }, + valid_attributes.merge( + { + routed_query_keywords_attributes: + { + '0' => { 'keyword' => 'some keyword phrase' }, + '1' => { 'keyword' => 'Some Keyword Phrase' }, + '2' => { 'keyword' => 'some other keyword phrase' }, + '3' => { 'keyword' => 'Some Other Keyword Phrase' } + } } - }) + ) end - it 'should reject the save of the keywords' do + let(:expected_errors) do + 'The following keywords have been duplicated: \'some keyword phrase\', ' \ + '\'some other keyword phrase\'. ' \ + 'Each keyword is case-insensitive and should be added only once.' + end + + it 'rejects the save of the keywords' do rq = affiliate.routed_queries.build(dup_attributes) expect(rq.valid?).to be false - expect(rq.errors[:routed_query_keywords]).to include("The following keywords have been duplicated: 'some keyword phrase', 'some other keyword phrase'. Each keyword is case-insensitive and should be added only once.") + expect(rq.errors[:routed_query_keywords]).to include(expected_errors) end end end describe '#label' do it 'returns a label containing the url and description' do - expect(routed_queries(:unclaimed_money).label).to eq('https://www.usa.gov/unclaimed_money: Everybody wants it') + expect(routed_queries(:unclaimed_money).label). + to eq('https://www.usa.gov/unclaimed_money: Everybody wants it') end end describe '#dup' do subject(:original_instance) { routed_queries(:unclaimed_money) } + include_examples 'site dupable' end end