Compare commits
	
		
			6 Commits
		
	
	
		
			2b67d2abc1
			...
			techhub
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					d926537305 | ||
| 
						 | 
					c2fb12d22d | ||
| 
						 | 
					2dc4552229 | ||
| 
						 | 
					8965e1bfa9 | ||
| 
						 | 
					1e27ab0885 | ||
| 
						 | 
					cef2c50a71 | 
							
								
								
									
										14
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -2,6 +2,20 @@
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
- Fix forwarder being called with `nil` status when quote post is soft-deleted (#36463 by @ClearlyClaire)
 | 
			
		||||
- Fix moderation warning e-mails that include posts (#36462 by @ClearlyClaire)
 | 
			
		||||
- Fix allow_referrer_origin typo (#36460 by @ShadowJonathan)
 | 
			
		||||
 | 
			
		||||
## [4.4.6] - 2025-10-13
 | 
			
		||||
 | 
			
		||||
### Security
 | 
			
		||||
 
 | 
			
		||||
@@ -59,9 +59,11 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
 | 
			
		||||
    @quote = Quote.find_by(approval_uri: object_uri, quoted_account: @account)
 | 
			
		||||
    return if @quote.nil?
 | 
			
		||||
 | 
			
		||||
    ActivityPub::Forwarder.new(@account, @json, @quote.status).forward!
 | 
			
		||||
    ActivityPub::Forwarder.new(@account, @json, @quote.status).forward! if @quote.status.present?
 | 
			
		||||
 | 
			
		||||
    @quote.reject!
 | 
			
		||||
    DistributionWorker.perform_async(@quote.status_id, { 'update' => true })
 | 
			
		||||
 | 
			
		||||
    DistributionWorker.perform_async(@quote.status_id, { 'update' => true }) if @quote.status.present?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def forwarder
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,7 @@ 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)
 | 
			
		||||
@@ -70,6 +71,10 @@ 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
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ class REST::BaseQuoteSerializer < ActiveModel::Serializer
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def quoted_status
 | 
			
		||||
    object.quoted_status if object.accepted? && object.quoted_status.present? && !status_filter.filtered_for_quote?
 | 
			
		||||
    object.quoted_status if object.accepted? && object.quoted_status.present? && !object.quoted_status&.reblog? && !status_filter.filtered_for_quote?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
 | 
			
		||||
 | 
			
		||||
  attributes :domain, :title, :version, :source_url, :description,
 | 
			
		||||
             :usage, :thumbnail, :icon, :languages, :configuration,
 | 
			
		||||
             :registrations, :api_versions
 | 
			
		||||
             :registrations, :api_versions, :max_toot_chars
 | 
			
		||||
 | 
			
		||||
  has_one :contact, serializer: ContactSerializer
 | 
			
		||||
  has_many :rules, serializer: REST::RuleSerializer
 | 
			
		||||
@@ -119,6 +119,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer
 | 
			
		||||
    Mastodon::Version.api_versions
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def max_toot_chars
 | 
			
		||||
    StatusLengthValidator::MAX_CHARS
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def registrations_enabled?
 | 
			
		||||
 
 | 
			
		||||
@@ -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?
 | 
			
		||||
    @quote.update(quoted_status: status) if status.present? && !status.reblog?
 | 
			
		||||
  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?
 | 
			
		||||
    if status.present? && !status.reblog?
 | 
			
		||||
      @quote.update(quoted_status: status)
 | 
			
		||||
      true
 | 
			
		||||
    else
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class StatusLengthValidator < ActiveModel::Validator
 | 
			
		||||
  MAX_CHARS = 500
 | 
			
		||||
  MAX_CHARS = 1024
 | 
			
		||||
  URL_PLACEHOLDER_CHARS = 23
 | 
			
		||||
  URL_PLACEHOLDER = 'x' * 23
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@
 | 
			
		||||
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
 | 
			
		||||
  %tr
 | 
			
		||||
    %td.email-status-content
 | 
			
		||||
      = render 'status_content', status: status
 | 
			
		||||
      = render 'notification_mailer/status_content', status: status
 | 
			
		||||
 | 
			
		||||
      %p.email-status-footer
 | 
			
		||||
        = link_to l(status.created_at.in_time_zone(time_zone.presence), format: :with_time_zone), web_url("@#{status.account.pretty_acct}/#{status.id}")
 | 
			
		||||
 
 | 
			
		||||
@@ -11,12 +11,12 @@
 | 
			
		||||
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
 | 
			
		||||
  %tr
 | 
			
		||||
    %td.email-status-content
 | 
			
		||||
      = render 'status_content', status: status
 | 
			
		||||
      = render 'notification_mailer/status_content', status: status
 | 
			
		||||
 | 
			
		||||
      - if status.local? && status.quote
 | 
			
		||||
        %table.email-inner-card-table{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
 | 
			
		||||
          %tr
 | 
			
		||||
            %td.email-inner-nested-card-td
 | 
			
		||||
              = render 'nested_quote', status: status.quote.quoted_status, time_zone: time_zone
 | 
			
		||||
              = render 'notification_mailer/nested_quote', status: status.quote.quoted_status, time_zone: time_zone
 | 
			
		||||
      %p.email-status-footer
 | 
			
		||||
        = link_to l(status.created_at.in_time_zone(time_zone.presence), format: :with_time_zone), web_url("@#{status.account.pretty_acct}/#{status.id}")
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,7 @@ defaults: &defaults
 | 
			
		||||
  require_invite_text: false
 | 
			
		||||
  backups_retention_period: 7
 | 
			
		||||
  captcha_enabled: false
 | 
			
		||||
  allow_referer_origin: false
 | 
			
		||||
  allow_referrer_origin: false
 | 
			
		||||
 | 
			
		||||
development:
 | 
			
		||||
  <<: *defaults
 | 
			
		||||
 
 | 
			
		||||
@@ -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.6
 | 
			
		||||
    image: ghcr.io/mastodon/mastodon:v4.4.8
 | 
			
		||||
    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.6
 | 
			
		||||
    image: ghcr.io/mastodon/mastodon-streaming:v4.4.8
 | 
			
		||||
    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.6
 | 
			
		||||
    image: ghcr.io/mastodon/mastodon:v4.4.8
 | 
			
		||||
    restart: always
 | 
			
		||||
    env_file: .env.production
 | 
			
		||||
    command: bundle exec sidekiq
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ module Mastodon
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def patch
 | 
			
		||||
      6
 | 
			
		||||
      8
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    def default_prerelease
 | 
			
		||||
 
 | 
			
		||||
@@ -1046,6 +1046,60 @@ 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) }
 | 
			
		||||
 
 | 
			
		||||
@@ -141,7 +141,9 @@ RSpec.describe UserMailer do
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#warning' do
 | 
			
		||||
    let(:strike) { Fabricate(:account_warning, target_account: receiver.account, text: 'dont worry its just the testsuite', action: 'suspend') }
 | 
			
		||||
    let(:status) { Fabricate(:status, account: receiver.account) }
 | 
			
		||||
    let(:quote) { Fabricate(:quote, state: :accepted, status: status) }
 | 
			
		||||
    let(:strike) { Fabricate(:account_warning, target_account: receiver.account, text: 'dont worry its just the testsuite', action: 'suspend', status_ids: [quote.status_id]) }
 | 
			
		||||
    let(:mail)   { described_class.warning(receiver, strike) }
 | 
			
		||||
 | 
			
		||||
    it 'renders warning notification' do
 | 
			
		||||
 
 | 
			
		||||
@@ -733,6 +733,72 @@ 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) }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user