diff --git a/CHANGELOG.md b/CHANGELOG.md index 54f68d56a..899a6c823 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,23 @@ Changelog 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 ### Changed diff --git a/SECURITY.md b/SECURITY.md index ebb05ba2c..78cf769be 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,9 +10,10 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through ## Supported Versions -| Version | Supported | -| ------- | ------------------ | -| 4.1.x | Yes | -| 4.0.x | Until 2023-10-31 | -| 3.5.x | Until 2023-12-31 | -| < 3.5 | No | +| Version | Supported | +| ------- | ---------------- | +| 4.2.x | Yes | +| 4.1.x | Yes | +| 4.0.x | No | +| 3.5.x | Until 2023-12-31 | +| < 3.5 | No | diff --git a/app/chewy/accounts_index.rb b/app/chewy/accounts_index.rb index e38e14a10..9b78af85d 100644 --- a/app/chewy/accounts_index.rb +++ b/app/chewy/accounts_index.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class AccountsIndex < Chewy::Index + include DatetimeClampingConcern + settings index: { refresh_interval: '30s' }, analysis: { analyzer: { content: { @@ -38,6 +40,6 @@ class AccountsIndex < Chewy::Index field :following_count, type: 'long', value: ->(account) { account.following_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 diff --git a/app/chewy/concerns/datetime_clamping_concern.rb b/app/chewy/concerns/datetime_clamping_concern.rb new file mode 100644 index 000000000..7f176b6e5 --- /dev/null +++ b/app/chewy/concerns/datetime_clamping_concern.rb @@ -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 diff --git a/app/chewy/tags_index.rb b/app/chewy/tags_index.rb index df3d9e4cc..8c778dc65 100644 --- a/app/chewy/tags_index.rb +++ b/app/chewy/tags_index.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class TagsIndex < Chewy::Index + include DatetimeClampingConcern + settings index: { refresh_interval: '30s' }, analysis: { analyzer: { content: { @@ -36,6 +38,6 @@ class TagsIndex < Chewy::Index field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? } 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 diff --git a/app/controllers/admin/account_actions_controller.rb b/app/controllers/admin/account_actions_controller.rb index e89404b60..e674bf55a 100644 --- a/app/controllers/admin/account_actions_controller.rb +++ b/app/controllers/admin/account_actions_controller.rb @@ -21,7 +21,7 @@ module Admin account_action.save! 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 redirect_to admin_account_path(@account.id) end diff --git a/app/lib/activitypub/linked_data_signature.rb b/app/lib/activitypub/linked_data_signature.rb index f90adaf6c..a2b8c6365 100644 --- a/app/lib/activitypub/linked_data_signature.rb +++ b/app/lib/activitypub/linked_data_signature.rb @@ -18,8 +18,8 @@ class ActivityPub::LinkedDataSignature return unless type == 'RsaSignature2017' - creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri) - creator ||= ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false) + creator = ActivityPub::TagManager.instance.uri_to_actor(creator_uri) + creator = ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false) if creator&.public_key.blank? return if creator.nil? @@ -27,9 +27,9 @@ class ActivityPub::LinkedDataSignature document_hash = hash(@json.without('signature')) to_be_verified = options_hash + document_hash - if creator.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), Base64.decode64(signature), to_be_verified) - creator - end + creator if creator.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), Base64.decode64(signature), to_be_verified) + rescue OpenSSL::PKey::RSAError + false end def sign!(creator, sign_with: nil) diff --git a/app/lib/activitypub/parser/status_parser.rb b/app/lib/activitypub/parser/status_parser.rb index 3ba154d01..45f5fc5bf 100644 --- a/app/lib/activitypub/parser/status_parser.rb +++ b/app/lib/activitypub/parser/status_parser.rb @@ -53,7 +53,8 @@ class ActivityPub::Parser::StatusParser end def created_at - @object['published']&.to_datetime + datetime = @object['published']&.to_datetime + datetime if datetime.present? && (0..9999).cover?(datetime.year) rescue ArgumentError nil end diff --git a/app/models/concerns/attachmentable.rb b/app/models/concerns/attachmentable.rb index 662b2ef52..a61c78dda 100644 --- a/app/models/concerns/attachmentable.rb +++ b/app/models/concerns/attachmentable.rb @@ -52,9 +52,13 @@ module Attachmentable 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) - 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 def appropriate_extension(attachment) diff --git a/app/models/tag.rb b/app/models/tag.rb index 47a05d00a..dcf8f9c78 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -33,7 +33,7 @@ class Tag < ApplicationRecord HASTAG_LAST_SEQUENCE = '([[:word:]_]*[[:alpha:]][[:word:]_]*)' HASHTAG_NAME_PAT = "#{HASHTAG_FIRST_SEQUENCE}|#{HASTAG_LAST_SEQUENCE}" - HASHTAG_RE = /(?:^|[^\/\)\w])#(#{HASHTAG_NAME_PAT})/i + HASHTAG_RE = %r{(? true }) end follow_request diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb index 7c1c14766..376c237a9 100644 --- a/app/workers/activitypub/delivery_worker.rb +++ b/app/workers/activitypub/delivery_worker.rb @@ -23,9 +23,10 @@ class ActivityPub::DeliveryWorker HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze def perform(json, source_account_id, inbox_url, options = {}) - return unless DeliveryFailureTracker.available?(inbox_url) - @options = options.with_indifferent_access + + return unless @options[:bypass_availability] || DeliveryFailureTracker.available?(inbox_url) + @json = json @source_account = Account.find(source_account_id) @inbox_url = inbox_url diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index ccce6d71e..d863fa6d4 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -3,7 +3,11 @@ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 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 base_host = Rails.configuration.x.web_domain diff --git a/config/routes.rb b/config/routes.rb index 49508dc39..64ed56845 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -126,7 +126,7 @@ Rails.application.routes.draw do get '/@:account_username/:id/embed', to: 'statuses#embed', as: :embed_short_account_status 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') namespace :settings do diff --git a/docker-compose.yml b/docker-compose.yml index 496eafdb8..877edcb4f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,7 +56,7 @@ services: web: build: . - image: ghcr.io/mastodon/mastodon:v4.1.10 + image: ghcr.io/mastodon/mastodon:v4.1.11 restart: always env_file: .env.production command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000" @@ -77,7 +77,7 @@ services: streaming: build: . - image: ghcr.io/mastodon/mastodon:v4.1.10 + image: ghcr.io/mastodon/mastodon:v4.1.11 restart: always env_file: .env.production command: node ./streaming @@ -95,7 +95,7 @@ services: sidekiq: build: . - image: ghcr.io/mastodon/mastodon:v4.1.10 + image: ghcr.io/mastodon/mastodon:v4.1.11 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 2d11ef4d2..466622a0c 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 10 + 11 end def flags diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 1a25395fa..378ba0cd1 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -29,7 +29,47 @@ RSpec.describe ActivityPub::Activity::Create do subject.perform 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 { id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, @@ -40,18 +80,16 @@ RSpec.describe ActivityPub::Activity::Create do } end - it 'creates status' do + it 'creates status with appropriate creation and edition dates', :aggregate_failures do status = sender.statuses.first expect(status).to_not be_nil expect(status.text).to eq 'Lorem ipsum' - end - it 'marks status as edited' do - status = sender.statuses.first + expect(status.created_at).to eq '2022-01-22T15:00:00Z'.to_datetime - expect(status).to_not be_nil - expect(status.edited?).to eq true + expect(status.edited?).to be true + expect(status.edited_at).to eq '2022-01-22T16:00:00Z'.to_datetime end end diff --git a/spec/lib/activitypub/linked_data_signature_spec.rb b/spec/lib/activitypub/linked_data_signature_spec.rb index d55a7c7fa..e6b16c98d 100644 --- a/spec/lib/activitypub/linked_data_signature_spec.rb +++ b/spec/lib/activitypub/linked_data_signature_spec.rb @@ -36,6 +36,40 @@ RSpec.describe ActivityPub::LinkedDataSignature do 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 let(:signature) { nil } diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index 102d2f625..19ff69559 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -31,44 +31,52 @@ RSpec.describe Tag do expect(subject.match('https://en.wikipedia.org/wiki/Ghostbusters_(song)#Lawsuit')).to be_nil 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 #aesthetic' do - expect(subject.match('this is #aesthetic').to_s).to eq ' #aesthetic' + expect(subject.match('this is #aesthetic').to_s).to eq '#aesthetic' end 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 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 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 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 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 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 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 it 'matches ・unicode in ぼっち・ざ・ろっく correctly' do - expect(subject.match('testing #ぼっち・ざ・ろっく').to_s).to eq ' #ぼっち・ざ・ろっく' + expect(subject.match('testing #ぼっち・ざ・ろっく').to_s).to eq '#ぼっち・ざ・ろっく' end 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 it 'does not match middle dots at the start' do @@ -76,7 +84,7 @@ RSpec.describe Tag do end 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 it 'does not match purely-numeric hashtags' do diff --git a/spec/requests/api/v1/accounts/featured_tags_spec.rb b/spec/requests/api/v1/accounts/featured_tags_spec.rb new file mode 100644 index 000000000..bae7d448b --- /dev/null +++ b/spec/requests/api/v1/accounts/featured_tags_spec.rb @@ -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