From 83fff75dbec141c2647b715c53f95118a61305ef Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 8 Oct 2025 15:14:16 +0200 Subject: [PATCH 01/10] Update dependency `rack` --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 985b4159d..6ff14fa3a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -533,7 +533,7 @@ GEM activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) - rack (2.2.17) + rack (2.2.19) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-cors (2.0.2) From 7a46c72d7a8177fe5d08b366a38f018932c76cf2 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 8 Oct 2025 15:14:35 +0200 Subject: [PATCH 02/10] Update dependency `uri` --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 6ff14fa3a..c5351a8c4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -769,7 +769,7 @@ GEM unf_ext unf_ext (0.0.8.2) unicode-display_width (2.4.2) - uri (0.12.4) + uri (0.12.5) validate_email (0.1.6) activemodel (>= 3.0) mail (>= 2.2.5) From d987c3629f7e58c98be6d32a4bf98a4b42b9fedd Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 10 Oct 2025 17:20:36 +0200 Subject: [PATCH 03/10] Ignore dotenv .local files in stable-4.2 (#36426) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 2bc8b18c8..f05eecfbf 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,6 @@ yarn-debug.log # Ignore Docker option files docker-compose.override.yml + +# Ignore dotenv .local files +.env*.local From 039bde19db2f51d4ac3e11d104f9bc8004c50d40 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 13 Oct 2025 10:52:05 +0200 Subject: [PATCH 04/10] Update dependency `rack` --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index c5351a8c4..e283d0207 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -533,7 +533,7 @@ GEM activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) - rack (2.2.19) + rack (2.2.20) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-cors (2.0.2) From 5226b757fe519a74151f30e4e5923a5ae9ed3f95 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 13 Oct 2025 10:52:48 +0200 Subject: [PATCH 05/10] Update dependency `openssl` --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index e283d0207..97ca604bd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -502,7 +502,7 @@ GEM validate_email validate_url webfinger (~> 1.2) - openssl (3.1.0) + openssl (3.1.2) openssl-signature_algorithm (1.3.0) openssl (> 2.0) orm_adapter (0.5.0) From 4bd193cdfec58bf2818d8d1f58328cc5c7f8eded Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 13 Oct 2025 14:19:14 +0200 Subject: [PATCH 06/10] Merge commit from fork * Streaming: Ensure disabled users cannot connect to streaming * Streaming: Disconnect when the user is disabled --------- Co-authored-by: Emelia Smith --- app/models/user.rb | 4 ++++ spec/models/user_spec.rb | 11 +++++++---- streaming/index.js | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 94d748453..08e3bf7ea 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -180,6 +180,10 @@ class User < ApplicationRecord def disable! update!(disabled: true) + + # This terminates all connections for the given account with the streaming + # server: + redis.publish("timeline:system:#{account.id}", Oj.dump(event: :kill)) end def enable! diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6c557f8c9..57e4f7f9e 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -448,12 +448,15 @@ RSpec.describe User do let(:current_sign_in_at) { Time.zone.now } - before do - user.disable! - end - it 'disables user' do + allow(redis).to receive(:publish) + + user.disable! + expect(user).to have_attributes(disabled: true) + + expect(redis) + .to have_received(:publish).with("timeline:system:#{user.account.id}", Oj.dump(event: :kill)).once end end diff --git a/streaming/index.js b/streaming/index.js index e599b1904..3fc5b3613 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -459,7 +459,7 @@ const startServer = async () => { return; } - client.query('SELECT oauth_access_tokens.id, oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, oauth_access_tokens.scopes, devices.device_id FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id LEFT OUTER JOIN devices ON oauth_access_tokens.id = devices.access_token_id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL LIMIT 1', [token], (err, result) => { + client.query('SELECT oauth_access_tokens.id, oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, oauth_access_tokens.scopes, devices.device_id FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id LEFT OUTER JOIN devices ON oauth_access_tokens.id = devices.access_token_id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL AND users.disabled IS FALSE LIMIT 1', [token], (err, result) => { done(); if (err) { From 40b619a91611dafe7b4f978c6c3341a1130e040f Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 13 Oct 2025 14:20:22 +0200 Subject: [PATCH 07/10] Merge commit from fork * Ensure tootctl revokes sessions, access tokens and web push subscriptions * Fix test coverage --------- Co-authored-by: Emelia Smith --- app/models/user.rb | 13 +++++++++---- lib/mastodon/cli/accounts.rb | 7 +++++-- spec/lib/mastodon/cli/accounts_spec.rb | 18 +++++++++++++----- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 08e3bf7ea..d58dc9ed1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -386,17 +386,22 @@ class User < ApplicationRecord end def reset_password! + # First, change password to something random, this revokes sessions and on-going access: + change_password!(SecureRandom.hex) + + # Finally, send a reset password prompt to the user + send_reset_password_instructions + end + + def change_password!(new_password) # First, change password to something random and deactivate all sessions transaction do - update(password: SecureRandom.hex) + update(password: new_password) session_activations.destroy_all end # Then, remove all authorized applications and connected push subscriptions revoke_access! - - # Finally, send a reset password prompt to the user - send_reset_password_instructions end protected diff --git a/lib/mastodon/cli/accounts.rb b/lib/mastodon/cli/accounts.rb index 33520df25..a4bbe5441 100644 --- a/lib/mastodon/cli/accounts.rb +++ b/lib/mastodon/cli/accounts.rb @@ -170,14 +170,17 @@ module Mastodon::CLI user.role_id = nil end - password = SecureRandom.hex if options[:reset_password] - user.password = password if options[:reset_password] user.email = options[:email] if options[:email] user.disabled = false if options[:enable] user.disabled = true if options[:disable] user.approved = true if options[:approve] user.otp_required_for_login = false if options[:disable_2fa] + # Password changes are a little different, as we also need to ensure + # sessions, subscriptions, and access tokens are revoked after changing: + password = SecureRandom.hex if options[:reset_password] + user.change_password!(password) if options[:reset_password] + if user.save user.confirm if options[:confirm] diff --git a/spec/lib/mastodon/cli/accounts_spec.rb b/spec/lib/mastodon/cli/accounts_spec.rb index a263d673d..eee2c7d25 100644 --- a/spec/lib/mastodon/cli/accounts_spec.rb +++ b/spec/lib/mastodon/cli/accounts_spec.rb @@ -314,12 +314,20 @@ describe Mastodon::CLI::Accounts do context 'with --reset-password option' do let(:options) { { reset_password: true } } - it 'returns a new password for the user' do - allow(SecureRandom).to receive(:hex).and_return('new_password') + let(:user) { Fabricate(:user, password: original_password) } + let(:original_password) { 'foobar12345' } + let(:new_password) { 'new_password12345' } - expect { cli.invoke(:modify, arguments, options) }.to output( - a_string_including('new_password') - ).to_stdout + it 'returns a new password for the user' do + allow(SecureRandom).to receive(:hex).and_return(new_password) + allow(Account).to receive(:find_local).and_return(user.account) + allow(user).to receive(:change_password!).and_call_original + + expect { cli.invoke(:modify, arguments, options) } + .to output(a_string_including(new_password)).to_stdout + + expect(user).to have_received(:change_password!).with(new_password) + expect(user.reload).to_not be_external_or_valid_password(original_password) end end From 68de818b868cf946939ff7eee8057fdf6ca75d88 Mon Sep 17 00:00:00 2001 From: Emelia Smith Date: Mon, 13 Oct 2025 14:20:57 +0200 Subject: [PATCH 08/10] Merge commit from fork --- streaming/index.js | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/streaming/index.js b/streaming/index.js index 3fc5b3613..8ec3188ec 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -152,17 +152,6 @@ const redisConfigFromEnv = (env) => { }; }; -const PUBLIC_CHANNELS = [ - 'public', - 'public:media', - 'public:local', - 'public:local:media', - 'public:remote', - 'public:remote:media', - 'hashtag', - 'hashtag:local', -]; - // Used for priming the counters/gauges for the various metrics that are // per-channel const CHANNEL_NAMES = [ @@ -171,7 +160,14 @@ const CHANNEL_NAMES = [ 'user:notification', 'list', 'direct', - ...PUBLIC_CHANNELS + 'public', + 'public:media', + 'public:local', + 'public:local:media', + 'public:remote', + 'public:remote:media', + 'hashtag', + 'hashtag:local' ]; const startServer = async () => { @@ -548,12 +544,6 @@ const startServer = async () => { const checkScopes = (req, channelName) => new Promise((resolve, reject) => { log.silly(req.requestId, `Checking OAuth scopes for ${channelName}`); - // When accessing public channels, no scopes are needed - if (PUBLIC_CHANNELS.includes(channelName)) { - resolve(); - return; - } - // The `read` scope has the highest priority, if the token has it // then it can access all streams const requiredScopes = ['read']; From 7a60edf0bd3258fdd3078ecee0a68aecbbccdcf5 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 13 Oct 2025 16:03:15 +0200 Subject: [PATCH 09/10] Fix streaming still being authorized for suspended accounts (#36451) --- app/models/account.rb | 4 ++++ streaming/index.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/models/account.rb b/app/models/account.rb index dac44d3ec..a7ea9e896 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -254,6 +254,10 @@ class Account < ApplicationRecord update!(suspended_at: date, suspension_origin: origin) create_canonical_email_block! if block_email end + + # This terminates all connections for the given account with the streaming + # server: + redis.publish("timeline:system:#{id}", Oj.dump(event: :kill)) if local? end def unsuspend! diff --git a/streaming/index.js b/streaming/index.js index 8ec3188ec..36f0610ca 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -455,7 +455,7 @@ const startServer = async () => { return; } - client.query('SELECT oauth_access_tokens.id, oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, oauth_access_tokens.scopes, devices.device_id FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id LEFT OUTER JOIN devices ON oauth_access_tokens.id = devices.access_token_id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL AND users.disabled IS FALSE LIMIT 1', [token], (err, result) => { + client.query('SELECT oauth_access_tokens.id, oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, oauth_access_tokens.scopes, devices.device_id FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id INNER JOIN accounts ON accounts.id = users.account_id LEFT OUTER JOIN devices ON oauth_access_tokens.id = devices.access_token_id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL AND users.disabled IS FALSE AND accounts.suspended_at IS NULL LIMIT 1', [token], (err, result) => { done(); if (err) { From 956ac0e3389457b58995312c599d735b0c7f00be Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 13 Oct 2025 16:03:29 +0200 Subject: [PATCH 10/10] Bump version to v4.2.27 (#36446) --- CHANGELOG.md | 9 +++++++++ docker-compose.yml | 6 +++--- lib/mastodon/version.rb | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99101b213..a0d7b3f50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. +## [4.2.27] - 2025-10-13 + +### Security + +- Update dependencies `rack` and `uri` +- Fix streaming server connection not being closed on user suspension (by @ThisIsMissEm, [GHSA-r2fh-jr9c-9pxh](https://github.com/mastodon/mastodon/security/advisories/GHSA-r2fh-jr9c-9pxh)) +- Fix password change through admin CLI not invalidating existing sessions and access tokens (by @ThisIsMissEm, [GHSA-f3q3-rmf7-9655](https://github.com/mastodon/mastodon/security/advisories/GHSA-f3q3-rmf7-9655)) +- Fix streaming server allowing access to public timelines even without the `read` or `read:statuses` OAuth scopes (by @ThisIsMissEm, [GHSA-7gwh-mw97-qjgp](https://github.com/mastodon/mastodon/security/advisories/GHSA-7gwh-mw97-qjgp)) + ## [4.2.26] - 2025-09-23 ### Security diff --git a/docker-compose.yml b/docker-compose.yml index bfa443d72..ceb0c4d79 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,7 +56,7 @@ services: web: build: . - image: ghcr.io/mastodon/mastodon:v4.2.26 + image: ghcr.io/mastodon/mastodon:v4.2.27 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.26 + image: ghcr.io/mastodon/mastodon:v4.2.27 restart: always env_file: .env.production command: node ./streaming @@ -95,7 +95,7 @@ services: sidekiq: build: . - image: ghcr.io/mastodon/mastodon:v4.2.26 + image: ghcr.io/mastodon/mastodon:v4.2.27 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 3acabcfbd..4474714f5 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 26 + 27 end def default_prerelease