SECURITY: SQL injection with default categories

This is a low severity security fix because it requires a logged in
admin user to update a site setting via the API directly to an invalid
value.

The fix adds validation for the affected site settings, as well as a
secondary fix to prevent injection in the event of bad data somehow
already exists.
This commit is contained in:
Robin Ward 2019-07-11 13:41:51 -04:00
parent afe922c30b
commit 1d38040579
5 changed files with 54 additions and 15 deletions

View File

@ -1375,8 +1375,9 @@ class User < ActiveRecord::Base
values = [] values = []
%w{watching watching_first_post tracking muted}.each do |s| %w{watching watching_first_post tracking muted}.each do |s|
category_ids = SiteSetting.get("default_categories_#{s}").split("|") category_ids = SiteSetting.get("default_categories_#{s}").split("|").map(&:to_i)
category_ids.each do |category_id| category_ids.each do |category_id|
next if category_id == 0
values << "(#{self.id}, #{category_id}, #{CategoryUser.notification_levels[s.to_sym]})" values << "(#{self.id}, #{category_id}, #{CategoryUser.notification_levels[s.to_sym]})"
end end
end end

View File

@ -191,6 +191,7 @@ en:
embed: embed:
load_from_remote: "There was an error loading that post." load_from_remote: "There was an error loading that post."
site_settings: site_settings:
invalid_category_id: "You specified a category that does not exist"
invalid_choice: invalid_choice:
one: "You specified the invalid choice %{name}" one: "You specified the invalid choice %{name}"
other: "You specified the invalid choices %{name}" other: "You specified the invalid choices %{name}"

View File

@ -7,48 +7,62 @@ module SiteSettings::Validations
raise Discourse::InvalidParameters.new(I18n.t("errors.site_settings.#{key}", opts)) raise Discourse::InvalidParameters.new(I18n.t("errors.site_settings.#{key}", opts))
end end
def validate_default_categories(new_val, default_categories_selected) def validate_category_ids(category_ids)
validate_error :default_categories_already_selected if (new_val.split("|").to_set & default_categories_selected).size > 0 category_ids = category_ids.split('|').map(&:to_i).to_set
validate_error :invalid_category_id if Category.where(id: category_ids).count != category_ids.size
category_ids
end
def validate_default_categories(category_ids, default_categories_selected)
validate_error :default_categories_already_selected if (category_ids & default_categories_selected).size > 0
end end
def validate_default_categories_watching(new_val) def validate_default_categories_watching(new_val)
category_ids = validate_category_ids(new_val)
default_categories_selected = [ default_categories_selected = [
SiteSetting.default_categories_tracking.split("|"), SiteSetting.default_categories_tracking.split("|"),
SiteSetting.default_categories_muted.split("|"), SiteSetting.default_categories_muted.split("|"),
SiteSetting.default_categories_watching_first_post.split("|") SiteSetting.default_categories_watching_first_post.split("|")
].flatten.to_set ].flatten.to_set
validate_default_categories(new_val, default_categories_selected) validate_default_categories(category_ids, default_categories_selected)
end end
def validate_default_categories_tracking(new_val) def validate_default_categories_tracking(new_val)
category_ids = validate_category_ids(new_val)
default_categories_selected = [ default_categories_selected = [
SiteSetting.default_categories_watching.split("|"), SiteSetting.default_categories_watching.split("|"),
SiteSetting.default_categories_muted.split("|"), SiteSetting.default_categories_muted.split("|"),
SiteSetting.default_categories_watching_first_post.split("|") SiteSetting.default_categories_watching_first_post.split("|")
].flatten.to_set ].flatten.to_set
validate_default_categories(new_val, default_categories_selected) validate_default_categories(category_ids, default_categories_selected)
end end
def validate_default_categories_muted(new_val) def validate_default_categories_muted(new_val)
category_ids = validate_category_ids(new_val)
default_categories_selected = [ default_categories_selected = [
SiteSetting.default_categories_watching.split("|"), SiteSetting.default_categories_watching.split("|"),
SiteSetting.default_categories_tracking.split("|"), SiteSetting.default_categories_tracking.split("|"),
SiteSetting.default_categories_watching_first_post.split("|") SiteSetting.default_categories_watching_first_post.split("|")
].flatten.to_set ].flatten.to_set
validate_default_categories(new_val, default_categories_selected) validate_default_categories(category_ids, default_categories_selected)
end end
def validate_default_categories_watching_first_post(new_val) def validate_default_categories_watching_first_post(new_val)
category_ids = validate_category_ids(new_val)
default_categories_selected = [ default_categories_selected = [
SiteSetting.default_categories_watching.split("|"), SiteSetting.default_categories_watching.split("|"),
SiteSetting.default_categories_tracking.split("|"), SiteSetting.default_categories_tracking.split("|"),
SiteSetting.default_categories_muted.split("|") SiteSetting.default_categories_muted.split("|")
].flatten.to_set ].flatten.to_set
validate_default_categories(new_val, default_categories_selected) validate_default_categories(category_ids, default_categories_selected)
end end
def validate_enable_s3_uploads(new_val) def validate_enable_s3_uploads(new_val)

