diff --git a/CHANGELOG.md b/CHANGELOG.md index 48129660c..32f6d2368 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,6 @@ All notable changes to this project will be documented in this file. -## [4.4.8] - 2025-10-21 - -### Security - -- Fix quote control bypass ([GHSA-8h43-rcqj-wpc6](https://github.com/mastodon/mastodon/security/advisories/GHSA-8h43-rcqj-wpc6)) - ## [4.4.7] - 2025-10-15 ### Fixed diff --git a/app/models/quote.rb b/app/models/quote.rb index 2b683b273..4b1072d6c 100644 --- a/app/models/quote.rb +++ b/app/models/quote.rb @@ -34,7 +34,6 @@ class Quote < ApplicationRecord before_validation :set_activity_uri, only: :create, if: -> { account.local? && quoted_account&.remote? } validates :activity_uri, presence: true, if: -> { account.local? && quoted_account&.remote? } validate :validate_visibility - validate :validate_original_quoted_status def accept! update!(state: :accepted) @@ -71,10 +70,6 @@ class Quote < ApplicationRecord errors.add(:quoted_status_id, :visibility_mismatch) end - def validate_original_quoted_status - errors.add(:quoted_status_id, :reblog_unallowed) if quoted_status&.reblog? - end - def set_activity_uri self.activity_uri = [ActivityPub::TagManager.instance.uri_for(account), '/quote_requests/', SecureRandom.uuid].join end diff --git a/app/serializers/rest/base_quote_serializer.rb b/app/serializers/rest/base_quote_serializer.rb index 2637014b6..be9d5cbe6 100644 --- a/app/serializers/rest/base_quote_serializer.rb +++ b/app/serializers/rest/base_quote_serializer.rb @@ -14,7 +14,7 @@ class REST::BaseQuoteSerializer < ActiveModel::Serializer end def quoted_status - object.quoted_status if object.accepted? && object.quoted_status.present? && !object.quoted_status&.reblog? && !status_filter.filtered_for_quote? + object.quoted_status if object.accepted? && object.quoted_status.present? && !status_filter.filtered_for_quote? end private diff --git a/app/services/activitypub/verify_quote_service.rb b/app/services/activitypub/verify_quote_service.rb index 4f92b3af5..822abcf40 100644 --- a/app/services/activitypub/verify_quote_service.rb +++ b/app/services/activitypub/verify_quote_service.rb @@ -72,7 +72,7 @@ class ActivityPub::VerifyQuoteService < BaseService status ||= ActivityPub::FetchRemoteStatusService.new.call(uri, on_behalf_of: @quote.account.followers.local.first, prefetched_body:, request_id: @request_id, depth: @depth + 1) - @quote.update(quoted_status: status) if status.present? && !status.reblog? + @quote.update(quoted_status: status) if status.present? rescue Mastodon::RecursionLimitExceededError, Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS => e @fetching_error = e end @@ -90,7 +90,7 @@ class ActivityPub::VerifyQuoteService < BaseService status = ActivityPub::FetchRemoteStatusService.new.call(object['id'], prefetched_body: object, on_behalf_of: @quote.account.followers.local.first, request_id: @request_id, depth: @depth) - if status.present? && !status.reblog? + if status.present? @quote.update(quoted_status: status) true else diff --git a/docker-compose.yml b/docker-compose.yml index fb0cedc7b..a2264b65d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,7 +59,7 @@ services: web: # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes # build: . - image: ghcr.io/mastodon/mastodon:v4.4.8 + image: ghcr.io/mastodon/mastodon:v4.4.7 restart: always env_file: .env.production command: bundle exec puma -C config/puma.rb @@ -83,7 +83,7 @@ services: # build: # dockerfile: ./streaming/Dockerfile # context: . - image: ghcr.io/mastodon/mastodon-streaming:v4.4.8 + image: ghcr.io/mastodon/mastodon-streaming:v4.4.7 restart: always env_file: .env.production command: node ./streaming/index.js @@ -102,7 +102,7 @@ services: sidekiq: # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes # build: . - image: ghcr.io/mastodon/mastodon:v4.4.8 + image: ghcr.io/mastodon/mastodon:v4.4.7 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index a2ca92a2f..50aa38657 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 8 + 7 end def default_prerelease diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 1c79174c6..12fbdba80 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -1046,60 +1046,6 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'with a quote of a known reblog that is otherwise valid' do - let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') } - let(:quoted_status) { Fabricate(:status, account: quoted_account, reblog: Fabricate(:status)) } - let(:approval_uri) { 'https://quoted.example.com/quote-approval' } - - let(:object_json) do - build_object( - type: 'Note', - content: 'woah what she said is amazing', - quote: ActivityPub::TagManager.instance.uri_for(quoted_status), - quoteAuthorization: approval_uri - ) - end - - before do - stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ - '@context': [ - 'https://www.w3.org/ns/activitystreams', - { - QuoteAuthorization: 'https://w3id.org/fep/044f#QuoteAuthorization', - gts: 'https://gotosocial.org/ns#', - interactionPolicy: { - '@id': 'gts:interactionPolicy', - '@type': '@id', - }, - interactingObject: { - '@id': 'gts:interactingObject', - '@type': '@id', - }, - interactionTarget: { - '@id': 'gts:interactionTarget', - '@type': '@id', - }, - }, - ], - type: 'QuoteAuthorization', - id: approval_uri, - attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account), - interactingObject: object_json[:id], - interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status), - })) - end - - it 'creates a status without the verified quote' do - expect { subject.perform }.to change(sender.statuses, :count).by(1) - - status = sender.statuses.first - expect(status).to_not be_nil - expect(status.quote).to_not be_nil - expect(status.quote.state).to_not eq 'accepted' - expect(status.quote.quoted_status).to be_nil - end - end - context 'when a vote to a local poll' do let(:poll) { Fabricate(:poll, options: %w(Yellow Blue)) } let!(:local_status) { Fabricate(:status, poll: poll) } diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index 8537931c7..43e6f682c 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -733,72 +733,6 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do end end - context 'when the status adds a verifiable quote of a reblog through an explicit update' do - let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') } - let(:quoted_status) { Fabricate(:status, account: quoted_account, reblog: Fabricate(:status)) } - let(:approval_uri) { 'https://quoted.example.com/approvals/1' } - - let(:payload) do - { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - { - '@id': 'https://w3id.org/fep/044f#quote', - '@type': '@id', - }, - { - '@id': 'https://w3id.org/fep/044f#quoteAuthorization', - '@type': '@id', - }, - ], - id: 'foo', - type: 'Note', - summary: 'Show more', - content: 'Hello universe', - updated: '2021-09-08T22:39:25Z', - quote: ActivityPub::TagManager.instance.uri_for(quoted_status), - quoteAuthorization: approval_uri, - } - end - - before do - stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ - '@context': [ - 'https://www.w3.org/ns/activitystreams', - { - QuoteAuthorization: 'https://w3id.org/fep/044f#QuoteAuthorization', - gts: 'https://gotosocial.org/ns#', - interactionPolicy: { - '@id': 'gts:interactionPolicy', - '@type': '@id', - }, - interactingObject: { - '@id': 'gts:interactingObject', - '@type': '@id', - }, - interactionTarget: { - '@id': 'gts:interactionTarget', - '@type': '@id', - }, - }, - ], - type: 'QuoteAuthorization', - id: approval_uri, - attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account), - interactingObject: ActivityPub::TagManager.instance.uri_for(status), - interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status), - })) - end - - it 'updates the approval URI but does not verify the quote' do - expect { subject.call(status, json, json) } - .to change(status, :quote).from(nil) - expect(status.quote.approval_uri).to eq approval_uri - expect(status.quote.state).to_not eq 'accepted' - expect(status.quote.quoted_status).to be_nil - end - end - context 'when the status adds a unverifiable quote through an implicit update' do let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') } let(:quoted_status) { Fabricate(:status, account: quoted_account) }