From 821e735524fe96a8a133923906224c39d2d99840 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 21 Nov 2025 04:16:50 -0500 Subject: [PATCH 1/6] Suggest ES image version 7.17.29 in docker compose (#36972) --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index cb691e53e1..ebabd12cc7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,7 +27,7 @@ services: # es: # restart: always - # image: docker.elastic.co/elasticsearch/elasticsearch:7.17.4 + # image: docker.elastic.co/elasticsearch/elasticsearch:7.17.29 # environment: # - "ES_JAVA_OPTS=-Xms512m -Xmx512m -Des.enforce.bootstrap.checks=true" # - "xpack.license.self_generated.type=basic" From 473c112dae830b06a3c2b2b4e5bedc3ccd9247f3 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 25 Nov 2025 11:18:34 +0100 Subject: [PATCH 2/6] Increase HTTP read timeout for expensive S3 batch delete operation (#37004) --- app/lib/attachment_batch.rb | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/app/lib/attachment_batch.rb b/app/lib/attachment_batch.rb index 374abfac49..1443a1ec60 100644 --- a/app/lib/attachment_batch.rb +++ b/app/lib/attachment_batch.rb @@ -112,10 +112,12 @@ class AttachmentBatch keys.each_slice(LIMIT) do |keys_slice| logger.debug { "Deleting #{keys_slice.size} objects" } - bucket.delete_objects(delete: { - objects: keys_slice.map { |key| { key: key } }, - quiet: true, - }) + with_overridden_timeout(bucket.client, 120) do + bucket.delete_objects(delete: { + objects: keys_slice.map { |key| { key: key } }, + quiet: true, + }) + end rescue => e retries += 1 @@ -134,6 +136,20 @@ class AttachmentBatch @bucket ||= records.first.public_send(@attachment_names.first).s3_bucket end + # Currently, the aws-sdk-s3 gem does not offer a way to cleanly override the timeout + # per-request. So we change the client's config instead. As this client will likely + # be re-used for other jobs, restore its original configuration in an `ensure` block. + def with_overridden_timeout(s3_client, longer_read_timeout) + original_timeout = s3_client.config.http_read_timeout + s3_client.config.http_read_timeout = [original_timeout, longer_read_timeout].max + + begin + yield + ensure + s3_client.config.http_read_timeout = original_timeout + end + end + def nullified_attributes @attachment_names.flat_map { |attachment_name| NULLABLE_ATTRIBUTES.map { |attribute| "#{attachment_name}_#{attribute}" } & klass.column_names }.index_with(nil) end From 43f8760c958c27a29653c8b1525c2078831ca807 Mon Sep 17 00:00:00 2001 From: Bruno Viveiros Date: Tue, 2 Sep 2025 07:04:59 -0300 Subject: [PATCH 3/6] fix: YouTube iframe being able to start at a defined time (#26584) --- .../features/status/components/card.jsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/javascript/mastodon/features/status/components/card.jsx b/app/javascript/mastodon/features/status/components/card.jsx index ee1fbe0f8f..567421273e 100644 --- a/app/javascript/mastodon/features/status/components/card.jsx +++ b/app/javascript/mastodon/features/status/components/card.jsx @@ -37,18 +37,20 @@ const getHostname = url => { const domParser = new DOMParser(); -const addAutoPlay = html => { +const handleIframeUrl = (html, url, providerName) => { const document = domParser.parseFromString(html, 'text/html').documentElement; const iframe = document.querySelector('iframe'); + const startTime = new URL(url).searchParams.get('t') if (iframe) { - if (iframe.src.indexOf('?') !== -1) { - iframe.src += '&'; - } else { - iframe.src += '?'; - } + const iframeUrl = new URL(iframe.src) - iframe.src += 'autoplay=1&auto_play=1'; + iframeUrl.searchParams.set('autoplay', 1) + iframeUrl.searchParams.set('auto_play', 1) + + if (startTime && providerName === "YouTube") iframeUrl.searchParams.set('start', startTime) + + iframe.src = iframeUrl.href // DOM parser creates html/body elements around original HTML fragment, // so we need to get innerHTML out of the body and not the entire document @@ -114,7 +116,7 @@ export default class Card extends PureComponent { renderVideo () { const { card } = this.props; - const content = { __html: addAutoPlay(card.get('html')) }; + const content = { __html: handleIframeUrl(card.get('html'), card.get('url'), card.get('provider_name')) }; return (
Date: Thu, 4 Dec 2025 17:27:20 +0100 Subject: [PATCH 4/6] Fixes YouTube embeds (#37126) --- .../mastodon/features/status/components/card.jsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/javascript/mastodon/features/status/components/card.jsx b/app/javascript/mastodon/features/status/components/card.jsx index 567421273e..161961fdb3 100644 --- a/app/javascript/mastodon/features/status/components/card.jsx +++ b/app/javascript/mastodon/features/status/components/card.jsx @@ -40,17 +40,20 @@ const domParser = new DOMParser(); const handleIframeUrl = (html, url, providerName) => { const document = domParser.parseFromString(html, 'text/html').documentElement; const iframe = document.querySelector('iframe'); - const startTime = new URL(url).searchParams.get('t') + const startTime = new URL(url).searchParams.get('t'); if (iframe) { - const iframeUrl = new URL(iframe.src) + const iframeUrl = new URL(iframe.src); - iframeUrl.searchParams.set('autoplay', 1) - iframeUrl.searchParams.set('auto_play', 1) + iframeUrl.searchParams.set('autoplay', 1); + iframeUrl.searchParams.set('auto_play', 1); - if (startTime && providerName === "YouTube") iframeUrl.searchParams.set('start', startTime) + if (providerName === 'YouTube') { + iframeUrl.searchParams.set('start', startTime || ''); + iframe.referrerPolicy = 'strict-origin-when-cross-origin'; + } - iframe.src = iframeUrl.href + iframe.src = iframeUrl.href; // DOM parser creates html/body elements around original HTML fragment, // so we need to get innerHTML out of the body and not the entire document From ef1af11956126218b6966623a2184bb5ef5cd5dd Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 8 Dec 2025 15:44:08 +0100 Subject: [PATCH 5/6] Merge commit from fork --- app/controllers/activitypub/likes_controller.rb | 2 +- app/controllers/activitypub/replies_controller.rb | 2 +- app/controllers/activitypub/shares_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/base_controller.rb | 2 +- app/controllers/api/v1/statuses/bookmarks_controller.rb | 2 +- app/controllers/api/v1/statuses/favourites_controller.rb | 2 +- app/controllers/api/v1/statuses/reblogs_controller.rb | 4 ++-- 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 +- 14 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/controllers/activitypub/likes_controller.rb b/app/controllers/activitypub/likes_controller.rb index 4aa6a4a771..6de110a272 100644 --- a/app/controllers/activitypub/likes_controller.rb +++ b/app/controllers/activitypub/likes_controller.rb @@ -22,7 +22,7 @@ class ActivityPub::LikesController < 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/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb index 0a19275d38..9f4b934d14 100644 --- a/app/controllers/activitypub/replies_controller.rb +++ b/app/controllers/activitypub/replies_controller.rb @@ -25,7 +25,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/activitypub/shares_controller.rb b/app/controllers/activitypub/shares_controller.rb index 65b4a5b383..8258278e58 100644 --- a/app/controllers/activitypub/shares_controller.rb +++ b/app/controllers/activitypub/shares_controller.rb @@ -22,7 +22,7 @@ class ActivityPub::SharesController < 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 ad1b82cb52..c8cb2d8b0d 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 ffc70a8496..b9d2b2ddd5 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/base_controller.rb b/app/controllers/api/v1/statuses/base_controller.rb index 3f56b68bcf..0c4c49a2c3 100644 --- a/app/controllers/api/v1/statuses/base_controller.rb +++ b/app/controllers/api/v1/statuses/base_controller.rb @@ -10,7 +10,7 @@ class Api::V1::Statuses::BaseController < 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/bookmarks_controller.rb b/app/controllers/api/v1/statuses/bookmarks_controller.rb index 109b12f467..b4b976ac3c 100644 --- a/app/controllers/api/v1/statuses/bookmarks_controller.rb +++ b/app/controllers/api/v1/statuses/bookmarks_controller.rb @@ -23,7 +23,7 @@ class Api::V1::Statuses::BookmarksController < Api::V1::Statuses::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 end diff --git a/app/controllers/api/v1/statuses/favourites_controller.rb b/app/controllers/api/v1/statuses/favourites_controller.rb index dbc75a0364..17eeccdbe7 100644 --- a/app/controllers/api/v1/statuses/favourites_controller.rb +++ b/app/controllers/api/v1/statuses/favourites_controller.rb @@ -25,7 +25,7 @@ class Api::V1::Statuses::FavouritesController < Api::V1::Statuses::BaseControlle 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 end diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb index 971b054c54..6a5788fca3 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::V1::Statuses::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::V1::Statuses::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_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 19cc71ae58..e702563594 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -125,7 +125,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 f82c1c50d7..fba56b4058 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 99eed018b0..03cad3e317 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 9d10468e69..0590ea4027 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -34,7 +34,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 341b0e6472..4dcff94bfe 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -59,7 +59,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 770cf420854b03c1994ed21baa32a05b75ed34b4 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 8 Dec 2025 16:20:21 +0100 Subject: [PATCH 6/6] Bump version to v4.3.16 (#37163) --- CHANGELOG.md | 12 ++++++++++++ docker-compose.yml | 6 +++--- lib/mastodon/version.rb | 2 +- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76f40bd1d1..bbf142bf7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. +## [4.3.16] - 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 YouTube embeds by sending referer (#37126 by @ChaosExAnima) +- Fix YouTube iframe not being able to start at a defined time (#26584 by @BrunoViveiros) +- Fix known expensive S3 batch delete operation failing because of short timeouts (#37004 by @ClearlyClaire) + ## [4.3.15] - 2025-11-20 ### Fixed diff --git a/docker-compose.yml b/docker-compose.yml index ebabd12cc7..e379a15430 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.3.15 + image: ghcr.io/mastodon/mastodon:v4.3.16 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.3.15 + image: ghcr.io/mastodon/mastodon-streaming:v4.3.16 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.3.15 + image: ghcr.io/mastodon/mastodon:v4.3.16 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 94de9035da..7b898ce5b6 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 15 + 16 end def default_prerelease