Merge tag 'v4.1.11' into chinwag-4.1

This commit is contained in:
Mike Barnes 2023-12-18 19:21:59 +11:00
commit fc58fc207f
22 changed files with 225 additions and 47 deletions

View file

@ -3,6 +3,23 @@ Changelog
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [4.1.11] - 2023-12-04
### Changed
- Change GIF max matrix size error to explicitly mention GIF files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27927))
- Change `Follow` activities delivery to bypass availability check ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/27586))
- Change Content-Security-Policy to be tighter on media paths ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26889))
### Fixed
- Fix incoming status creation date not being restricted to standard ISO8601 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27655), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28081))
- Fix posts from force-sensitized accounts being able to trend ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27620))
- Fix processing LDSigned activities from actors with unknown public keys ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27474))
- Fix error and incorrect URLs in `/api/v1/accounts/:id/featured_tags` for remote accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27459))
- Fix report processing notice not mentioning the report number when performing a custom action ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27442))
- Fix some link anchors being recognized as hashtags ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27271), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27584))
## [4.1.10] - 2023-10-10 ## [4.1.10] - 2023-10-10
### Changed ### Changed

View file

@ -10,9 +10,10 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
## Supported Versions ## Supported Versions
| Version | Supported | | Version | Supported |
| ------- | ------------------ | | ------- | ---------------- |
| 4.1.x | Yes | | 4.2.x | Yes |
| 4.0.x | Until 2023-10-31 | | 4.1.x | Yes |
| 3.5.x | Until 2023-12-31 | | 4.0.x | No |
| < 3.5 | No | | 3.5.x | Until 2023-12-31 |
| < 3.5 | No |

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class AccountsIndex < Chewy::Index class AccountsIndex < Chewy::Index
include DatetimeClampingConcern
settings index: { refresh_interval: '30s' }, analysis: { settings index: { refresh_interval: '30s' }, analysis: {
analyzer: { analyzer: {
content: { content: {
@ -38,6 +40,6 @@ class AccountsIndex < Chewy::Index
field :following_count, type: 'long', value: ->(account) { account.following_count } field :following_count, type: 'long', value: ->(account) { account.following_count }
field :followers_count, type: 'long', value: ->(account) { account.followers_count } field :followers_count, type: 'long', value: ->(account) { account.followers_count }
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at } field :last_status_at, type: 'date', value: ->(account) { clamp_date(account.last_status_at || account.created_at) }
end end
end end

View file

@ -0,0 +1,14 @@
# frozen_string_literal: true
module DatetimeClampingConcern
extend ActiveSupport::Concern
MIN_ISO8601_DATETIME = '0000-01-01T00:00:00Z'.to_datetime.freeze
MAX_ISO8601_DATETIME = '9999-12-31T23:59:59Z'.to_datetime.freeze
class_methods do
def clamp_date(datetime)
datetime.clamp(MIN_ISO8601_DATETIME, MAX_ISO8601_DATETIME)
end
end
end

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class TagsIndex < Chewy::Index class TagsIndex < Chewy::Index
include DatetimeClampingConcern
settings index: { refresh_interval: '30s' }, analysis: { settings index: { refresh_interval: '30s' }, analysis: {
analyzer: { analyzer: {
content: { content: {
@ -36,6 +38,6 @@ class TagsIndex < Chewy::Index
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? } field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
field :usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts } field :usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts }
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at } field :last_status_at, type: 'date', value: ->(tag) { clamp_date(tag.last_status_at || tag.created_at) }
end end
end end

View file

@ -21,7 +21,7 @@ module Admin
account_action.save! account_action.save!
if account_action.with_report? if account_action.with_report?
redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: params[:report_id]) redirect_to admin_reports_path, notice: I18n.t('admin.reports.processed_msg', id: resource_params[:report_id])
else else
redirect_to admin_account_path(@account.id) redirect_to admin_account_path(@account.id)
end end

View file

@ -18,8 +18,8 @@ class ActivityPub::LinkedDataSignature
return unless type == 'RsaSignature2017' return unless type == 'RsaSignature2017'
creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri) creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri)
creator ||= ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false) creator = ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false) if creator&.public_key.blank?
return if creator.nil? return if creator.nil?
@ -27,9 +27,9 @@ class ActivityPub::LinkedDataSignature
document_hash = hash(@json.without('signature')) document_hash = hash(@json.without('signature'))
to_be_verified = options_hash + document_hash to_be_verified = options_hash + document_hash
if creator.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), Base64.decode64(signature), to_be_verified) creator if creator.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), Base64.decode64(signature), to_be_verified)
creator rescue OpenSSL::PKey::RSAError
end false
end end
def sign!(creator, sign_with: nil) def sign!(creator, sign_with: nil)

View file

