Compare commits
1 Commits
techhub
...
2b67d2abc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b67d2abc1 |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -2,20 +2,6 @@
|
|||||||
|
|
||||||
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.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
|
## [4.4.6] - 2025-10-13
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|||||||
@@ -59,11 +59,9 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
|||||||
@quote = Quote.find_by(approval_uri: object_uri, quoted_account: @account)
|
@quote = Quote.find_by(approval_uri: object_uri, quoted_account: @account)
|
||||||
return if @quote.nil?
|
return if @quote.nil?
|
||||||
|
|
||||||
ActivityPub::Forwarder.new(@account, @json, @quote.status).forward! if @quote.status.present?
|
ActivityPub::Forwarder.new(@account, @json, @quote.status).forward!
|
||||||
|
|
||||||
@quote.reject!
|
@quote.reject!
|
||||||
|
DistributionWorker.perform_async(@quote.status_id, { 'update' => true })
|
||||||
DistributionWorker.perform_async(@quote.status_id, { 'update' => true }) if @quote.status.present?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def forwarder
|
def forwarder
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ class Quote < ApplicationRecord
|
|||||||
before_validation :set_activity_uri, only: :create, if: -> { account.local? && quoted_account&.remote? }
|
before_validation :set_activity_uri, only: :create, if: -> { account.local? && quoted_account&.remote? }
|
||||||
validates :activity_uri, presence: true, if: -> { account.local? && quoted_account&.remote? }
|
validates :activity_uri, presence: true, if: -> { account.local? && quoted_account&.remote? }
|
||||||
validate :validate_visibility
|
validate :validate_visibility
|
||||||
validate :validate_original_quoted_status
|
|
||||||
|
|
||||||
def accept!
|
def accept!
|
||||||
update!(state: :accepted)
|
update!(state: :accepted)
|
||||||
@@ -71,10 +70,6 @@ class Quote < ApplicationRecord
|
|||||||
errors.add(:quoted_status_id, :visibility_mismatch)
|
errors.add(:quoted_status_id, :visibility_mismatch)
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_original_quoted_status
|
|
||||||
errors.add(:quoted_status_id, :reblog_unallowed) if quoted_status&.reblog?
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_activity_uri
|
def set_activity_uri
|
||||||
self.activity_uri = [ActivityPub::TagManager.instance.uri_for(account), '/quote_requests/', SecureRandom.uuid].join
|
self.activity_uri = [ActivityPub::TagManager.instance.uri_for(account), '/quote_requests/', SecureRandom.uuid].join
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class REST::BaseQuoteSerializer < ActiveModel::Serializer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def quoted_status
|
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
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -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)
|
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
|
rescue Mastodon::RecursionLimitExceededError, Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS => e
|
||||||
@fetching_error = e
|
@fetching_error = e
|
||||||
end
|
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)
|
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)
|
@quote.update(quoted_status: status)
|
||||||
true
|
true
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
||||||
%tr
|
%tr
|
||||||
%td.email-status-content
|
%td.email-status-content
|
||||||
= render 'notification_mailer/status_content', status: status
|
= render 'status_content', status: status
|
||||||
|
|
||||||
%p.email-status-footer
|
%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}")
|
= 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' }
|
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
||||||
%tr
|
%tr
|
||||||
%td.email-status-content
|
%td.email-status-content
|
||||||
= render 'notification_mailer/status_content', status: status
|
= render 'status_content', status: status
|
||||||
|
|
||||||
- if status.local? && status.quote
|
- if status.local? && status.quote
|
||||||
%table.email-inner-card-table{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
%table.email-inner-card-table{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
||||||
%tr
|
%tr
|
||||||
%td.email-inner-nested-card-td
|
%td.email-inner-nested-card-td
|
||||||
= render 'notification_mailer/nested_quote', status: status.quote.quoted_status, time_zone: time_zone
|
= render 'nested_quote', status: status.quote.quoted_status, time_zone: time_zone
|
||||||
%p.email-status-footer
|
%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}")
|
= 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
|
require_invite_text: false
|
||||||
backups_retention_period: 7
|
backups_retention_period: 7
|
||||||
captcha_enabled: false
|
captcha_enabled: false
|
||||||
allow_referrer_origin: false
|
allow_referer_origin: false
|
||||||
|
|
||||||
development:
|
development:
|
||||||
<<: *defaults
|
<<: *defaults
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ services:
|
|||||||
web:
|
web:
|
||||||
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
|
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
|
||||||
# build: .
|
# build: .
|
||||||
image: ghcr.io/mastodon/mastodon:v4.4.8
|
image: ghcr.io/mastodon/mastodon:v4.4.6
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bundle exec puma -C config/puma.rb
|
command: bundle exec puma -C config/puma.rb
|
||||||
@@ -83,7 +83,7 @@ services:
|
|||||||
# build:
|
# build:
|
||||||
# dockerfile: ./streaming/Dockerfile
|
# dockerfile: ./streaming/Dockerfile
|
||||||
# context: .
|
# context: .
|
||||||
image: ghcr.io/mastodon/mastodon-streaming:v4.4.8
|
image: ghcr.io/mastodon/mastodon-streaming:v4.4.6
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: node ./streaming/index.js
|
command: node ./streaming/index.js
|
||||||
@@ -102,7 +102,7 @@ services:
|
|||||||
sidekiq:
|
sidekiq:
|
||||||
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
|
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
|
||||||
# build: .
|
# build: .
|
||||||
image: ghcr.io/mastodon/mastodon:v4.4.8
|
image: ghcr.io/mastodon/mastodon:v4.4.6
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bundle exec sidekiq
|
command: bundle exec sidekiq
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ module Mastodon
|
|||||||
end
|
end
|
||||||
|
|
||||||
def patch
|
def patch
|
||||||
8
|
6
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_prerelease
|
def default_prerelease
|
||||||
|
|||||||
@@ -1046,60 +1046,6 @@ RSpec.describe ActivityPub::Activity::Create do
|
|||||||
end
|
end
|
||||||
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
|
context 'when a vote to a local poll' do
|
||||||
let(:poll) { Fabricate(:poll, options: %w(Yellow Blue)) }
|
let(:poll) { Fabricate(:poll, options: %w(Yellow Blue)) }
|
||||||
let!(:local_status) { Fabricate(:status, poll: poll) }
|
let!(:local_status) { Fabricate(:status, poll: poll) }
|
||||||
|
|||||||
@@ -141,9 +141,7 @@ RSpec.describe UserMailer do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe '#warning' do
|
describe '#warning' do
|
||||||
let(:status) { Fabricate(:status, account: receiver.account) }
|
let(:strike) { Fabricate(:account_warning, target_account: receiver.account, text: 'dont worry its just the testsuite', action: 'suspend') }
|
||||||
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) }
|
let(:mail) { described_class.warning(receiver, strike) }
|
||||||
|
|
||||||
it 'renders warning notification' do
|
it 'renders warning notification' do
|
||||||
|
|||||||
@@ -733,72 +733,6 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
|||||||
end
|
end
|
||||||
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
|
context 'when the status adds a unverifiable quote through an implicit update' do
|
||||||
let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
|
let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') }
|
||||||
let(:quoted_status) { Fabricate(:status, account: quoted_account) }
|
let(:quoted_status) { Fabricate(:status, account: quoted_account) }
|
||||||
|
|||||||
Reference in New Issue
Block a user