diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 3322bce2bf..9d5f4d0a2d 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -21,11 +21,11 @@ Metrics/BlockNesting: # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/CyclomaticComplexity: - Max: 25 + Enabled: false # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/PerceivedComplexity: - Max: 27 + Enabled: false Rails/OutputSafety: Exclude: diff --git a/SECURITY.md b/SECURITY.md index 385c946512..bd8f5ab25d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -17,5 +17,4 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through | ------- | ---------------- | | 4.4.x | Yes | | 4.3.x | Until 2026-05-06 | -| 4.2.x | Until 2026-01-08 | -| < 4.2 | No | +| < 4.3 | No | diff --git a/app/controllers/concerns/cache_concern.rb b/app/controllers/concerns/cache_concern.rb index 1823b5b8ed..d43bdd904e 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) diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 322f3e27ad..f5b55fff6a 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -22,13 +22,13 @@ class ActivityPub::Activity class << self def factory(json, account, **options) @json = json - klass&.new(json, account, **options) + klass_for(json)&.new(json, account, **options) end private - def klass - case @json['type'] + def klass_for(json) + case json['type'] when 'Create' ActivityPub::Activity::Create when 'Announce' 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)]) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 9332aea2df..8473d348d7 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -491,6 +491,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) 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? diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index 6ce29d7b8b..7fd9ced772 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -34,7 +34,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