@ -53,7 +53,8 @@ class ActivityPub::Parser::StatusParser
end end
def created_at def created_at
@object['published']&.to_datetime datetime = @object['published']&.to_datetime
datetime if datetime.present? && (0..9999).cover?(datetime.year)
rescue ArgumentError rescue ArgumentError
nil nil
end end

View file

@ -52,9 +52,13 @@ module Attachmentable
return if attachment.blank? || !/image.*/.match?(attachment.content_type) || attachment.queued_for_write[:original].blank? return if attachment.blank? || !/image.*/.match?(attachment.content_type) || attachment.queued_for_write[:original].blank?
width, height = FastImage.size(attachment.queued_for_write[:original].path) width, height = FastImage.size(attachment.queued_for_write[:original].path)
matrix_limit = attachment.content_type == 'image/gif' ? GIF_MATRIX_LIMIT : MAX_MATRIX_LIMIT return unless width.present? && height.present?
raise Mastodon::DimensionsValidationError, "#{width}x#{height} images are not supported" if width.present? && height.present? && (width * height > matrix_limit) if attachment.content_type == 'image/gif' && width * height > GIF_MATRIX_LIMIT
raise Mastodon::DimensionsValidationError, "#{width}x#{height} GIF files are not supported"
elsif width * height > MAX_MATRIX_LIMIT
raise Mastodon::DimensionsValidationError, "#{width}x#{height} images are not supported"
end
end end
def appropriate_extension(attachment) def appropriate_extension(attachment)

View file

@ -33,7 +33,7 @@ class Tag < ApplicationRecord
HASTAG_LAST_SEQUENCE = '([[:word:]_]*[[:alpha:]][[:word:]_]*)' HASTAG_LAST_SEQUENCE = '([[:word:]_]*[[:alpha:]][[:word:]_]*)'
HASHTAG_NAME_PAT = "#{HASHTAG_FIRST_SEQUENCE}|#{HASTAG_LAST_SEQUENCE}" HASHTAG_NAME_PAT = "#{HASHTAG_FIRST_SEQUENCE}|#{HASTAG_LAST_SEQUENCE}"
HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_PAT})/i HASHTAG_RE = %r{(?<![=/)\w])#(#{HASHTAG_NAME_PAT})}i
HASHTAG_NAME_RE = /\A(#{HASHTAG_NAME_PAT})\z/i HASHTAG_NAME_RE = /\A(#{HASHTAG_NAME_PAT})\z/i
HASHTAG_INVALID_CHARS_RE = /[^[:alnum:]#{HASHTAG_SEPARATORS}]/ HASHTAG_INVALID_CHARS_RE = /[^[:alnum:]#{HASHTAG_SEPARATORS}]/

View file

@ -91,7 +91,7 @@ class Trends::Statuses < Trends::Base
private private
def eligible?(status) def eligible?(status)
status.public_visibility? && status.account.discoverable? && !status.account.silenced? && status.spoiler_text.blank? && !status.sensitive? && !status.reply? && valid_locale?(status.language) status.public_visibility? && status.account.discoverable? && !status.account.silenced? && !status.account.sensitized? && status.spoiler_text.blank? && !status.sensitive? && !status.reply? && valid_locale?(status.language)
end end
def calculate_scores(statuses, at_time) def calculate_scores(statuses, at_time)

View file

@ -10,7 +10,9 @@ class REST::FeaturedTagSerializer < ActiveModel::Serializer
end end
def url def url
short_account_tag_url(object.account, object.tag) # The path is hardcoded because we have to deal with both local and
# remote users, which are different routes
account_with_domain_url(object.account, "tagged/#{object.tag.to_param}")
end end
def name def name

View file

@ -71,7 +71,7 @@ class FollowService < BaseService
if @target_account.local? if @target_account.local?
LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, 'follow_request') LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, 'follow_request')
elsif @target_account.activitypub? elsif @target_account.activitypub?
ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url) ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url, { 'bypass_availability' => true })
end end
follow_request follow_request

View file

@ -23,9 +23,10 @@ class ActivityPub::DeliveryWorker
HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze
def perform(json, source_account_id, inbox_url, options = {}) def perform(json, source_account_id, inbox_url, options = {})
return unless DeliveryFailureTracker.available?(inbox_url)
@options = options.with_indifferent_access @options = options.with_indifferent_access
return unless @options[:bypass_availability] || DeliveryFailureTracker.available?(inbox_url)
@json = json @json = json
@source_account = Account.find(source_account_id) @source_account = Account.find(source_account_id)
@inbox_url = inbox_url @inbox_url = inbox_url

View file

@ -3,7 +3,11 @@
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
def host_to_url(str) def host_to_url(str)
"http#{Rails.configuration.x.use_https ? 's' : ''}://#{str.split('/').first}" if str.present? return if str.blank?
uri = Addressable::URI.parse("http#{Rails.configuration.x.use_https ? 's' : ''}://#{str}")
uri.path += '/' unless uri.path.blank? || uri.path.end_with?('/')
uri.to_s
end end
base_host = Rails.configuration.x.web_domain base_host = Rails.configuration.x.web_domain