View File

@ -6,6 +6,24 @@ require 'site_settings/validations'
describe SiteSettings::Validations do describe SiteSettings::Validations do
subject { Class.new.include(described_class).new } subject { Class.new.include(described_class).new }
context "default_categories" do
fab!(:category) { Fabricate(:category) }
it "supports valid categories" do
expect { subject.validate_default_categories_watching("#{category.id}") }.not_to raise_error
end
it "won't allow you to input junk categories" do
expect {
subject.validate_default_categories_watching("junk")
}.to raise_error(Discourse::InvalidParameters)
expect {
subject.validate_default_categories_watching("#{category.id}|12312323")
}.to raise_error(Discourse::InvalidParameters)
end
end
context "s3 buckets reusage" do context "s3 buckets reusage" do
let(:error_message) { I18n.t("errors.site_settings.s3_bucket_reused") } let(:error_message) { I18n.t("errors.site_settings.s3_bucket_reused") }

View File

@ -1581,6 +1581,11 @@ describe User do
context "when user preferences are overriden" do context "when user preferences are overriden" do
fab!(:category0) { Fabricate(:category) }
fab!(:category1) { Fabricate(:category) }
fab!(:category2) { Fabricate(:category) }
fab!(:category3) { Fabricate(:category) }
before do before do
SiteSetting.default_email_digest_frequency = 1440 # daily SiteSetting.default_email_digest_frequency = 1440 # daily
SiteSetting.default_email_level = UserOption.email_level_types[:never] SiteSetting.default_email_level = UserOption.email_level_types[:never]
@ -1596,10 +1601,10 @@ describe User do
SiteSetting.default_topics_automatic_unpin = false SiteSetting.default_topics_automatic_unpin = false
SiteSetting.default_categories_watching = "1" SiteSetting.default_categories_watching = category0.id.to_s
SiteSetting.default_categories_tracking = "2" SiteSetting.default_categories_tracking = category1.id.to_s
SiteSetting.default_categories_muted = "3" SiteSetting.default_categories_muted = category2.id.to_s
SiteSetting.default_categories_watching_first_post = "4" SiteSetting.default_categories_watching_first_post = category3.id.to_s
end end
it "has overriden preferences" do it "has overriden preferences" do
@ -1617,10 +1622,10 @@ describe User do
expect(options.auto_track_topics_after_msecs).to eq(0) expect(options.auto_track_topics_after_msecs).to eq(0)
expect(options.notification_level_when_replying).to eq(3) expect(options.notification_level_when_replying).to eq(3)
expect(CategoryUser.lookup(user, :watching).pluck(:category_id)).to eq([1]) expect(CategoryUser.lookup(user, :watching).pluck(:category_id)).to eq([category0.id])
expect(CategoryUser.lookup(user, :tracking).pluck(:category_id)).to eq([2]) expect(CategoryUser.lookup(user, :tracking).pluck(:category_id)).to eq([category1.id])
expect(CategoryUser.lookup(user, :muted).pluck(:category_id)).to eq([3]) expect(CategoryUser.lookup(user, :muted).pluck(:category_id)).to eq([category2.id])
expect(CategoryUser.lookup(user, :watching_first_post).pluck(:category_id)).to eq([4]) expect(CategoryUser.lookup(user, :watching_first_post).pluck(:category_id)).to eq([category3.id])
end end
it "does not set category preferences for staged users" do it "does not set category preferences for staged users" do