Fix banned text being able to be circumvented via unicode (#35978)

This commit is contained in:
Eugen Rochko 2025-09-02 09:29:03 +02:00 committed by GitHub
parent 6e09dd10a7
commit 7fc884ba00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 55 additions and 24 deletions

View File

@ -32,26 +32,30 @@ class Antispam
end end
end end
def local_preflight_check!(status) def initialize(status)
return unless considered_spam?(status) @status = status
end
report_if_needed!(status.account) def local_preflight_check!
return unless considered_spam?
raise SilentlyDrop, status report_if_needed!
raise SilentlyDrop, @status
end end
private private
def considered_spam?(status) def considered_spam?
(all_time_suspicious?(status) || recent_suspicious?(status)) && suspicious_reply_or_mention?(status) (all_time_suspicious? || recent_suspicious?) && suspicious_reply_or_mention?
end end
def all_time_suspicious?(status) def all_time_suspicious?
all_time_spammy_texts.any? { |spammy_text| status.text.include?(spammy_text) } all_time_spammy_texts.any? { |spammy_text| status_text.include?(spammy_text) }
end end
def recent_suspicious?(status) def recent_suspicious?
status.account.created_at >= ACCOUNT_AGE_EXEMPTION.ago && spammy_texts.any? { |spammy_text| status.text.include?(spammy_text) } @status.account.created_at >= ACCOUNT_AGE_EXEMPTION.ago && spammy_texts.any? { |spammy_text| status_text.include?(spammy_text) }
end end
def spammy_texts def spammy_texts
@ -62,25 +66,26 @@ class Antispam
redis.smembers('antispam:all_time_spammy_texts') redis.smembers('antispam:all_time_spammy_texts')
end end
def suspicious_reply_or_mention?(status) def suspicious_reply_or_mention?
parent = status.thread account_ids = ([@status.in_reply_to_account_id] + @status.mentions.map(&:account_id)).uniq
return true if parent.present? && !Follow.exists?(account_id: parent.account_id, target_account: status.account_id) !Follow.exists?(account_id: account_ids, target_account_id: @status.account.id)
account_ids = status.mentions.map(&:account_id).uniq
!Follow.exists?(account_id: account_ids, target_account_id: status.account.id)
end end
def report_if_needed!(account) def report_if_needed!
return if system_reports.unresolved.exists?(target_account: account) return if system_reports.unresolved.exists?(target_account: @status.account)
system_reports.create!( system_reports.create!(
category: :spam, category: :spam,
comment: 'Account automatically reported for posting a banned URL', comment: 'Account automatically reported for posting a banned URL',
target_account: account target_account: @status.account
) )
end end
def system_reports def system_reports
Account.representative.reports Account.representative.reports
end end
def status_text
@status_text ||= @status.text.unicode_normalize(:nfkc).downcase
end
end end

View File

@ -39,8 +39,6 @@ class PostStatusService < BaseService
@in_reply_to = @options[:thread] @in_reply_to = @options[:thread]
@quoted_status = @options[:quoted_status] @quoted_status = @options[:quoted_status]
@antispam = Antispam.new
return idempotency_duplicate if idempotency_given? && idempotency_duplicate? return idempotency_duplicate if idempotency_given? && idempotency_duplicate?
validate_media! validate_media!
@ -82,7 +80,9 @@ class PostStatusService < BaseService
process_mentions_service.call(@status, save_records: false) process_mentions_service.call(@status, save_records: false)
safeguard_mentions!(@status) safeguard_mentions!(@status)
attach_quote!(@status) attach_quote!(@status)
@antispam.local_preflight_check!(@status)
antispam = Antispam.new(@status)
antispam.local_preflight_check!
# The following transaction block is needed to wrap the UPDATEs to # The following transaction block is needed to wrap the UPDATEs to
# the media attachments when the status is created # the media attachments when the status is created
@ -113,7 +113,9 @@ class PostStatusService < BaseService
def schedule_status! def schedule_status!
status_for_validation = @account.statuses.build(status_attributes) status_for_validation = @account.statuses.build(status_attributes)
@antispam.local_preflight_check!(status_for_validation)
antispam = Antispam.new(status_for_validation)
antispam.local_preflight_check!
if status_for_validation.valid? if status_for_validation.valid?
# Marking the status as destroyed is necessary to prevent the status from being # Marking the status as destroyed is necessary to prevent the status from being

View File

@ -4,7 +4,7 @@ require 'rails_helper'
RSpec.describe Antispam do RSpec.describe Antispam do
describe '#local_preflight_check!' do describe '#local_preflight_check!' do
subject { described_class.new.local_preflight_check!(status) } subject { described_class.new(status).local_preflight_check! }
let(:status) { Fabricate :status } let(:status) { Fabricate :status }
@ -39,6 +39,30 @@ RSpec.describe Antispam do
end end
end end
context 'when status matches unicode variants' do
let(:status) { Fabricate :status, text: 'I use https://𝐛𝐚𝐧𝐧𝐞𝐝.𝐞𝐱𝐚𝐦𝐩𝐥𝐞 urls in my text' }
it 'raises error and reports' do
expect { subject }
.to raise_error(described_class::SilentlyDrop)
.and change(spam_reports, :count).by(1)
end
context 'when report already exists' do
before { Fabricate :report, account: Account.representative, target_account: status.account }
it 'raises error and does not report' do
expect { subject }
.to raise_error(described_class::SilentlyDrop)
.and not_change(spam_reports, :count)
end
end
def spam_reports
Account.representative.reports.where(target_account: status.account).spam
end
end
context 'when status does not match' do context 'when status does not match' do
it { is_expected.to be_nil } it { is_expected.to be_nil }
end end