View file

@ -126,7 +126,7 @@ Rails.application.routes.draw do
get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status
end end
get '/@:username_with_domain/(*any)', to: 'home#index', constraints: { username_with_domain: /([^\/])+?/ }, format: false get '/@:username_with_domain/(*any)', to: 'home#index', constraints: { username_with_domain: %r{([^/])+?} }, as: :account_with_domain, format: false
get '/settings', to: redirect('/settings/profile') get '/settings', to: redirect('/settings/profile')
namespace :settings do namespace :settings do

View file

@ -56,7 +56,7 @@ services:
web: web:
build: . build: .
image: ghcr.io/mastodon/mastodon:v4.1.10 image: ghcr.io/mastodon/mastodon:v4.1.11
restart: always restart: always
env_file: .env.production env_file: .env.production
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000" command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
@ -77,7 +77,7 @@ services:
streaming: streaming:
build: . build: .
image: ghcr.io/mastodon/mastodon:v4.1.10 image: ghcr.io/mastodon/mastodon:v4.1.11
restart: always restart: always
env_file: .env.production env_file: .env.production
command: node ./streaming command: node ./streaming
@ -95,7 +95,7 @@ services:
sidekiq: sidekiq:
build: . build: .
image: ghcr.io/mastodon/mastodon:v4.1.10 image: ghcr.io/mastodon/mastodon:v4.1.11
restart: always restart: always
env_file: .env.production env_file: .env.production
command: bundle exec sidekiq command: bundle exec sidekiq

View file

@ -13,7 +13,7 @@ module Mastodon
end end
def patch def patch
10 11
end end
def flags def flags

View file

@ -29,7 +29,47 @@ RSpec.describe ActivityPub::Activity::Create do
subject.perform subject.perform
end end
context 'object has been edited' do context 'when object publication date is below ISO8601 range' do
let(:object_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
published: '-0977-11-03T08:31:22Z',
}
end
it 'creates status with a valid creation date', :aggregate_failures do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.text).to eq 'Lorem ipsum'
expect(status.created_at).to be_within(30).of(Time.now.utc)
end
end
context 'when object publication date is above ISO8601 range' do
let(:object_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
published: '10000-11-03T08:31:22Z',
}
end
it 'creates status with a valid creation date', :aggregate_failures do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.text).to eq 'Lorem ipsum'
expect(status.created_at).to be_within(30).of(Time.now.utc)
end
end
context 'when object has been edited' do
let(:object_json) do let(:object_json) do
{ {
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
@ -40,18 +80,16 @@ RSpec.describe ActivityPub::Activity::Create do
} }
end end
it 'creates status' do it 'creates status with appropriate creation and edition dates', :aggregate_failures do
status = sender.statuses.first status = sender.statuses.first
expect(status).to_not be_nil expect(status).to_not be_nil
expect(status.text).to eq 'Lorem ipsum' expect(status.text).to eq 'Lorem ipsum'
end
it 'marks status as edited' do expect(status.created_at).to eq '2022-01-22T15:00:00Z'.to_datetime
status = sender.statuses.first
expect(status).to_not be_nil expect(status.edited?).to be true
expect(status.edited?).to eq true expect(status.edited_at).to eq '2022-01-22T16:00:00Z'.to_datetime
end end
end end

View file

@ -36,6 +36,40 @@ RSpec.describe ActivityPub::LinkedDataSignature do
end end
end end
context 'when local account record is missing a public key' do
let(:raw_signature) do
{
'creator' => 'http://example.com/alice',
'created' => '2017-09-23T20:21:34Z',
}
end
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => sign(sender, raw_signature, raw_json)) }
let(:service_stub) { instance_double(ActivityPub::FetchRemoteKeyService) }
before do
# Ensure signature is computed with the old key
signature
# Unset key
old_key = sender.public_key
sender.update!(private_key: '', public_key: '')
allow(ActivityPub::FetchRemoteKeyService).to receive(:new).and_return(service_stub)
allow(service_stub).to receive(:call).with('http://example.com/alice', id: false) do
sender.update!(public_key: old_key)
sender
end
end
it 'fetches key and returns creator' do
expect(subject.verify_actor!).to eq sender
expect(service_stub).to have_received(:call).with('http://example.com/alice', id: false).once
end
end
context 'when signature is missing' do context 'when signature is missing' do
let(:signature) { nil } let(:signature) { nil }

View file

