Compare commits

..

42 Commits

Author SHA1 Message Date
Claire
eca84d1c15 Merge pull request #3359 from ClearlyClaire/glitch-soc/merge-4.3
Merge upstream changes up to fa553d8484 into stable-4.3
2026-01-20 16:25:27 +01:00
Claire
3bab7a5498 Merge commit 'fa553d848417ffe48f23794f7b798b87eb57477f' into glitch-soc/merge-4.3 2026-01-20 16:00:33 +01:00
Claire
fa553d8484 Bump version to v4.3.18 (#37548) 2026-01-20 15:53:53 +01:00
Claire
8beb150516 Merge commit from fork
* Add limit on inbox payload size

The 1MB limit is consistent with the limit we use when fetching remote resources

* Add limit to number of options from federated polls

* Add a limit to the number of federated profile fields

* Add limit on federated username length

* Add hard limits for federated display name and account bio

* Add hard limits for `alsoKnownAs` and `attributionDomains`

* Add hard limit on federated custom emoji shortcode

* Highlight most destructive limits and expand on their reasoning
2026-01-20 15:14:45 +01:00
Claire
d5050227e9 Merge commit from fork 2026-01-20 15:13:42 +01:00
Claire
0a58ed0303 Merge commit from fork 2026-01-20 15:13:09 +01:00
Claire
69b09edcd2 Merge commit from fork 2026-01-20 15:10:38 +01:00
Claire
9cd5a9ad6a Merge pull request #3355 from ClearlyClaire/glitch-soc/merge-4.3
Merge upstream changes up to afd4420989 into stable-4.3
2026-01-19 19:30:28 +01:00
Claire
6704bf7834 Merge commit 'afd44209891b0d5129d41cf471f259a1eb85df2f' into glitch-soc/merge-4.3 2026-01-19 18:53:15 +01:00
Claire
afd4420989 Disable rubocop rule disabled in main 2026-01-19 11:37:53 +01:00
Claire
04ecb5c319 Fix FeedManager#filter_from_home error when handling a reblog of a deleted status (#37486) 2026-01-19 11:37:53 +01:00
Claire
b28fdf2d49 Simplify status batch removal SQL query (#37469) 2026-01-19 11:37:53 +01:00
Joshua Rogers
7c9e17c20a Fix Vary parsing in cache control enforcement (#37426) 2026-01-19 11:37:53 +01:00
Joshua Rogers
06bae4a936 Fix thread-unsafe ActivityPub activity dispatch (#37423) 2026-01-19 11:37:53 +01:00
Shlee
eff2d57cdb Fix SignatureParser accepting duplicate parameters in HTTP Signature header (#37375)
Co-authored-by: Claire <claire.github-309c@sitedethib.com>
2026-01-19 11:37:53 +01:00
Shlee
444a360c11 SharedConnectionPool - NoMethodError: undefined method 'site' for Integer (#37374) 2026-01-19 11:37:53 +01:00
Claire
94ee9c5a29 Update SECURITY.md (#37503) 2026-01-15 14:17:34 +01:00
Claire
2d567a78ae Merge pull request #3338 from ClearlyClaire/glitch-soc/merge-4.3
Merge upstream changes up to 004f3aa235 into stable-4.3
2026-01-07 15:22:50 +01:00
Claire
80062846d6 Merge commit '004f3aa2356e64a463feff26dda3ed41547ed718' into glitch-soc/merge-4.3 2026-01-07 14:49:53 +01:00
Claire
004f3aa235 Bump version to v4.3.17 (#37411) 2026-01-07 14:45:06 +01:00
Claire
b2bcd34486 Merge commit from fork 2026-01-07 14:15:13 +01:00
Claire
0f4e8a6240 Merge commit from fork 2026-01-07 14:14:42 +01:00
Claire
4467365c34 Merge pull request #3326 from ClearlyClaire/glitch-soc/merge-4.3
Merge upstream changes up to 8a1965e522 into stable-4.3
2025-12-28 19:47:42 +01:00
Claire
b76d94bc4b Merge commit '8a1965e522834fac0b8ad28c1bea7a786f4ef181' into glitch-soc/merge-4.3 2025-12-28 11:31:14 +01:00
Claire
8a1965e522 Fix mentions of domain-blocked users being processed (#37257) 2025-12-19 11:00:21 +01:00
Claire
eeadda0e71 Merge pull request #3310 from ClearlyClaire/glitch-soc/merge-4.3
Merge upstream changes up to 770cf42085 into stable-4.3
2025-12-08 17:31:12 +01:00
Echo
96a5d173f1 [Glitch] Fixes YouTube embeds
Port 9bc9ebc59e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-12-08 17:07:01 +01:00
Bruno Viveiros
5057735a54 [Glitch] fix: YouTube iframe being able to start at a defined time
Port bdff970a5e to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
2025-12-08 17:03:04 +01:00
Claire
27b74ba7b8 Merge commit '770cf420854b03c1994ed21baa32a05b75ed34b4' into glitch-soc/merge-4.3 2025-12-08 17:01:25 +01:00
Claire
770cf42085 Bump version to v4.3.16 (#37163) 2025-12-08 16:20:21 +01:00
Claire
ef1af11956 Merge commit from fork 2025-12-08 15:44:08 +01:00
Echo
140e011e73 Fixes YouTube embeds (#37126) 2025-12-05 11:15:00 +01:00
Bruno Viveiros
43f8760c95 fix: YouTube iframe being able to start at a defined time (#26584) 2025-12-05 11:15:00 +01:00
Claire
473c112dae Increase HTTP read timeout for expensive S3 batch delete operation (#37004) 2025-12-05 11:15:00 +01:00
Matt Jankowski
821e735524 Suggest ES image version 7.17.29 in docker compose (#36972) 2025-12-05 11:15:00 +01:00
Claire
167c46adce Merge pull request #3293 from ClearlyClaire/glitch-soc/merge-4.3
Merge upstream changes up to 3260d25a8e into stable-4.3
2025-11-20 15:25:26 +01:00
Claire
7040d14476 Merge commit '3260d25a8e77635aa7ab874c9ca9acf51dfb36fb' into glitch-soc/merge-4.3 2025-11-20 15:01:17 +01:00
Claire
3260d25a8e Bump version to v4.3.15 (#36947) 2025-11-20 14:41:15 +01:00
Claire
b635c419fc Update dependency glob (#36943) 2025-11-19 16:29:53 +01:00
Shugo Maeda
d2f1767b81 Fix ArgumentError of tootctl upgrade storage-schema (#36914) 2025-11-19 15:20:08 +01:00
Claire
9636fc22cc Fix Update importing old previously-unknown activities and treating them as recent ones (#36848) 2025-11-19 15:20:08 +01:00
Claire
3924b33914 Update security policy for 4.3 (#36756) 2025-11-06 14:58:16 +01:00
48 changed files with 272 additions and 80 deletions

View File

@@ -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:

View File

@@ -2,6 +2,53 @@
All notable changes to this project will be documented in this file.
## [4.3.18] - 2026-01-20
### Security
- Fix missing limits on various federated properties [GHSA-gg8q-rcg7-p79g](https://github.com/mastodon/mastodon/security/advisories/GHSA-gg8q-rcg7-p79g)
- Fix remote user suspension bypass [GHSA-5h2f-wg8j-xqwp](https://github.com/mastodon/mastodon/security/advisories/GHSA-5h2f-wg8j-xqwp)
- Fix missing length limits on some user-provided fields [GHSA-6x3w-9g92-gvf3](https://github.com/mastodon/mastodon/security/advisories/GHSA-6x3w-9g92-gvf3)
- Fix missing access check for push notification settings update [GHSA-f3q8-7vw3-69v4](https://github.com/mastodon/mastodon/security/advisories/GHSA-f3q8-7vw3-69v4)
### Fixed
- Fix `FeedManager#filter_from_home` error when handling a reblog of a deleted status (#37486 by @ClearlyClaire)
- Fix needlessly complicated SQL query in status batch removal (#37469 by @ClearlyClaire)
- Fix `Vary` parsing in cache control enforcement (#37426 by @MegaManSec)
- Fix thread-unsafe ActivityPub activity dispatch (#37423 by @MegaManSec)
- Fix SignatureParser accepting duplicate parameters in HTTP Signature header (#37375 by @shleeable)
## [4.3.17] - 2026-01-07
### Security
- Fix SSRF protection bypass ([GHSA](https://github.com/mastodon/mastodon/security/advisories/GHSA-xfrj-c749-jxxq))
- Fix missing ownership check in severed relationships controller ([GHSA](https://github.com/mastodon/mastodon/security/advisories/GHSA-ww85-x9cp-5v24))
### Fixed
- Fix mentions of domain-blocked users being processed (#37257 by @ClearlyClaire)
## [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
- Fix `tootctl upgrade storage-schema` failing with `ArgumentError` (#36914 by @shugo)
- Fix old previously-undiscovered posts being treated as new when receiving an `Update` (#36848 by @ClearlyClaire)
## [4.3.14] - 2025-10-13
### Security

View File

@@ -47,3 +47,22 @@ Mastodon requires all `POST` requests to be signed, and MAY require `GET` reques
### Additional documentation
- [Mastodon documentation](https://docs.joinmastodon.org/)
## Size limits
Mastodon imposes a few hard limits on federated content.
These limits are intended to be very generous and way above what the Mastodon user experience is optimized for, so as to accomodate future changes and unusual or unforeseen usage patterns, while still providing some limits for performance reasons.
The following table attempts to summary those limits.
| Limited property | Size limit | Consequence of exceeding the limit |
| ------------------------------------------------------------- | ---------- | ---------------------------------- |
| Serialized JSON-LD | 1MB | **Activity is rejected/dropped** |
| Profile fields (actor `PropertyValue` attachments) name/value | 2047 | Field name/value is truncated |
| Number of profile fields (actor `PropertyValue` attachments) | 50 | Fields list is truncated |
| Poll options (number of `anyOf`/`oneOf` in a `Question`) | 500 | Items list is truncated |
| Account username (actor `preferredUsername`) length | 2048 | **Actor will be rejected** |
| Account display name (actor `name`) length | 2048 | Display name will be truncated |
| Account note (actor `summary`) length | 20kB | Account note will be truncated |
| Account `attributionDomains` | 256 | List will be truncated |
| Account aliases (actor `alsoKnownAs`) | 256 | List will be truncated |
| Custom emoji shortcode (`Emoji` `name`) | 2048 | Emoji will be rejected |

View File

@@ -16,6 +16,5 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
| Version | Supported |
| ------- | ---------------- |
| 4.4.x | Yes |
| 4.3.x | Yes |
| 4.2.x | Until 2026-01-08 |
| < 4.2 | No |
| 4.3.x | Until 2026-05-06 |
| < 4.3 | No |

View File

@@ -3,6 +3,7 @@
class ActivityPub::InboxesController < ActivityPub::BaseController
include JsonLdHelper
before_action :skip_large_payload
before_action :skip_unknown_actor_activity
before_action :require_actor_signature!
skip_before_action :authenticate_user!
@@ -16,6 +17,10 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
private
def skip_large_payload
head 413 if request.content_length > ActivityPub::Activity::MAX_JSON_SIZE
end
def skip_unknown_actor_activity
head 202 if unknown_affected_account?
end

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -127,7 +127,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

View File

@@ -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

View File

@@ -55,7 +55,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
end
def set_push_subscription
@push_subscription = ::Web::PushSubscription.find(params[:id])
@push_subscription = ::Web::PushSubscription.where(user_id: active_session.user_id).find(params[:id])
end
def subscription_params

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -27,7 +27,7 @@ class SeveredRelationshipsController < ApplicationController
private
def set_event
@event = AccountRelationshipSeveranceEvent.find(params[:id])
@event = AccountRelationshipSeveranceEvent.where(account: current_account).find(params[:id])
end
def following_data

View File

@@ -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

View File

@@ -26,18 +26,23 @@ 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);
iframeUrl.searchParams.set('autoplay', 1);
iframeUrl.searchParams.set('auto_play', 1);
if (providerName === 'YouTube') {
iframeUrl.searchParams.set('start', startTime || '');
iframe.referrerPolicy = 'strict-origin-when-cross-origin';
}
iframe.src += 'autoplay=1&auto_play=1';
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
@@ -103,7 +108,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 (
<div

View File

@@ -37,18 +37,23 @@ 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);
iframeUrl.searchParams.set('autoplay', 1);
iframeUrl.searchParams.set('auto_play', 1);
if (providerName === 'YouTube') {
iframeUrl.searchParams.set('start', startTime || '');
iframe.referrerPolicy = 'strict-origin-when-cross-origin';
}
iframe.src += 'autoplay=1&auto_play=1';
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 +119,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 (
<div

View File

@@ -5,6 +5,7 @@ class ActivityPub::Activity
include Redisable
include Lockable
MAX_JSON_SIZE = 1.megabyte
SUPPORTED_TYPES = %w(Note Question).freeze
CONVERTED_TYPES = %w(Image Audio Video Article Page Event).freeze
@@ -22,13 +23,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'

View File

@@ -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,10 @@ 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
# Also ignore unknown objects from suspended users for the same reasons
return if @status.nil? && (@account.suspended? || object_too_old?)
# We may be getting `Create` and `Update` out of order
@status ||= ActivityPub::Activity::Create.new(@json, @account, **@options).perform
@@ -35,4 +42,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

View File

@@ -3,6 +3,10 @@
class ActivityPub::Parser::PollParser
include JsonLdHelper
# Limit the number of items for performance purposes.
# We truncate rather than error out to avoid missing the post entirely.
MAX_ITEMS = 500
def initialize(json)
@json = json
end
@@ -48,6 +52,6 @@ class ActivityPub::Parser::PollParser
private
def items
@json['anyOf'] || @json['oneOf']
(@json['anyOf'] || @json['oneOf'])&.take(MAX_ITEMS)
end
end

View File

@@ -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

View File

@@ -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)])

View File

@@ -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)

View File

@@ -1,9 +1,7 @@
# frozen_string_literal: true
module PrivateAddressCheck
module_function
CIDR_LIST = [
IP4_CIDR_LIST = [
IPAddr.new('0.0.0.0/8'), # Current network (only valid as source address)
IPAddr.new('100.64.0.0/10'), # Shared Address Space
IPAddr.new('172.16.0.0/12'), # Private network
@@ -16,6 +14,9 @@ module PrivateAddressCheck
IPAddr.new('224.0.0.0/4'), # IP multicast (former Class D network)
IPAddr.new('240.0.0.0/4'), # Reserved (former Class E network)
IPAddr.new('255.255.255.255'), # Broadcast
].freeze
CIDR_LIST = (IP4_CIDR_LIST + IP4_CIDR_LIST.map(&:ipv4_mapped) + [
IPAddr.new('64:ff9b::/96'), # IPv4/IPv6 translation (RFC 6052)
IPAddr.new('100::/64'), # Discard prefix (RFC 6666)
IPAddr.new('2001::/32'), # Teredo tunneling
@@ -25,7 +26,9 @@ module PrivateAddressCheck
IPAddr.new('2002::/16'), # 6to4
IPAddr.new('fc00::/7'), # Unique local address
IPAddr.new('ff00::/8'), # Multicast
].freeze
]).freeze
module_function
def private_address?(address)
address.private? || address.loopback? || address.link_local? || CIDR_LIST.any? { |cidr| cidr.include?(address) }

View File

@@ -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?

View File

@@ -76,6 +76,13 @@ class Account < ApplicationRecord
DISPLAY_NAME_LENGTH_LIMIT = (ENV['MAX_DISPLAY_NAME_CHARS'] || 30).to_i
NOTE_LENGTH_LIMIT = (ENV['MAX_BIO_CHARS'] || 500).to_i
# Hard limits for federated content
USERNAME_LENGTH_HARD_LIMIT = 2048
DISPLAY_NAME_LENGTH_HARD_LIMIT = 2048
NOTE_LENGTH_HARD_LIMIT = 20.kilobytes
ATTRIBUTION_DOMAINS_HARD_LIMIT = 256
ALSO_KNOWN_AS_HARD_LIMIT = 256
AUTOMATED_ACTOR_TYPES = %w(Application Service).freeze
include Attachmentable # Load prior to Avatar & Header concerns
@@ -103,7 +110,7 @@ class Account < ApplicationRecord
validates_with UniqueUsernameValidator, if: -> { will_save_change_to_username? }
# Remote user validations, also applies to internal actors
validates :username, format: { with: USERNAME_ONLY_RE }, if: -> { (!local? || actor_type == 'Application') && will_save_change_to_username? }
validates :username, format: { with: USERNAME_ONLY_RE }, length: { maximum: USERNAME_LENGTH_HARD_LIMIT }, if: -> { (!local? || actor_type == 'Application') && will_save_change_to_username? }
# Remote user validations
validates :uri, presence: true, unless: :local?, on: :create

View File

@@ -27,6 +27,8 @@ class CustomEmoji < ApplicationRecord
LOCAL_LIMIT = (ENV['MAX_EMOJI_SIZE'] || 256.kilobytes).to_i
LIMIT = [LOCAL_LIMIT, (ENV['MAX_REMOTE_EMOJI_SIZE'] || 256.kilobytes).to_i].max
MINIMUM_SHORTCODE_SIZE = 2
MAX_SHORTCODE_SIZE = 128
MAX_FEDERATED_SHORTCODE_SIZE = 2048
SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}'
@@ -48,7 +50,8 @@ class CustomEmoji < ApplicationRecord
validates_attachment :image, content_type: { content_type: IMAGE_MIME_TYPES }, presence: true
validates_attachment_size :image, less_than: LIMIT, unless: :local?
validates_attachment_size :image, less_than: LOCAL_LIMIT, if: :local?
validates :shortcode, uniqueness: { scope: :domain }, format: { with: SHORTCODE_ONLY_RE }, length: { minimum: MINIMUM_SHORTCODE_SIZE }
validates :shortcode, uniqueness: { scope: :domain }, format: { with: SHORTCODE_ONLY_RE }, length: { minimum: MINIMUM_SHORTCODE_SIZE, maximum: MAX_FEDERATED_SHORTCODE_SIZE }
validates :shortcode, length: { maximum: MAX_SHORTCODE_SIZE }, if: :local?
scope :local, -> { where(domain: nil) }
scope :remote, -> { where.not(domain: nil) }

View File

@@ -30,6 +30,8 @@ class CustomFilter < ApplicationRecord
EXPIRATION_DURATIONS = [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].freeze
TITLE_LENGTH_LIMIT = 256
include Expireable
include Redisable
@@ -41,6 +43,7 @@ class CustomFilter < ApplicationRecord
accepts_nested_attributes_for :keywords, reject_if: :all_blank, allow_destroy: true
validates :title, :context, presence: true
validates :title, length: { maximum: TITLE_LENGTH_LIMIT }
validate :context_must_be_valid
normalizes :context, with: ->(context) { context.map(&:strip).filter_map(&:presence) }

View File

@@ -17,7 +17,9 @@ class CustomFilterKeyword < ApplicationRecord
belongs_to :custom_filter
validates :keyword, presence: true
KEYWORD_LENGTH_LIMIT = 512
validates :keyword, presence: true, length: { maximum: KEYWORD_LENGTH_LIMIT }
alias_attribute :phrase, :keyword

View File

@@ -17,6 +17,7 @@ class List < ApplicationRecord
include Paginable
PER_ACCOUNT_LIMIT = 50
TITLE_LENGTH_LIMIT = 256
enum :replies_policy, { list: 0, followed: 1, none: 2 }, prefix: :show
@@ -26,7 +27,7 @@ class List < ApplicationRecord
has_many :accounts, through: :list_accounts
has_many :active_accounts, -> { merge(ListAccount.active) }, through: :list_accounts, source: :account
validates :title, presence: true
validates :title, presence: true, length: { maximum: TITLE_LENGTH_LIMIT }
validate :validate_account_lists_limit, on: :create

View File

@@ -6,6 +6,7 @@ class ActivityPub::ProcessAccountService < BaseService
include Redisable
include Lockable
MAX_PROFILE_FIELDS = 50
SUBDOMAINS_RATELIMIT = 10
DISCOVERIES_PER_REQUEST = 400
@@ -122,15 +123,15 @@ class ActivityPub::ProcessAccountService < BaseService
def set_immediate_attributes!
@account.featured_collection_url = valid_collection_uri(@json['featured'])
@account.display_name = @json['name'] || ''
@account.note = @json['summary'] || ''
@account.display_name = (@json['name'] || '')[0...(Account::DISPLAY_NAME_LENGTH_HARD_LIMIT)]
@account.note = (@json['summary'] || '')[0...(Account::NOTE_LENGTH_HARD_LIMIT)]
@account.locked = @json['manuallyApprovesFollowers'] || false
@account.fields = property_values || {}
@account.also_known_as = as_array(@json['alsoKnownAs'] || []).map { |item| value_or_id(item) }
@account.also_known_as = as_array(@json['alsoKnownAs'] || []).take(Account::ALSO_KNOWN_AS_HARD_LIMIT).map { |item| value_or_id(item) }
@account.discoverable = @json['discoverable'] || false
@account.indexable = @json['indexable'] || false
@account.memorial = @json['memorial'] || false
@account.attribution_domains = as_array(@json['attributionDomains'] || []).map { |item| value_or_id(item) }
@account.attribution_domains = as_array(@json['attributionDomains'] || []).take(Account::ATTRIBUTION_DOMAINS_HARD_LIMIT).map { |item| value_or_id(item) }
end
def set_fetchable_key!
@@ -251,7 +252,10 @@ class ActivityPub::ProcessAccountService < BaseService
def property_values
return unless @json['attachment'].is_a?(Array)
as_array(@json['attachment']).select { |attachment| attachment['type'] == 'PropertyValue' }.map { |attachment| attachment.slice('name', 'value') }
as_array(@json['attachment'])
.select { |attachment| attachment['type'] == 'PropertyValue' }
.take(MAX_PROFILE_FIELDS)
.map { |attachment| attachment.slice('name', 'value') }
end
def mismatching_origin?(url)

View File

@@ -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

View File

@@ -14,6 +14,8 @@ class FanOutOnWriteService < BaseService
@account = status.account
@options = options
return if @status.proper.account.suspended?
check_race_condition!
warm_payload_cache!

View File

@@ -71,7 +71,7 @@ class ProcessMentionsService < BaseService
# Make sure we never mention blocked accounts
unless @current_mentions.empty?
mentioned_domains = @current_mentions.filter_map { |m| m.account.domain }.uniq
blocked_domains = Set.new(mentioned_domains.empty? ? [] : AccountDomainBlock.where(account_id: @status.account_id, domain: mentioned_domains))
blocked_domains = Set.new(mentioned_domains.empty? ? [] : AccountDomainBlock.where(account_id: @status.account_id, domain: mentioned_domains).pluck(:domain))
mentioned_account_ids = @current_mentions.map(&:account_id)
blocked_account_ids = Set.new(@status.account.block_relationships.where(target_account_id: mentioned_account_ids).pluck(:target_account_id))

View File

@@ -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"
@@ -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/glitch-soc/mastodon:v4.3.14
image: ghcr.io/glitch-soc/mastodon:v4.3.18
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/glitch-soc/mastodon-streaming:v4.3.14
image: ghcr.io/glitch-soc/mastodon-streaming:v4.3.18
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/glitch-soc/mastodon:v4.3.14
image: ghcr.io/glitch-soc/mastodon:v4.3.18
restart: always
env_file: .env.production
command: bundle exec sidekiq

View File

@@ -123,12 +123,12 @@ module Mastodon::CLI
progress.log("Moving #{previous_path} to #{upgraded_path}") if options[:verbose]
begin
move_previous_to_upgraded
move_previous_to_upgraded(previous_path, upgraded_path)
rescue => e
progress.log(pastel.red("Error processing #{previous_path}: #{e}"))
success = false
remove_directory
remove_directory(upgraded_path)
end
end

View File

@@ -13,7 +13,7 @@ module Mastodon
end
def patch
14
18
end
def default_prerelease

View File

@@ -0,0 +1,20 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe PrivateAddressCheck do
describe 'private_address?' do
it 'returns true for private addresses' do
# rubocop:disable RSpec/ExpectActual
expect(
[
'192.168.1.7',
'0.0.0.0',
'127.0.0.1',
'::ffff:0.0.0.1',
]
).to all satisfy('return true') { |addr| described_class.private_address?(IPAddr.new(addr)) }
# rubocop:enable RSpec/ExpectActual
end
end
end

View File

@@ -3,9 +3,10 @@
require 'rails_helper'
RSpec.describe 'Severed Relationships' do
let(:account_rs_event) { Fabricate :account_relationship_severance_event }
let(:account_rs_event) { Fabricate(:account_relationship_severance_event) }
let(:user) { account_rs_event.account.user }
before { sign_in Fabricate(:user) }
before { sign_in user }
describe 'GET /severed_relationships/:id/following' do
it 'returns a CSV file with correct data' do
@@ -22,6 +23,17 @@ RSpec.describe 'Severed Relationships' do
expect(response.body)
.to include('Account address')
end
context 'when the user is not the subject of the event' do
let(:user) { Fabricate(:user) }
it 'returns a 404' do
get following_severed_relationship_path(account_rs_event, format: :csv)
expect(response)
.to have_http_status(404)
end
end
end
describe 'GET /severed_relationships/:id/followers' do
@@ -39,5 +51,16 @@ RSpec.describe 'Severed Relationships' do
expect(response.body)
.to include('Account address')
end
context 'when the user is not the subject of the event' do
let(:user) { Fabricate(:user) }
it 'returns a 404' do
get followers_severed_relationship_path(account_rs_event, format: :csv)
expect(response)
.to have_http_status(404)
end
end
end
end

View File

@@ -8,9 +8,9 @@ RSpec.describe ProcessMentionsService do
let(:account) { Fabricate(:account, username: 'alice') }
context 'when mentions contain blocked accounts' do
let(:non_blocked_account) { Fabricate(:account) }
let(:individually_blocked_account) { Fabricate(:account) }
let(:domain_blocked_account) { Fabricate(:account, domain: 'evil.com') }
let!(:non_blocked_account) { Fabricate(:account) }
let!(:individually_blocked_account) { Fabricate(:account) }
let!(:domain_blocked_account) { Fabricate(:account, domain: 'evil.com', protocol: :activitypub) }
let(:status) { Fabricate(:status, account: account, text: "Hello @#{non_blocked_account.acct} @#{individually_blocked_account.acct} @#{domain_blocked_account.acct}", visibility: :public) }
before do

View File

@@ -9087,8 +9087,8 @@ __metadata:
linkType: hard
"glob@npm:^10.2.2, glob@npm:^10.2.6, glob@npm:^10.3.10":
version: 10.4.5
resolution: "glob@npm:10.4.5"
version: 10.5.0
resolution: "glob@npm:10.5.0"
dependencies:
foreground-child: "npm:^3.1.0"
jackspeak: "npm:^3.1.2"
@@ -9098,7 +9098,7 @@ __metadata:
path-scurry: "npm:^1.11.1"
bin:
glob: dist/esm/bin.mjs
checksum: 10c0/19a9759ea77b8e3ca0a43c2f07ecddc2ad46216b786bb8f993c445aee80d345925a21e5280c7b7c6c59e860a0154b84e4b2b60321fea92cd3c56b4a7489f160e
checksum: 10c0/100705eddbde6323e7b35e1d1ac28bcb58322095bd8e63a7d0bef1a2cdafe0d0f7922a981b2b48369a4f8c1b077be5c171804534c3509dfe950dde15fbe6d828
languageName: node
linkType: hard