Skip to content

Commit

Permalink
Merge pull request #61 from MozillaSocial/merge-v4.2.6
Browse files Browse the repository at this point in the history
Merge v4.2.6
  • Loading branch information
bakulf committed Feb 14, 2024
2 parents ab7baf4 + 60e0213 commit 120d417
Show file tree
Hide file tree
Showing 16 changed files with 213 additions and 34 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,25 @@

All notable changes to this project will be documented in this file.

## [4.2.6] - 2024-02-14

### Security

- Update the `sidekiq-unique-jobs` dependency (see [GHSA-cmh9-rx85-xj38](https://github.com/mhenrixon/sidekiq-unique-jobs/security/advisories/GHSA-cmh9-rx85-xj38))
In addition, we have disabled the web interface for `sidekiq-unique-jobs` out of caution.
If you need it, you can re-enable it by setting `ENABLE_SIDEKIQ_UNIQUE_JOBS_UI=true`.
If you only need to clear all locks, you can now use `bundle exec rake sidekiq_unique_jobs:delete_all_locks`.
- Update the `nokogiri` dependency (see [GHSA-xc9x-jj77-9p9j](https://github.com/sparklemotion/nokogiri/security/advisories/GHSA-xc9x-jj77-9p9j))
- Disable administrative Doorkeeper routes ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/29187))
- Fix ongoing streaming sessions not being invalidated when applications get deleted in some cases ([GHSA-7w3c-p9j8-mq3x](https://github.com/mastodon/mastodon/security/advisories/GHSA-7w3c-p9j8-mq3x))
In some rare cases, the streaming server was not notified of access tokens revocation on application deletion.
- Change external authentication behavior to never reattach a new identity to an existing user by default ([GHSA-vm39-j3vx-pch3](https://github.com/mastodon/mastodon/security/advisories/GHSA-vm39-j3vx-pch3))
Up until now, Mastodon has allowed new identities from external authentication providers to attach to an existing local user based on their verified e-mail address.
This allowed upgrading users from a database-stored password to an external authentication provider, or move from one authentication provider to another.
However, this behavior may be unexpected, and means that when multiple authentication providers are configured, the overall security would be that of the least secure authentication provider.
For these reasons, this behavior is now locked under the `ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH` environment variable.
In addition, regardless of this environment variable, Mastodon will refuse to attach two identities from the same authentication provider to the same account.

## [4.2.5] - 2024-02-01

### Security
Expand Down
14 changes: 7 additions & 7 deletions Gemfile.lock
Expand Up @@ -212,7 +212,7 @@ GEM
climate_control (0.2.0)
cocoon (1.2.15)
color_diff (0.1)
concurrent-ruby (1.2.2)
concurrent-ruby (1.2.3)
connection_pool (2.4.1)
cose (1.3.0)
cbor (~> 0.5.9)
Expand Down Expand Up @@ -457,7 +457,7 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2023.0808)
mini_mime (1.1.5)
mini_portile2 (2.8.4)
mini_portile2 (2.8.5)
minitest (5.19.0)
msgpack (1.7.1)
multi_json (1.15.0)
Expand All @@ -480,7 +480,7 @@ GEM
net-protocol
net-ssh (7.1.0)
nio4r (2.7.0)
nokogiri (1.15.4)
nokogiri (1.16.2)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
oj (3.16.1)
Expand Down Expand Up @@ -539,7 +539,7 @@ GEM
pundit (2.3.0)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.7.1)
racc (1.7.3)
rack (2.2.8)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
Expand Down Expand Up @@ -698,7 +698,7 @@ GEM
sentry-ruby (~> 5.16.1)
sentry-ruby (5.16.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (6.5.10)
sidekiq (6.5.12)
connection_pool (>= 2.2.5, < 3)
rack (~> 2.0)
redis (>= 4.5.0, < 5)
Expand All @@ -708,7 +708,7 @@ GEM
rufus-scheduler (~> 3.2)
sidekiq (>= 6, < 8)
tilt (>= 1.4.0)
sidekiq-unique-jobs (7.1.29)
sidekiq-unique-jobs (7.1.33)
brpoplpush-redis_script (> 0.1.1, <= 2.0.0)
concurrent-ruby (~> 1.0, >= 1.0.5)
redis (< 5.0)
Expand Down Expand Up @@ -753,7 +753,7 @@ GEM
terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0)
test-prof (1.2.3)
thor (1.2.2)
thor (1.3.0)
tilt (2.2.0)
timeout (0.4.0)
tpm-key_attestation (0.12.0)
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/auth/omniauth_callbacks_controller.rb
Expand Up @@ -6,7 +6,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def self.provides_callback_for(provider)
define_method provider do
@provider = provider
@user = User.find_for_oauth(request.env['omniauth.auth'], current_user)
@user = User.find_for_omniauth(request.env['omniauth.auth'], current_user)

if @user.persisted?
record_login_activity
Expand Down
20 changes: 20 additions & 0 deletions app/lib/application_extension.rb
Expand Up @@ -4,14 +4,34 @@ module ApplicationExtension
extend ActiveSupport::Concern

included do
include Redisable

has_many :created_users, class_name: 'User', foreign_key: 'created_by_application_id', inverse_of: :created_by_application

validates :name, length: { maximum: 60 }
validates :website, url: true, length: { maximum: 2_000 }, if: :website?
validates :redirect_uri, length: { maximum: 2_000 }

# The relationship used between Applications and AccessTokens is using
# dependent: delete_all, which means the ActiveRecord callback in
# AccessTokenExtension is not run, so instead we manually announce to
# streaming that these tokens are being deleted.
before_destroy :push_to_streaming_api, prepend: true
end

def confirmation_redirect_uri
redirect_uri.lines.first.strip
end

def push_to_streaming_api
# TODO: #28793 Combine into a single topic
payload = Oj.dump(event: :kill)
access_tokens.in_batches do |tokens|
redis.pipelined do |pipeline|
tokens.ids.each do |id|
pipeline.publish("timeline:access_token:#{id}", payload)
end
end
end
end
end
52 changes: 38 additions & 14 deletions app/models/concerns/omniauthable.rb
Expand Up @@ -19,17 +19,18 @@ def email_present?
end

class_methods do
def find_for_oauth(auth, signed_in_resource = nil)
def find_for_omniauth(auth, signed_in_resource = nil)
# EOLE-SSO Patch
auth.uid = (auth.uid[0][:uid] || auth.uid[0][:user]) if auth.uid.is_a? Hashie::Array
identity = Identity.find_for_oauth(auth)
identity = Identity.find_for_omniauth(auth)

# If a signed_in_resource is provided it always overrides the existing user
# to prevent the identity being locked with accidentally created accounts.
# Note that this may leave zombie accounts (with no associated identity) which
# can be cleaned up at a later date.
user = signed_in_resource || identity.user
user ||= create_for_oauth(auth)
user ||= reattach_for_auth(auth)
user ||= create_for_auth(auth)

if identity.user.nil?
identity.user = user
Expand All @@ -39,19 +40,35 @@ def find_for_oauth(auth, signed_in_resource = nil)
user
end

def create_for_oauth(auth)
# Check if the user exists with provided email. If no email was provided,
# we assign a temporary email and ask the user to verify it on
# the next step via Auth::SetupController.show
private

strategy = Devise.omniauth_configs[auth.provider.to_sym].strategy
assume_verified = strategy&.security&.assume_email_is_verified
email_is_verified = auth.info.verified || auth.info.verified_email || auth.info.email_verified || assume_verified
email = auth.info.verified_email || auth.info.email
def reattach_for_auth(auth)
# If allowed, check if a user exists with the provided email address,
# and return it if they does not have an associated identity with the
# current authentication provider.

# This can be used to provide a choice of alternative auth providers
# or provide smooth gradual transition between multiple auth providers,
# but this is discouraged because any insecure provider will put *all*
# local users at risk, regardless of which provider they registered with.

return unless ENV['ALLOW_UNSAFE_AUTH_PROVIDER_REATTACH'] == 'true'

user = User.find_by(email: email) if email_is_verified
email, email_is_verified = email_from_auth(auth)
return unless email_is_verified

return user unless user.nil?
user = User.find_by(email: email)
return if user.nil? || Identity.exists?(provider: auth.provider, user_id: user.id)

user
end

def create_for_auth(auth)
# Create a user for the given auth params. If no email was provided,
# we assign a temporary email and ask the user to verify it on
# the next step via Auth::SetupController.show

email, email_is_verified = email_from_auth(auth)

user = User.new(user_params_from_auth(email, auth))

Expand All @@ -66,7 +83,14 @@ def create_for_oauth(auth)
user
end

private
def email_from_auth(auth)
strategy = Devise.omniauth_configs[auth.provider.to_sym].strategy
assume_verified = strategy&.security&.assume_email_is_verified
email_is_verified = auth.info.verified || auth.info.verified_email || auth.info.email_verified || assume_verified
email = auth.info.verified_email || auth.info.email

[email, email_is_verified]
end

def user_params_from_auth(email, auth)
{
Expand Down
2 changes: 1 addition & 1 deletion app/models/identity.rb
Expand Up @@ -17,7 +17,7 @@ class Identity < ApplicationRecord
validates :uid, presence: true, uniqueness: { scope: :provider }
validates :provider, presence: true

def self.find_for_oauth(auth)
def self.find_for_omniauth(auth)
find_or_create_by(uid: auth.uid, provider: auth.provider)
end
end
10 changes: 10 additions & 0 deletions app/models/user.rb
Expand Up @@ -360,6 +360,16 @@ def revoke_access!
Doorkeeper::AccessToken.by_resource_owner(self).in_batches do |batch|
batch.update_all(revoked_at: Time.now.utc)
Web::PushSubscription.where(access_token_id: batch).delete_all

# Revoke each access token for the Streaming API, since `update_all``
# doesn't trigger ActiveRecord Callbacks:
# TODO: #28793 Combine into a single topic
payload = Oj.dump(event: :kill)
redis.pipelined do |pipeline|
batch.ids.each do |id|
pipeline.publish("timeline:access_token:#{id}", payload)
end
end
end
end

Expand Down
9 changes: 7 additions & 2 deletions config/initializers/doorkeeper.rb
Expand Up @@ -21,9 +21,14 @@
user unless user&.otp_required_for_login?
end

# If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below.
# Doorkeeper provides some administrative interfaces for managing OAuth
# Applications, allowing creation, edit, and deletion of applications from the
# server. At present, these administrative routes are not integrated into
# Mastodon, and as such, we've disabled them by always return a 403 forbidden
# response for them. This does not affect the ability for users to manage
# their own OAuth Applications.
admin_authenticator do
current_user&.admin? || redirect_to(new_user_session_url)
head 403
end

# Authorization Code expiration time (default 10 minutes).
Expand Down
2 changes: 1 addition & 1 deletion config/routes.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true

require 'sidekiq_unique_jobs/web'
require 'sidekiq_unique_jobs/web' if ENV['ENABLE_SIDEKIQ_UNIQUE_JOBS_UI'] == true
require 'sidekiq-scheduler/web'

class RedirectWithVary < ActionDispatch::Routing::PathRedirect
Expand Down
6 changes: 3 additions & 3 deletions docker-compose.yml
Expand Up @@ -56,7 +56,7 @@ services:

web:
build: .
image: ghcr.io/mastodon/mastodon:v4.2.5
image: ghcr.io/mastodon/mastodon:v4.2.6
restart: always
env_file: .env.production
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
Expand All @@ -77,7 +77,7 @@ services:

streaming:
build: .
image: ghcr.io/mastodon/mastodon:v4.2.5
image: ghcr.io/mastodon/mastodon:v4.2.6
restart: always
env_file: .env.production
command: node ./streaming
Expand All @@ -95,7 +95,7 @@ services:

sidekiq:
build: .
image: ghcr.io/mastodon/mastodon:v4.2.5
image: ghcr.io/mastodon/mastodon:v4.2.6
restart: always
env_file: .env.production
command: bundle exec sidekiq
Expand Down
2 changes: 1 addition & 1 deletion lib/mastodon/version.rb
Expand Up @@ -13,7 +13,7 @@ def minor
end

def patch
5
6
end

def default_prerelease
Expand Down
11 changes: 11 additions & 0 deletions lib/tasks/sidekiq_unique_jobs.rake
@@ -0,0 +1,11 @@
# frozen_string_literal: true

namespace :sidekiq_unique_jobs do
task delete_all_locks: :environment do
digests = SidekiqUniqueJobs::Digests.new
digests.delete_by_pattern('*', count: digests.count)

expiring_digests = SidekiqUniqueJobs::ExpiringDigests.new
expiring_digests.delete_by_pattern('*', count: expiring_digests.count)
end
end
6 changes: 3 additions & 3 deletions spec/models/identity_spec.rb
Expand Up @@ -3,16 +3,16 @@
require 'rails_helper'

RSpec.describe Identity do
describe '.find_for_oauth' do
describe '.find_for_omniauth' do
let(:auth) { Fabricate(:identity, user: Fabricate(:user)) }

it 'calls .find_or_create_by' do
expect(described_class).to receive(:find_or_create_by).with(uid: auth.uid, provider: auth.provider)
described_class.find_for_oauth(auth)
described_class.find_for_omniauth(auth)
end

it 'returns an instance of Identity' do
expect(described_class.find_for_oauth(auth)).to be_instance_of described_class
expect(described_class.find_for_omniauth(auth)).to be_instance_of described_class
end
end
end
7 changes: 7 additions & 0 deletions spec/models/user_spec.rb
Expand Up @@ -438,7 +438,10 @@
let!(:access_token) { Fabricate(:access_token, resource_owner_id: user.id) }
let!(:web_push_subscription) { Fabricate(:web_push_subscription, access_token: access_token) }

let(:redis_pipeline_stub) { instance_double(Redis::Namespace, publish: nil) }

before do
allow(redis).to receive(:pipelined).and_yield(redis_pipeline_stub)
user.reset_password!
end

Expand All @@ -454,6 +457,10 @@
expect(Doorkeeper::AccessToken.active_for(user).count).to eq 0
end

it 'revokes streaming access for all access tokens' do
expect(redis_pipeline_stub).to have_received(:publish).with("timeline:access_token:#{access_token.id}", Oj.dump(event: :kill)).once
end

it 'removes push subscriptions' do
expect(Web::PushSubscription.where(user: user).or(Web::PushSubscription.where(access_token: access_token)).count).to eq 0
end
Expand Down

0 comments on commit 120d417

Please sign in to comment.