0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-05 17:25:34 +08:00
discourse/app/services/user_silencer.rb
Bannon Tanner 2625630482
SECURITY: Stale featured rows expose unlisted topic content (#41810)
## Summary

Prevent the exposure of unlisted topic metadata in the categories
response by enforcing live visibility checks on featured topics.
Additionally, the UserSilencer service now correctly removes featured
topic records when bulk-hiding topics from new users, ensuring stale
records do not bypass visibility constraints.

## Source

- Patch Triage: https://patch.discourse.org/patch-triage/1427

Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
2026-07-17 09:19:08 -05:00

128 lines
3.4 KiB
Ruby
Vendored

# frozen_string_literal: true
class UserSilencer
attr_reader :user_history
def initialize(user, by_user = nil, opts = {})
@user, @by_user, @opts = user, by_user, opts
end
def self.silence(user, by_user = nil, opts = {})
UserSilencer.new(user, by_user, opts).silence
end
def self.auto_silence(user, by_user = nil, opts = {})
UserSilencer.new(user, by_user, opts).auto_silence
end
def self.unsilence(user, by_user = nil, opts = {})
UserSilencer.new(user, by_user, opts).unsilence
end
def self.was_silenced_for?(post)
return false if post.blank?
UserHistory.where(action: UserHistory.actions[:silence_user], post: post).exists?
end
def silence
return false if @user.staff?
hide_posts unless @opts[:keep_posts]
return false if @user.silenced_till.present?
@user.silenced_till = @opts[:silenced_till] || 1000.years.from_now
if @user.save
message_type = @opts[:message] || :silenced_by_staff
details = StaffMessageFormat.new(:silence, @opts[:reason], @opts[:message_body]).format
context = "#{message_type}: #{@opts[:reason]}"
if @by_user
log_params = { context: context, details: details, reviewable_id: @opts[:reviewable_id] }
log_params[:post_id] = @opts[:post_id].to_i if @opts[:post_id]
@user_history = StaffActionLogger.new(@by_user).log_silence_user(@user, log_params)
end
silence_message_params = {}
DiscourseEvent.trigger(
:user_silenced,
user: @user,
silenced_by: @by_user,
reason: @opts[:reason],
message: @opts[:message_body],
user_history: @user_history,
post_id: @opts[:post_id],
silenced_till: @user.silenced_till,
silenced_at: DateTime.now,
silence_message_params: silence_message_params,
)
silence_message_params.merge!(post_alert_options: { skip_send_email: true })
SystemMessage.create(@user, message_type, silence_message_params)
true
end
end
def auto_silence
if silence
notify_moderators
true
else
false
end
end
def hide_posts
return unless @user.trust_level == TrustLevel[0]
Post
.where(user_id: @user.id)
.where("created_at > ?", 24.hours.ago)
.update_all(
[
"hidden = true, hidden_reason_id = COALESCE(hidden_reason_id, ?)",
Post.hidden_reasons[:new_user_spam_threshold_reached],
],
)
topic_ids =
Post
.where(user_id: @user.id, post_number: 1)
.where("created_at > ?", 24.hours.ago)
.pluck(:topic_id)
unless topic_ids.empty?
Topic.where(id: topic_ids).update_all(visible: false)
CategoryFeaturedTopic.where(topic_id: topic_ids).delete_all
end
end
def unsilence
@user.silenced_till = nil
if @user.save
DiscourseEvent.trigger(:user_unsilenced, user: @user, by_user: @by_user)
SystemMessage.create(@user, :unsilenced)
if @by_user
StaffActionLogger.new(@by_user).log_unsilence_user(
@user,
reviewable_id: @opts[:reviewable_id],
)
end
end
end
private
def notify_moderators
return if !SiteSetting.notify_mods_when_user_silenced
GroupMessage.create(
Group[:moderators].name,
@opts[:reason] ? :user_automatically_silenced_with_reason : :user_automatically_silenced,
user: @user,
limit_once_per: false,
message_params: {
reason: @opts[:reason],
},
)
end
end