From 79bf6950a5bc5e21f74a71cf17137deabb48249a Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 19 Nov 2025 14:13:06 +0100 Subject: [PATCH 1/3] Fix `Update` importing old previously-unknown activities and treating them as recent ones (#36848) --- app/lib/activitypub/activity/update.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/lib/activitypub/activity/update.rb b/app/lib/activitypub/activity/update.rb index 15025ca5e..5185507bd 100644 --- a/app/lib/activitypub/activity/update.rb +++ b/app/lib/activitypub/activity/update.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true class ActivityPub::Activity::Update < ActivityPub::Activity + # Updates to unknown objects older than that are ignored + OBJECT_AGE_THRESHOLD = 1.day + def perform @account.schedule_refresh_if_stale! @@ -28,6 +31,9 @@ class ActivityPub::Activity::Update < ActivityPub::Activity @status = Status.find_by(uri: object_uri, account_id: @account.id) + # Ignore updates for old unknown objects, since those are updates we are not interested in + return if @status.nil? && object_too_old? + # We may be getting `Create` and `Update` out of order @status ||= ActivityPub::Activity::Create.new(@json, @account, **@options).perform @@ -35,4 +41,10 @@ class ActivityPub::Activity::Update < ActivityPub::Activity ActivityPub::ProcessStatusUpdateService.new.call(@status, @json, @object, request_id: @options[:request_id]) end + + def object_too_old? + @object['published'].present? && @object['published'].to_datetime < OBJECT_AGE_THRESHOLD.ago + rescue Date::Error + false + end end From 3c443b007f0b1bc0efd938692d4290d1084a62a3 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 8 Dec 2025 15:44:08 +0100 Subject: [PATCH 2/3] Merge commit from fork --- app/controllers/activitypub/replies_controller.rb | 2 +- app/controllers/api/v1/polls/votes_controller.rb | 2 +- app/controllers/api/v1/polls_controller.rb | 2 +- app/controllers/api/v1/statuses/bookmarks_controller.rb | 4 ++-- .../api/v1/statuses/favourited_by_accounts_controller.rb | 2 +- app/controllers/api/v1/statuses/favourites_controller.rb | 4 ++-- app/controllers/api/v1/statuses/histories_controller.rb | 2 +- app/controllers/api/v1/statuses/mutes_controller.rb | 2 +- .../api/v1/statuses/reblogged_by_accounts_controller.rb | 2 +- app/controllers/api/v1/statuses/reblogs_controller.rb | 4 ++-- app/controllers/api/v1/statuses/sources_controller.rb | 2 +- app/controllers/api/v1/statuses_controller.rb | 2 +- app/controllers/api/web/embeds_controller.rb | 2 +- app/controllers/authorize_interactions_controller.rb | 2 +- app/controllers/media_controller.rb | 2 +- app/controllers/statuses_controller.rb | 2 +- 16 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb index 25f095cfa..359717876 100644 --- a/app/controllers/activitypub/replies_controller.rb +++ b/app/controllers/activitypub/replies_controller.rb @@ -27,7 +27,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController def set_status @status = @account.statuses.find(params[:status_id]) authorize @status, :show? - rescue Mastodon::NotPermittedError + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError not_found end diff --git a/app/controllers/api/v1/polls/votes_controller.rb b/app/controllers/api/v1/polls/votes_controller.rb index 513b937ef..ebd96d7f1 100644 --- a/app/controllers/api/v1/polls/votes_controller.rb +++ b/app/controllers/api/v1/polls/votes_controller.rb @@ -17,7 +17,7 @@ class Api::V1::Polls::VotesController < Api::BaseController def set_poll @poll = Poll.attached.find(params[:poll_id]) authorize @poll.status, :show? - rescue Mastodon::NotPermittedError + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError not_found end diff --git a/app/controllers/api/v1/polls_controller.rb b/app/controllers/api/v1/polls_controller.rb index ffc70a849..b9d2b2ddd 100644 --- a/app/controllers/api/v1/polls_controller.rb +++ b/app/controllers/api/v1/polls_controller.rb @@ -17,7 +17,7 @@ class Api::V1::PollsController < Api::BaseController def set_poll @poll = Poll.attached.find(params[:id]) authorize @poll.status, :show? - rescue Mastodon::NotPermittedError + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError not_found end diff --git a/app/controllers/api/v1/statuses/bookmarks_controller.rb b/app/controllers/api/v1/statuses/bookmarks_controller.rb index 19963c002..c4fb2304d 100644 --- a/app/controllers/api/v1/statuses/bookmarks_controller.rb +++ b/app/controllers/api/v1/statuses/bookmarks_controller.rb @@ -25,7 +25,7 @@ class Api::V1::Statuses::BookmarksController < Api::BaseController bookmark&.destroy! render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_account.id, bookmarks_map: { @status.id => false }) - rescue Mastodon::NotPermittedError + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError not_found end @@ -34,7 +34,7 @@ class Api::V1::Statuses::BookmarksController < Api::BaseController def set_status @status = Status.find(params[:status_id]) authorize @status, :show? - rescue Mastodon::NotPermittedError + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError not_found end end diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb index 73eb11e71..10bf6afe5 100644 --- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb @@ -64,7 +64,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController def set_status @status = Status.find(params[:status_id]) authorize @status, :show? - rescue Mastodon::NotPermittedError + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError not_found end diff --git a/app/controllers/api/v1/statuses/favourites_controller.rb b/app/controllers/api/v1/statuses/favourites_controller.rb index f3428e3df..918c32085 100644 --- a/app/controllers/api/v1/statuses/favourites_controller.rb +++ b/app/controllers/api/v1/statuses/favourites_controller.rb @@ -27,7 +27,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController relationships = StatusRelationshipsPresenter.new([@status], current_account.id, favourites_map: { @status.id => false }, attributes_map: { @status.id => { favourites_count: count } }) render json: @status, serializer: REST::StatusSerializer, relationships: relationships - rescue Mastodon::NotPermittedError + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError not_found end @@ -36,7 +36,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController def set_status @status = Status.find(params[:status_id]) authorize @status, :show? - rescue Mastodon::NotPermittedError + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError not_found end end diff --git a/app/controllers/api/v1/statuses/histories_controller.rb b/app/controllers/api/v1/statuses/histories_controller.rb index 2913472b0..022b1b501 100644 --- a/app/controllers/api/v1/statuses/histories_controller.rb +++ b/app/controllers/api/v1/statuses/histories_controller.rb @@ -20,7 +20,7 @@ class Api::V1::Statuses::HistoriesController < Api::BaseController def set_status @status = Status.find(params[:status_id]) authorize @status, :show? - rescue Mastodon::NotPermittedError + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError not_found end end diff --git a/app/controllers/api/v1/statuses/mutes_controller.rb b/app/controllers/api/v1/statuses/mutes_controller.rb index 87071a2b9..71f67bda3 100644 --- a/app/controllers/api/v1/statuses/mutes_controller.rb +++ b/app/controllers/api/v1/statuses/mutes_controller.rb @@ -27,7 +27,7 @@ class Api::V1::Statuses::MutesController < Api::BaseController def set_status @status = Status.find(params[:status_id]) authorize @status, :show? - rescue Mastodon::NotPermittedError + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError not_found end diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb index 41672e753..268dbc132 100644 --- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb @@ -60,7 +60,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController def set_status @status = Status.find(params[:status_id]) authorize @status, :show? - rescue Mastodon::NotPermittedError + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError not_found end diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb index 3ca623117..5b605e7c2 100644 --- a/app/controllers/api/v1/statuses/reblogs_controller.rb +++ b/app/controllers/api/v1/statuses/reblogs_controller.rb @@ -36,7 +36,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController relationships = StatusRelationshipsPresenter.new([@status], current_account.id, reblogs_map: { @reblog.id => false }, attributes_map: { @reblog.id => { reblogs_count: count } }) render json: @reblog, serializer: REST::StatusSerializer, relationships: relationships - rescue Mastodon::NotPermittedError + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError not_found end @@ -45,7 +45,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController def set_reblog @reblog = Status.find(params[:status_id]) authorize @reblog, :show? - rescue Mastodon::NotPermittedError + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError not_found end diff --git a/app/controllers/api/v1/statuses/sources_controller.rb b/app/controllers/api/v1/statuses/sources_controller.rb index 434086451..81f2fef2a 100644 --- a/app/controllers/api/v1/statuses/sources_controller.rb +++ b/app/controllers/api/v1/statuses/sources_controller.rb @@ -15,7 +15,7 @@ class Api::V1::Statuses::SourcesController < Api::BaseController def set_status @status = Status.find(params[:status_id]) authorize @status, :show? - rescue Mastodon::NotPermittedError + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError not_found end end diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 064e7632a..5707ab661 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -118,7 +118,7 @@ class Api::V1::StatusesController < Api::BaseController def set_status @status = Status.find(params[:id]) authorize @status, :show? - rescue Mastodon::NotPermittedError + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError not_found end diff --git a/app/controllers/api/web/embeds_controller.rb b/app/controllers/api/web/embeds_controller.rb index 63c3f2d90..676d67485 100644 --- a/app/controllers/api/web/embeds_controller.rb +++ b/app/controllers/api/web/embeds_controller.rb @@ -30,7 +30,7 @@ class Api::Web::EmbedsController < Api::Web::BaseController def set_status @status = Status.find(params[:id]) authorize @status, :show? - rescue Mastodon::NotPermittedError + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError not_found end end diff --git a/app/controllers/authorize_interactions_controller.rb b/app/controllers/authorize_interactions_controller.rb index 99eed018b..03cad3e31 100644 --- a/app/controllers/authorize_interactions_controller.rb +++ b/app/controllers/authorize_interactions_controller.rb @@ -21,7 +21,7 @@ class AuthorizeInteractionsController < ApplicationController def set_resource @resource = located_resource authorize(@resource, :show?) if @resource.is_a?(Status) - rescue Mastodon::NotPermittedError + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError not_found end diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index 53eee4001..2727b8ca8 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -36,7 +36,7 @@ class MediaController < ApplicationController def verify_permitted_status! authorize @media_attachment.status, :show? - rescue Mastodon::NotPermittedError + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError not_found end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index effaba363..fdd63cc4f 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -63,7 +63,7 @@ class StatusesController < ApplicationController def set_status @status = @account.statuses.find(params[:id]) authorize @status, :show? - rescue Mastodon::NotPermittedError + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError not_found end From 998e9cedd3fcbfa55146ba3f2b7c497dfb7efec9 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 8 Dec 2025 16:20:24 +0100 Subject: [PATCH 3/3] Bump version to v4.2.28 (#37164) --- CHANGELOG.md | 10 ++++++++++ docker-compose.yml | 6 +++--- lib/mastodon/version.rb | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0d7b3f50..6c27aca0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. +## [4.2.28] - 2025-12-08 + +### Security + +- Fix inconsistent error handling leaking information on existence of private posts ([GHSA-gwhw-gcjx-72v8](https://github.com/mastodon/mastodon/security/advisories/GHSA-gwhw-gcjx-72v8)) + +### Fixed + +- Fix old previously-undiscovered posts being treated as new when receiving an `Update` (#36848 by @ClearlyClaire) + ## [4.2.27] - 2025-10-13 ### Security diff --git a/docker-compose.yml b/docker-compose.yml index ceb0c4d79..56fea4eec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,7 +56,7 @@ services: web: build: . - image: ghcr.io/mastodon/mastodon:v4.2.27 + image: ghcr.io/mastodon/mastodon:v4.2.28 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.2.27 + image: ghcr.io/mastodon/mastodon:v4.2.28 restart: always env_file: .env.production command: node ./streaming @@ -95,7 +95,7 @@ services: sidekiq: build: . - image: ghcr.io/mastodon/mastodon:v4.2.27 + image: ghcr.io/mastodon/mastodon:v4.2.28 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 4474714f5..257c04648 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 27 + 28 end def default_prerelease