From 617926742cd48aa2eb33beae68edddb21578575a Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Jan 2026 14:17:38 +0100 Subject: [PATCH 01/15] Update SECURITY.md (#37505) --- SECURITY.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 12052652e6..e5790a66fa 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -18,5 +18,4 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through | 4.5.x | Yes | | 4.4.x | Yes | | 4.3.x | Until 2026-05-06 | -| 4.2.x | Until 2026-01-08 | -| < 4.2 | No | +| < 4.3 | No | From 53437c4653e891d0679c505f70861710fa0d2cdf Mon Sep 17 00:00:00 2001 From: diondiondion Date: Fri, 19 Dec 2025 09:39:25 +0100 Subject: [PATCH 02/15] Fix mobile admin sidebar displaying under batch table toolbar (#37307) --- app/javascript/styles/mastodon/admin.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index b5cb56d425..6c5e5b1c9d 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -163,6 +163,7 @@ $content-width: 840px; width: 100%; max-width: $content-width; flex: 1 1 auto; + isolation: isolate; } @media screen and (max-width: ($content-width + $sidebar-width)) { From f354bbe8aaad0b51062d64a0d6d51f47e0c92e55 Mon Sep 17 00:00:00 2001 From: Echo Date: Fri, 19 Dec 2025 14:43:27 +0100 Subject: [PATCH 03/15] Remove trailing variation selector code for legacy emojis (#37320) --- app/javascript/mastodon/features/emoji/normalize.test.ts | 1 + app/javascript/mastodon/features/emoji/normalize.ts | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/app/javascript/mastodon/features/emoji/normalize.test.ts b/app/javascript/mastodon/features/emoji/normalize.test.ts index b4c7669961..8222ab81e5 100644 --- a/app/javascript/mastodon/features/emoji/normalize.test.ts +++ b/app/javascript/mastodon/features/emoji/normalize.test.ts @@ -33,6 +33,7 @@ describe('emojiToUnicodeHex', () => { ['⚫', '26AB'], ['🖤', '1F5A4'], ['💀', '1F480'], + ['❤️', '2764'], // Checks for trailing variation selector removal. ['💂‍♂️', '1F482-200D-2642-FE0F'], ] as const)( 'emojiToUnicodeHex converts %s to %s', diff --git a/app/javascript/mastodon/features/emoji/normalize.ts b/app/javascript/mastodon/features/emoji/normalize.ts index a09505e97f..59304c89e8 100644 --- a/app/javascript/mastodon/features/emoji/normalize.ts +++ b/app/javascript/mastodon/features/emoji/normalize.ts @@ -30,6 +30,12 @@ export function emojiToUnicodeHex(emoji: string): string { codes.push(code); } } + + // Handles how Emojibase removes the variation selector for single code emojis. + // See: https://emojibase.dev/docs/spec/#merged-variation-selectors + if (codes.at(1) === VARIATION_SELECTOR_CODE && codes.length === 2) { + codes.pop(); + } return hexNumbersToString(codes); } From 1eb8d1b967132e13263a784f8b8f1fa007ea664f Mon Sep 17 00:00:00 2001 From: Shlee Date: Fri, 9 Jan 2026 23:20:50 +0700 Subject: [PATCH 04/15] SharedConnectionPool - NoMethodError: undefined method 'site' for Integer (#37374) --- app/lib/connection_pool/shared_connection_pool.rb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/lib/connection_pool/shared_connection_pool.rb b/app/lib/connection_pool/shared_connection_pool.rb index 1cfcc5823b..c7dd747eda 100644 --- a/app/lib/connection_pool/shared_connection_pool.rb +++ b/app/lib/connection_pool/shared_connection_pool.rb @@ -41,12 +41,17 @@ class ConnectionPool::SharedConnectionPool < ConnectionPool # ConnectionPool 2.4+ calls `checkin(force: true)` after fork. # When this happens, we should remove all connections from Thread.current - ::Thread.current.keys.each do |name| # rubocop:disable Style/HashEachMethods - next unless name.to_s.start_with?("#{@key}-") + connection_keys = ::Thread.current.keys.select { |key| key.to_s.start_with?("#{@key}-") && !key.to_s.start_with?("#{@key_count}-") } + count_keys = ::Thread.current.keys.select { |key| key.to_s.start_with?("#{@key_count}-") } - @available.push(::Thread.current[name]) - ::Thread.current[name] = nil + connection_keys.each do |key| + @available.push(::Thread.current[key]) + ::Thread.current[key] = nil end + count_keys.each do |key| + ::Thread.current[key] = nil + end + elsif ::Thread.current[key(preferred_tag)] if ::Thread.current[key_count(preferred_tag)] == 1 @available.push(::Thread.current[key(preferred_tag)]) From adea0b7b31995a9eb7f804440a67e6906de08bfb Mon Sep 17 00:00:00 2001 From: Shlee Date: Thu, 8 Jan 2026 17:47:53 +0700 Subject: [PATCH 05/15] Fix SignatureParser accepting duplicate parameters in HTTP Signature header (#37375) Co-authored-by: Claire --- app/lib/signature_parser.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/lib/signature_parser.rb b/app/lib/signature_parser.rb index 7a75080d98..00a45b8251 100644 --- a/app/lib/signature_parser.rb +++ b/app/lib/signature_parser.rb @@ -25,9 +25,13 @@ class SignatureParser # Use `skip` instead of `scan` as we only care about the subgroups while scanner.skip(PARAM_RE) + key = scanner[:key] + # Detect a duplicate key + raise Mastodon::SignatureVerificationError, 'Error parsing signature with duplicate keys' if params.key?(key) + # This is not actually correct with regards to quoted pairs, but it's consistent # with our previous implementation, and good enough in practice. - params[scanner[:key]] = scanner[:value] || scanner[:quoted_value][1...-1] + params[key] = scanner[:value] || scanner[:quoted_value][1...-1] scanner.skip(/\s*/) return params if scanner.eos? From deeaf50472cafad984fe5107c32979cb8f80e648 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Wed, 7 Jan 2026 16:39:22 +0100 Subject: [PATCH 06/15] Fix URI generation for reblogs by accounts with numerical AP ids (#37415) --- app/lib/activitypub/tag_manager.rb | 2 +- spec/lib/activitypub/tag_manager_spec.rb | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 43574d3657..3174d1792e 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -50,7 +50,7 @@ class ActivityPub::TagManager context_url(target) unless target.parent_account_id.nil? || target.parent_status_id.nil? when :note, :comment, :activity if target.account.numeric_ap_id? - return activity_ap_account_status_url(target.account, target) if target.reblog? + return activity_ap_account_status_url(target.account.id, target) if target.reblog? ap_account_status_url(target.account.id, target) else diff --git a/spec/lib/activitypub/tag_manager_spec.rb b/spec/lib/activitypub/tag_manager_spec.rb index cad46ad903..6cbb58055e 100644 --- a/spec/lib/activitypub/tag_manager_spec.rb +++ b/spec/lib/activitypub/tag_manager_spec.rb @@ -128,6 +128,28 @@ RSpec.describe ActivityPub::TagManager do .to eq("#{host_prefix}/ap/users/#{status.account.id}/statuses/#{status.id}") end end + + context 'with a reblog' do + let(:status) { Fabricate(:status, account:, reblog: Fabricate(:status)) } + + context 'when using a numeric ID based scheme' do + let(:account) { Fabricate(:account, id_scheme: :numeric_ap_id) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.uri_for(status)) + .to eq("#{host_prefix}/ap/users/#{status.account.id}/statuses/#{status.id}/activity") + end + end + + context 'when using the legacy username based scheme' do + let(:account) { Fabricate(:account, id_scheme: :username_ap_id) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.uri_for(status)) + .to eq("#{host_prefix}/users/#{status.account.username}/statuses/#{status.id}/activity") + end + end + end end context 'with a remote status' do From 0cda0689184277c7e58ec021ab062c5a3e1d589b Mon Sep 17 00:00:00 2001 From: Joshua Rogers Date: Sat, 10 Jan 2026 03:20:59 +1100 Subject: [PATCH 07/15] Fix thread-unsafe ActivityPub activity dispatch (#37423) --- app/lib/activitypub/activity.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 64ee9acd05..d07d1c2f24 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -21,14 +21,13 @@ class ActivityPub::Activity class << self def factory(json, account, **) - @json = json - klass&.new(json, account, **) + klass_for(json)&.new(json, account, **) end private - def klass - case @json['type'] + def klass_for(json) + case json['type'] when 'Create' ActivityPub::Activity::Create when 'Announce' From 57f658dc5cc0da37fb29e684fe1ad2ab3d0a15c5 Mon Sep 17 00:00:00 2001 From: Joshua Rogers Date: Sat, 10 Jan 2026 03:21:05 +1100 Subject: [PATCH 08/15] Fix arg order for non_matching_uri_hosts? call in QuoteRequest (#37425) --- app/lib/activitypub/activity/quote_request.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/activitypub/activity/quote_request.rb b/app/lib/activitypub/activity/quote_request.rb index 12f48ebb2b..46c45cde27 100644 --- a/app/lib/activitypub/activity/quote_request.rb +++ b/app/lib/activitypub/activity/quote_request.rb @@ -47,7 +47,7 @@ class ActivityPub::Activity::QuoteRequest < ActivityPub::Activity # NOTE: Replacing the object's context by that of the parent activity is # not sound, but it's consistent with the rest of the codebase instrument = @json['instrument'].merge({ '@context' => @json['@context'] }) - return if non_matching_uri_hosts?(instrument['id'], @account.uri) + return if non_matching_uri_hosts?(@account.uri, instrument['id']) ActivityPub::FetchRemoteStatusService.new.call(instrument['id'], prefetched_body: instrument, on_behalf_of: quoted_status.account, request_id: @options[:request_id]) end From f7b6e571512af9a1558663fd4dd379fee8cf6d38 Mon Sep 17 00:00:00 2001 From: Joshua Rogers Date: Sat, 10 Jan 2026 03:21:18 +1100 Subject: [PATCH 09/15] Fix Vary parsing in cache control enforcement (#37426) --- app/controllers/concerns/cache_concern.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/concerns/cache_concern.rb b/app/controllers/concerns/cache_concern.rb index b1b09f2aab..3527cdaca0 100644 --- a/app/controllers/concerns/cache_concern.rb +++ b/app/controllers/concerns/cache_concern.rb @@ -19,7 +19,7 @@ module CacheConcern # from being used as cache keys, while allowing to `Vary` on them (to not serve # anonymous cached data to authenticated requests when authentication matters) def enforce_cache_control! - vary = response.headers['Vary']&.split&.map { |x| x.strip.downcase } + vary = response.headers['Vary'].to_s.split(',').map { |x| x.strip.downcase }.reject(&:empty?) return unless vary.present? && %w(cookie authorization signature).any? { |header| vary.include?(header) && request.headers[header].present? } response.cache_control.replace(private: true, no_store: true) From 57e0c6562ff01caabd2f0197263e17a80302b141 Mon Sep 17 00:00:00 2001 From: Shlee Date: Tue, 13 Jan 2026 17:40:08 +0700 Subject: [PATCH 10/15] Fix `quote_approval_policy` being reset to user defaults when omitted in status update (#37436) --- app/controllers/api/v1/statuses_controller.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 1c5fd9f2ae..25f9dec252 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -106,9 +106,7 @@ class Api::V1::StatusesController < Api::BaseController @status = Status.where(account: current_account).find(params[:id]) authorize @status, :update? - UpdateStatusService.new.call( - @status, - current_account.id, + update_options = { text: status_params[:status], media_ids: status_params[:media_ids], media_attributes: status_params[:media_attributes], @@ -116,8 +114,11 @@ class Api::V1::StatusesController < Api::BaseController language: status_params[:language], spoiler_text: status_params[:spoiler_text], poll: status_params[:poll], - quote_approval_policy: quote_approval_policy - ) + } + + update_options[:quote_approval_policy] = quote_approval_policy if status_params[:quote_approval_policy].present? + + UpdateStatusService.new.call(@status, current_account.id, update_options) render json: @status, serializer: REST::StatusSerializer end From f1c9c89c396ac9437d91025dcbe590d92188625f Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 13 Jan 2026 11:21:55 -0500 Subject: [PATCH 11/15] Add spec for quote policy update change (#37474) --- spec/requests/api/v1/statuses_spec.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec/requests/api/v1/statuses_spec.rb b/spec/requests/api/v1/statuses_spec.rb index 2cb67272b7..a89be2aa25 100644 --- a/spec/requests/api/v1/statuses_spec.rb +++ b/spec/requests/api/v1/statuses_spec.rb @@ -508,6 +508,15 @@ RSpec.describe '/api/v1/statuses' do .to start_with('application/json') end end + + context 'when status has non-default quote policy and param is omitted' do + let(:status) { Fabricate(:status, account: user.account, quote_approval_policy: 'nobody') } + + it 'preserves existing quote approval policy' do + expect { subject } + .to_not(change { status.reload.quote_approval_policy }) + end + end end end From 85eda5b46f47b839917a157f0fa7ae3d3b230a7c Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 13 Jan 2026 11:18:26 +0100 Subject: [PATCH 12/15] Simplify status batch removal SQL query (#37469) --- app/services/batched_remove_status_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index 5d6ea2550e..826dbcc720 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -31,7 +31,7 @@ class BatchedRemoveStatusService < BaseService # transaction lock the database, but we use the delete method instead # of destroy to avoid all callbacks. We rely on foreign keys to # cascade the delete faster without loading the associations. - statuses_and_reblogs.each_slice(50) { |slice| Status.where(id: slice.map(&:id)).delete_all } + statuses_and_reblogs.each_slice(50) { |slice| Status.unscoped.where(id: slice.pluck(:id)).delete_all } # Since we skipped all callbacks, we also need to manually # deindex the statuses From ded7f50f2c2672879292ff571b2e531ea87f77e4 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 14 Jan 2026 11:51:23 +0100 Subject: [PATCH 13/15] Fix `FeedManager#filter_from_home` error when handling a reblog of a deleted status (#37486) --- app/lib/feed_manager.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 9c5c306e96..ab5ee106c7 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -450,6 +450,7 @@ class FeedManager return :filter if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?) return :skip_home if timeline_type != :list && crutches[:exclusive_list_users][status.account_id].present? return :filter if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language) + return :filter if status.reblog? && status.reblog.blank? check_for_blocks = crutches[:active_mentions][status.id] || [] check_for_blocks.push(status.account_id) From 3479b453e5f3522d591ebb7d2a3c4e332a3266f1 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Fri, 19 Dec 2025 09:39:25 +0100 Subject: [PATCH 14/15] [Glitch] Fix mobile admin sidebar displaying under batch table toolbar Port 53437c4653e891d0679c505f70861710fa0d2cdf to glitch-soc Signed-off-by: Claire --- app/javascript/flavours/glitch/styles/admin.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index 3199358730..7a36db424c 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -163,6 +163,7 @@ $content-width: 840px; width: 100%; max-width: $content-width; flex: 1 1 auto; + isolation: isolate; } @media screen and (max-width: ($content-width + $sidebar-width)) { From 25d572e9b9855fe85c5d9766b1b3a6762a85a93e Mon Sep 17 00:00:00 2001 From: Echo Date: Fri, 19 Dec 2025 14:43:27 +0100 Subject: [PATCH 15/15] [Glitch] Remove trailing variation selector code for legacy emojis Port f354bbe8aaad0b51062d64a0d6d51f47e0c92e55 to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/features/emoji/normalize.test.ts | 1 + app/javascript/flavours/glitch/features/emoji/normalize.ts | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/app/javascript/flavours/glitch/features/emoji/normalize.test.ts b/app/javascript/flavours/glitch/features/emoji/normalize.test.ts index b4c7669961..8222ab81e5 100644 --- a/app/javascript/flavours/glitch/features/emoji/normalize.test.ts +++ b/app/javascript/flavours/glitch/features/emoji/normalize.test.ts @@ -33,6 +33,7 @@ describe('emojiToUnicodeHex', () => { ['⚫', '26AB'], ['🖤', '1F5A4'], ['💀', '1F480'], + ['❤️', '2764'], // Checks for trailing variation selector removal. ['💂‍♂️', '1F482-200D-2642-FE0F'], ] as const)( 'emojiToUnicodeHex converts %s to %s', diff --git a/app/javascript/flavours/glitch/features/emoji/normalize.ts b/app/javascript/flavours/glitch/features/emoji/normalize.ts index f0a502dcb5..d8aa1c29f0 100644 --- a/app/javascript/flavours/glitch/features/emoji/normalize.ts +++ b/app/javascript/flavours/glitch/features/emoji/normalize.ts @@ -30,6 +30,12 @@ export function emojiToUnicodeHex(emoji: string): string { codes.push(code); } } + + // Handles how Emojibase removes the variation selector for single code emojis. + // See: https://emojibase.dev/docs/spec/#merged-variation-selectors + if (codes.at(1) === VARIATION_SELECTOR_CODE && codes.length === 2) { + codes.pop(); + } return hexNumbersToString(codes); }