mirror of
https://github.com/mastodon/mastodon.git
synced 2025-07-12 15:33:14 +00:00
Compare commits
5 Commits
0a604dcea1
...
07c9accec8
Author | SHA1 | Date | |
---|---|---|---|
![]() |
07c9accec8 | ||
![]() |
b23596109f | ||
![]() |
75707d05a9 | ||
![]() |
c1f8763e9b | ||
![]() |
129dd5bd9c |
9
app/controllers/admin/settings/protections_controller.rb
Normal file
9
app/controllers/admin/settings/protections_controller.rb
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Admin::Settings::ProtectionsController < Admin::SettingsController
|
||||||
|
private
|
||||||
|
|
||||||
|
def after_update_redirect_path
|
||||||
|
admin_settings_protections_path
|
||||||
|
end
|
||||||
|
end
|
1
app/javascript/material-icons/400-24px/security.svg
Normal file
1
app/javascript/material-icons/400-24px/security.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-80q-139-35-229.5-159.5T160-516v-244l320-120 320 120v244q0 152-90.5 276.5T480-80Zm0-84q97-30 162-118.5T718-480H480v-315l-240 90v207q0 7 2 18h238v316Z"/></svg>
|
After Width: | Height: | Size: 259 B |
|
@ -14,7 +14,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
private
|
private
|
||||||
|
|
||||||
def create_status
|
def create_status
|
||||||
return reject_payload! if unsupported_object_type? || non_matching_uri_hosts?(@account.uri, object_uri) || tombstone_exists? || !related_to_local_activity?
|
return reject_payload! if unsupported_object_type? || non_matching_uri_hosts?(@account.uri, object_uri) || tombstone_exists? || !related_to_local_activity? || reject_pattern?
|
||||||
|
|
||||||
with_redis_lock("create:#{object_uri}") do
|
with_redis_lock("create:#{object_uri}") do
|
||||||
return if delete_arrived_first?(object_uri) || poll_vote?
|
return if delete_arrived_first?(object_uri) || poll_vote?
|
||||||
|
@ -450,6 +450,10 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||||
Tombstone.exists?(uri: object_uri)
|
Tombstone.exists?(uri: object_uri)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reject_pattern?
|
||||||
|
Setting.reject_pattern.present? && @object['content']&.match?(Setting.reject_pattern)
|
||||||
|
end
|
||||||
|
|
||||||
def forward_for_reply
|
def forward_for_reply
|
||||||
return unless @status.distributable? && @json['signature'].present? && reply_to_local?
|
return unless @status.distributable? && @json['signature'].present? && reply_to_local?
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,7 @@ class Form::AdminSettings
|
||||||
app_icon
|
app_icon
|
||||||
favicon
|
favicon
|
||||||
min_age
|
min_age
|
||||||
|
reject_pattern
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
INTEGER_KEYS = %i(
|
INTEGER_KEYS = %i(
|
||||||
|
@ -94,6 +95,7 @@ class Form::AdminSettings
|
||||||
validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) }
|
validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) }
|
||||||
validates :min_age, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@min_age) }
|
validates :min_age, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@min_age) }
|
||||||
validates :site_short_description, length: { maximum: DESCRIPTION_LIMIT }, if: -> { defined?(@site_short_description) }
|
validates :site_short_description, length: { maximum: DESCRIPTION_LIMIT }, if: -> { defined?(@site_short_description) }
|
||||||
|
validates :reject_pattern, regexp_syntax: true, if: -> { defined?(@reject_pattern) }
|
||||||
validates :status_page_url, url: true, allow_blank: true
|
validates :status_page_url, url: true, allow_blank: true
|
||||||
validate :validate_site_uploads
|
validate :validate_site_uploads
|
||||||
|
|
||||||
|
|
11
app/validators/regexp_syntax_validator.rb
Normal file
11
app/validators/regexp_syntax_validator.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class RegexpSyntaxValidator < ActiveModel::EachValidator
|
||||||
|
def validate_each(record, attribute, value)
|
||||||
|
return if value.blank?
|
||||||
|
|
||||||
|
Regexp.compile(value)
|
||||||
|
rescue RegexpError => e
|
||||||
|
record.errors.add(attribute, I18n.t('applications.invalid_regexp', message: e.message))
|
||||||
|
end
|
||||||
|
end
|
22
app/views/admin/settings/protections/show.html.haml
Normal file
22
app/views/admin/settings/protections/show.html.haml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
- content_for :page_title do
|
||||||
|
= t('admin.settings.protections.title')
|
||||||
|
|
||||||
|
- content_for :heading do
|
||||||
|
%h2= t('admin.settings.title')
|
||||||
|
= render partial: 'admin/settings/shared/links'
|
||||||
|
|
||||||
|
= simple_form_for @admin_settings, url: admin_settings_protections_path, html: { method: :patch } do |f|
|
||||||
|
= render 'shared/error_messages', object: @admin_settings
|
||||||
|
|
||||||
|
%p.lead= t('admin.settings.protections.preamble')
|
||||||
|
|
||||||
|
%h4= t('admin.settings.protections.activitypub')
|
||||||
|
|
||||||
|
.fields-group
|
||||||
|
= f.input :reject_pattern,
|
||||||
|
as: :text,
|
||||||
|
input_html: { rows: 8 },
|
||||||
|
wrapper: :with_block_label
|
||||||
|
|
||||||
|
.actions
|
||||||
|
= f.button :button, t('generic.save_changes'), type: :submit
|
|
@ -7,3 +7,4 @@
|
||||||
primary.item :discovery, safe_join([material_symbol('search'), t('admin.settings.discovery.title')]), admin_settings_discovery_path
|
primary.item :discovery, safe_join([material_symbol('search'), t('admin.settings.discovery.title')]), admin_settings_discovery_path
|
||||||
primary.item :content_retention, safe_join([material_symbol('history'), t('admin.settings.content_retention.title')]), admin_settings_content_retention_path
|
primary.item :content_retention, safe_join([material_symbol('history'), t('admin.settings.content_retention.title')]), admin_settings_content_retention_path
|
||||||
primary.item :appearance, safe_join([material_symbol('computer'), t('admin.settings.appearance.title')]), admin_settings_appearance_path
|
primary.item :appearance, safe_join([material_symbol('computer'), t('admin.settings.appearance.title')]), admin_settings_appearance_path
|
||||||
|
primary.item :protections, safe_join([material_symbol('security'), t('admin.settings.protections.title')]), admin_settings_protections_path
|
||||||
|
|
|
@ -842,6 +842,10 @@ en:
|
||||||
all: To everyone
|
all: To everyone
|
||||||
disabled: To no one
|
disabled: To no one
|
||||||
users: To logged-in local users
|
users: To logged-in local users
|
||||||
|
protections:
|
||||||
|
activitypub: ActivityPub
|
||||||
|
preamble: Settings to protect your server
|
||||||
|
title: Protection
|
||||||
registrations:
|
registrations:
|
||||||
moderation_recommandation: Please make sure you have an adequate and reactive moderation team before you open registrations to everyone!
|
moderation_recommandation: Please make sure you have an adequate and reactive moderation team before you open registrations to everyone!
|
||||||
preamble: Control who can create an account on your server.
|
preamble: Control who can create an account on your server.
|
||||||
|
@ -1178,6 +1182,7 @@ en:
|
||||||
applications:
|
applications:
|
||||||
created: Application successfully created
|
created: Application successfully created
|
||||||
destroyed: Application successfully deleted
|
destroyed: Application successfully deleted
|
||||||
|
invalid_regexp: 'The provided Regexp is invalid: %{message}'
|
||||||
logout: Logout
|
logout: Logout
|
||||||
regenerate_token: Regenerate access token
|
regenerate_token: Regenerate access token
|
||||||
token_regenerated: Access token successfully regenerated
|
token_regenerated: Access token successfully regenerated
|
||||||
|
|
|
@ -94,6 +94,7 @@ en:
|
||||||
min_age: Users will be asked to confirm their date of birth during sign-up
|
min_age: Users will be asked to confirm their date of birth during sign-up
|
||||||
peers_api_enabled: A list of domain names this server has encountered in the fediverse. No data is included here about whether you federate with a given server, just that your server knows about it. This is used by services that collect statistics on federation in a general sense.
|
peers_api_enabled: A list of domain names this server has encountered in the fediverse. No data is included here about whether you federate with a given server, just that your server knows about it. This is used by services that collect statistics on federation in a general sense.
|
||||||
profile_directory: The profile directory lists all users who have opted-in to be discoverable.
|
profile_directory: The profile directory lists all users who have opted-in to be discoverable.
|
||||||
|
reject_pattern: Set a regular expression pattern to inspect Create Activity content, and refuse Activity if you match
|
||||||
require_invite_text: When sign-ups require manual approval, make the “Why do you want to join?” text input mandatory rather than optional
|
require_invite_text: When sign-ups require manual approval, make the “Why do you want to join?” text input mandatory rather than optional
|
||||||
site_contact_email: How people can reach you for legal or support inquiries.
|
site_contact_email: How people can reach you for legal or support inquiries.
|
||||||
site_contact_username: How people can reach you on Mastodon.
|
site_contact_username: How people can reach you on Mastodon.
|
||||||
|
@ -285,6 +286,7 @@ en:
|
||||||
peers_api_enabled: Publish list of discovered servers in the API
|
peers_api_enabled: Publish list of discovered servers in the API
|
||||||
profile_directory: Enable profile directory
|
profile_directory: Enable profile directory
|
||||||
registrations_mode: Who can sign-up
|
registrations_mode: Who can sign-up
|
||||||
|
reject_pattern: Reject Pattern
|
||||||
require_invite_text: Require a reason to join
|
require_invite_text: Require a reason to join
|
||||||
show_domain_blocks: Show domain blocks
|
show_domain_blocks: Show domain blocks
|
||||||
show_domain_blocks_rationale: Show why domains were blocked
|
show_domain_blocks_rationale: Show why domains were blocked
|
||||||
|
|
|
@ -68,6 +68,7 @@ namespace :admin do
|
||||||
resource :about, only: [:show, :update], controller: 'about'
|
resource :about, only: [:show, :update], controller: 'about'
|
||||||
resource :appearance, only: [:show, :update], controller: 'appearance'
|
resource :appearance, only: [:show, :update], controller: 'appearance'
|
||||||
resource :discovery, only: [:show, :update], controller: 'discovery'
|
resource :discovery, only: [:show, :update], controller: 'discovery'
|
||||||
|
resource :protections, only: [:show, :update], controller: 'protections'
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :site_uploads, only: [:destroy]
|
resources :site_uploads, only: [:destroy]
|
||||||
|
|
|
@ -52,6 +52,7 @@ defaults: &defaults
|
||||||
backups_retention_period: 7
|
backups_retention_period: 7
|
||||||
captcha_enabled: false
|
captcha_enabled: false
|
||||||
allow_referer_origin: false
|
allow_referer_origin: false
|
||||||
|
reject_pattern: ''
|
||||||
|
|
||||||
development:
|
development:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
|
|
|
@ -26,6 +26,8 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||||
before do
|
before do
|
||||||
sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender))
|
sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender))
|
||||||
|
|
||||||
|
Setting.reject_pattern = 'spam'
|
||||||
|
|
||||||
stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt'))
|
stub_request(:get, 'http://example.com/attachment.png').to_return(request_fixture('avatar.txt'))
|
||||||
stub_request(:get, 'http://example.com/emoji.png').to_return(body: attachment_fixture('emojo.png'))
|
stub_request(:get, 'http://example.com/emoji.png').to_return(body: attachment_fixture('emojo.png'))
|
||||||
stub_request(:get, 'http://example.com/emojib.png').to_return(body: attachment_fixture('emojo.png'), headers: { 'Content-Type' => 'application/octet-stream' })
|
stub_request(:get, 'http://example.com/emojib.png').to_return(body: attachment_fixture('emojo.png'), headers: { 'Content-Type' => 'application/octet-stream' })
|
||||||
|
@ -95,6 +97,24 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
let(:spam_object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), 'post3'].join('/'),
|
||||||
|
type: 'Note',
|
||||||
|
to: [
|
||||||
|
'https://www.w3.org/ns/activitystreams#Public',
|
||||||
|
ActivityPub::TagManager.instance.uri_for(follower),
|
||||||
|
],
|
||||||
|
content: '@bob lorem ispam',
|
||||||
|
published: 1.hour.ago.utc.iso8601,
|
||||||
|
updated: 1.hour.ago.utc.iso8601,
|
||||||
|
tag: {
|
||||||
|
type: 'Mention',
|
||||||
|
href: ActivityPub::TagManager.instance.uri_for(follower),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def activity_for_object(json)
|
def activity_for_object(json)
|
||||||
{
|
{
|
||||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
@ -168,6 +188,17 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||||
# It has queued a mention resolve job
|
# It has queued a mention resolve job
|
||||||
expect(MentionResolveWorker).to have_enqueued_sidekiq_job(status.id, invalid_mention_json[:tag][:href], anything)
|
expect(MentionResolveWorker).to have_enqueued_sidekiq_job(status.id, invalid_mention_json[:tag][:href], anything)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'do not process posts that contain reject patterns', :aggregate_failures do
|
||||||
|
stub_request(:get, spam_object_json[:id]).to_return(status: 500)
|
||||||
|
described_class.new(activity_for_object(spam_object_json), sender, delivery: true).perform
|
||||||
|
|
||||||
|
# …it creates a status
|
||||||
|
status = Status.find_by(uri: spam_object_json[:id])
|
||||||
|
|
||||||
|
# Check the process did crash
|
||||||
|
expect(status.nil?).to be true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
|
|
25
spec/system/admin/settings/protection_spec.rb
Normal file
25
spec/system/admin/settings/protection_spec.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Admin::Settings::Protections' do
|
||||||
|
let(:admin_user) { Fabricate(:admin_user) }
|
||||||
|
|
||||||
|
before { sign_in(admin_user) }
|
||||||
|
|
||||||
|
it 'Saves changes to protections settings' do
|
||||||
|
visit admin_settings_protections_path
|
||||||
|
|
||||||
|
fill_in reject_pattern_field,
|
||||||
|
with: 'https://foo.bar'
|
||||||
|
|
||||||
|
click_on submit_button
|
||||||
|
|
||||||
|
expect(page)
|
||||||
|
.to have_content(success_message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def reject_pattern_field
|
||||||
|
form_label 'form_admin_settings.reject_pattern'
|
||||||
|
end
|
||||||
|
end
|
55
spec/validators/regexp_syntax_validator_spec.rb
Normal file
55
spec/validators/regexp_syntax_validator_spec.rb
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe RegexpSyntaxValidator do
|
||||||
|
let(:record_class) do
|
||||||
|
Class.new do
|
||||||
|
include ActiveModel::Validations
|
||||||
|
|
||||||
|
attr_accessor :text
|
||||||
|
|
||||||
|
validates :text, regexp_syntax: true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:record) { record_class.new }
|
||||||
|
|
||||||
|
describe '#validate_each' do
|
||||||
|
context 'with a nil value' do
|
||||||
|
it 'does not add errors' do
|
||||||
|
record.text = nil
|
||||||
|
|
||||||
|
expect(record).to be_valid
|
||||||
|
expect(record.errors).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a blank' do
|
||||||
|
it 'does not add errors' do
|
||||||
|
record.text = ''
|
||||||
|
|
||||||
|
expect(record).to be_valid
|
||||||
|
expect(record.errors).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with valid regexp' do
|
||||||
|
it 'does not add errors' do
|
||||||
|
record.text = 'spam|https://foo.bar'
|
||||||
|
|
||||||
|
expect(record).to be_valid
|
||||||
|
expect(record.errors).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with more lines than limit' do
|
||||||
|
it 'adds an error' do
|
||||||
|
record.text = '('
|
||||||
|
|
||||||
|
expect(record).to_not be_valid
|
||||||
|
expect(record.errors.where(:text)).to_not be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user