2
0

Compare commits

...

3 Commits

Author SHA1 Message Date
Nicolas Dextraze
d926537305 fix max chars to 1024 2025-10-22 18:00:32 -04:00
Claire
c2fb12d22d Bump version to v4.4.8 (#36542) 2025-10-21 15:12:37 +02:00
Claire
2dc4552229 Merge commit from fork
* Add validation to reject quotes of reblogs

* Do not process quotes of reblogs as potentially valid quotes

* Refuse to serve quoted reblogs over REST API
2025-10-21 15:00:28 +02:00
10 changed files with 144 additions and 9 deletions

View File

@@ -2,6 +2,12 @@
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

View File

@@ -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

View File

@@ -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

View File

@@ -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?

View File

@@ -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

View File

@@ -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

View File

@@ -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.7
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.7
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.7
image: ghcr.io/mastodon/mastodon:v4.4.8
restart: always
env_file: .env.production
command: bundle exec sidekiq

View File

@@ -13,7 +13,7 @@ module Mastodon
end
def patch
7
8
end
def default_prerelease

View File

@@ -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) }

View File

@@ -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) }