@ -31,44 +31,52 @@ RSpec.describe Tag do
expect(subject.match('https://en.wikipedia.org/wiki/Ghostbusters_(song)#Lawsuit')).to be_nil expect(subject.match('https://en.wikipedia.org/wiki/Ghostbusters_(song)#Lawsuit')).to be_nil
end end
it 'does not match URLs with hashtag-like anchors after a numeral' do
expect(subject.match('https://gcc.gnu.org/bugzilla/show_bug.cgi?id=111895#c4')).to be_nil
end
it 'does not match URLs with hashtag-like anchors after an empty query parameter' do
expect(subject.match('https://en.wikipedia.org/wiki/Ghostbusters_(song)?foo=#Lawsuit')).to be_nil
end
it 'matches #' do it 'matches #' do
expect(subject.match('this is #').to_s).to eq ' #' expect(subject.match('this is #').to_s).to eq '#'
end end
it 'matches digits at the start' do it 'matches digits at the start' do
expect(subject.match('hello #3d').to_s).to eq ' #3d' expect(subject.match('hello #3d').to_s).to eq '#3d'
end end
it 'matches digits in the middle' do it 'matches digits in the middle' do
expect(subject.match('hello #l33ts35k').to_s).to eq ' #l33ts35k' expect(subject.match('hello #l33ts35k').to_s).to eq '#l33ts35k'
end end
it 'matches digits at the end' do it 'matches digits at the end' do
expect(subject.match('hello #world2016').to_s).to eq ' #world2016' expect(subject.match('hello #world2016').to_s).to eq '#world2016'
end end
it 'matches underscores at the beginning' do it 'matches underscores at the beginning' do
expect(subject.match('hello #_test').to_s).to eq ' #_test' expect(subject.match('hello #_test').to_s).to eq '#_test'
end end
it 'matches underscores at the end' do it 'matches underscores at the end' do
expect(subject.match('hello #test_').to_s).to eq ' #test_' expect(subject.match('hello #test_').to_s).to eq '#test_'
end end
it 'matches underscores in the middle' do it 'matches underscores in the middle' do
expect(subject.match('hello #one_two_three').to_s).to eq ' #one_two_three' expect(subject.match('hello #one_two_three').to_s).to eq '#one_two_three'
end end
it 'matches middle dots' do it 'matches middle dots' do
expect(subject.match('hello #one·two·three').to_s).to eq ' #one·two·three' expect(subject.match('hello #one·two·three').to_s).to eq '#one·two·three'
end end
it 'matches ・unicode in ぼっち・ざ・ろっく correctly' do it 'matches ・unicode in ぼっち・ざ・ろっく correctly' do
expect(subject.match('testing #ぼっち・ざ・ろっく').to_s).to eq ' #ぼっち・ざ・ろっく' expect(subject.match('testing #ぼっち・ざ・ろっく').to_s).to eq '#ぼっち・ざ・ろっく'
end end
it 'matches ZWNJ' do it 'matches ZWNJ' do
expect(subject.match('just add #نرم‌افزار and').to_s).to eq ' #نرم‌افزار' expect(subject.match('just add #نرم‌افزار and').to_s).to eq '#نرم‌افزار'
end end
it 'does not match middle dots at the start' do it 'does not match middle dots at the start' do
@ -76,7 +84,7 @@ RSpec.describe Tag do
end end
it 'does not match middle dots at the end' do it 'does not match middle dots at the end' do
expect(subject.match('hello #one·two·three·').to_s).to eq ' #one·two·three' expect(subject.match('hello #one·two·three·').to_s).to eq '#one·two·three'
end end
it 'does not match purely-numeric hashtags' do it 'does not match purely-numeric hashtags' do

View file

@ -0,0 +1,50 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'account featured tags API' do
let(:user) { Fabricate(:user) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
let(:scopes) { 'read:accounts' }
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
let(:account) { Fabricate(:account) }
describe 'GET /api/v1/accounts/:id/featured_tags' do
subject do
get "/api/v1/accounts/#{account.id}/featured_tags", headers: headers
end
before do
account.featured_tags.create!(name: 'foo')
account.featured_tags.create!(name: 'bar')
end
it 'returns the expected tags', :aggregate_failures do
subject
expect(response).to have_http_status(200)
expect(body_as_json).to contain_exactly(a_hash_including({
name: 'bar',
url: "https://cb6e6126.ngrok.io/@#{account.username}/tagged/bar",
}), a_hash_including({
name: 'foo',
url: "https://cb6e6126.ngrok.io/@#{account.username}/tagged/foo",
}))
end
context 'when the account is remote' do
it 'returns the expected tags', :aggregate_failures do
subject
expect(response).to have_http_status(200)
expect(body_as_json).to contain_exactly(a_hash_including({
name: 'bar',
url: "https://cb6e6126.ngrok.io/@#{account.pretty_acct}/tagged/bar",
}), a_hash_including({
name: 'foo',
url: "https://cb6e6126.ngrok.io/@#{account.pretty_acct}/tagged/foo",
}))
end
end
end
end