CVE-2024-23832 (Ruby, published 2024-02-01T17:15Z)
Description: value: Mastodon is a free, open-source social network server based on ActivityPub Mastodon allows configuration of LDAP for authentication. Due to insufficient origin validation in all Mastodon, attackers can impersonate and take over any remote account. Every Mastodon version prior to 3.5.17 is vulnerable, as well as 4.0.x versions prior to 4.0.13, 4.1.x version prior to 4.1.13, and 4.2.x versions prior to 4.2.5.
Severity: nan (CVSS v3 base score: 9.8)
Weakness: CWE-290 \u2014 Authentication Bypass by Spoofing
This attack-focused weakness is caused by incorrectly implemented authentication schemes that are subject to spoofing attacks.
Repository: https://github.com/mastodon/mastodon
Files changed:
app/controllers/concerns/signature_verification.rb
app/helpers/jsonld_helper.rb
app/lib/activitypub/activity.rb
app/lib/activitypub/linked_data_signature.rb
app/services/activitypub/fetch_remote_account_service.rb
app/services/activitypub/fetch_remote_actor_service.rb
app/services/activitypub/fetch_remote_key_service.rb
app/services/activitypub/fetch_remote_status_service.rb
app/services/activitypub/process_account_service.rb
app/services/fetch_resource_service.rb
lib/mastodon/version.rb
spec/lib/activitypub/linked_data_signature_spec.rb
spec/services/activitypub/fetch_remote_account_service_spec.rb
spec/services/activitypub/fetch_remote_actor_service_spec.rb
spec/services/activitypub/fetch_remote_key_service_spec.rb
spec/services/fetch_resource_service_spec.rb
spec/services/resolve_url_service_spec.rb
Commit message: Merge pull request from GHSA-3fjr-858r-92rw

* Fix insufficient origin validation

* Bump version to 4.3.0-alpha.1

Vulnerable code:
      account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false, suppress_errors: false) }
  def fetch_resource(uri, id, on_behalf_of = nil, request_options: {})
    unless id
      ActivityPub::FetchRemoteStatusService.new.call(object_uri, id: true, on_behalf_of: @account.followers.local.first, request_id: @options[:request_id])
    creator = ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false) if creator&.public_key.blank?
  def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true, request_id: nil)
  def call(uri, id: true, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true, request_id: nil)
        fetch_resource(uri, id)
        body_to_json(prefetched_body, compare_id: id ? uri : nil)
  def call(uri, id: true, prefetched_body: nil, suppress_errors: true)
    if prefetched_body.nil?
      if id
        @json = fetch_resource_without_id_validation(uri)
        if actor_type?
          @json = fetch_resource(@json['id'], true)
        elsif uri != @json['id']
          raise Error, "Fetched URI #{uri} has wrong id #{@json['id']}"
        end
      else
        @json = fetch_resource(uri, id)
      end
    else
      @json = body_to_json(prefetched_body, compare_id: id ? uri : nil)
    end
  def call(uri, id: true, prefetched_body: nil, on_behalf_of: nil, expected_actor_uri: nil, request_id: nil)
              fetch_resource(uri, id, on_behalf_of)
              body_to_json(prefetched_body, compare_id: id ? uri : nil)
    actor = ActivityPub::FetchRemoteAccountService.new.call(uri, id: true, request_id: @request_id) if actor.nil? || actor.possibly_stale?
    account ||= ActivityPub::FetchRemoteAccountService.new.call(@json['movedTo'], id: true, break_on_redirect: true, request_id: @options[:request_id])
      [json['id'], { prefetched_body: body, id: true }] if supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES) || expected_type?(json))
      'alpha.0'
        allow(service_stub).to receive(:call).with('http://example.com/alice', id: false) do
        expect(service_stub).to have_received(:call).with('http://example.com/alice', id: false).once
    let(:account) { subject.call('https://example.com/alice', id: true) }
    let(:account) { subject.call('https://example.com/alice', id: true) }
    let(:account) { subject.call(public_key_id, id: false) }
          id: 1,
        it { is_expected.to eq [1, { prefetched_body: body, id: true }] }
        it { is_expected.to eq [1, { prefetched_body: body, id: true }] }
        it { is_expected.to eq [1, { prefetched_body: json, id: true }] }
        it { is_expected.to eq [1, { prefetched_body: json, id: true }] }

Fixed code:
      account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, suppress_errors: false) }
  def fetch_resource(uri, id_is_known, on_behalf_of = nil, request_options: {})
    unless id_is_known
      ActivityPub::FetchRemoteStatusService.new.call(object_uri, on_behalf_of: @account.followers.local.first, request_id: @options[:request_id])
    creator = ActivityPub::FetchRemoteKeyService.new.call(creator_uri) if creator&.public_key.blank?
  def call(uri, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true, request_id: nil)
  def call(uri, prefetched_body: nil, break_on_redirect: false, only_key: false, suppress_errors: true, request_id: nil)
        fetch_resource(uri, true)
        body_to_json(prefetched_body, compare_id: uri)
  def call(uri, suppress_errors: true)
    @json = fetch_resource(uri, false)
  def call(uri, prefetched_body: nil, on_behalf_of: nil, expected_actor_uri: nil, request_id: nil)
              fetch_resource(uri, true, on_behalf_of)
              body_to_json(prefetched_body, compare_id: uri)
    actor = ActivityPub::FetchRemoteAccountService.new.call(uri, request_id: @request_id) if actor.nil? || actor.possibly_stale?
    account ||= ActivityPub::FetchRemoteAccountService.new.call(@json['movedTo'], break_on_redirect: true, request_id: @options[:request_id])
      return unless supported_context?(json) && (equals_or_includes_any?(json['type'], ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES) || expected_type?(json))

      if json['id'] != @url
        return if terminal

        return process(json['id'], terminal: true)
      end

      [@url, { prefetched_body: body }]
      'alpha.1'
        allow(service_stub).to receive(:call).with('http://example.com/alice') do
        expect(service_stub).to have_received(:call).with('http://example.com/alice').once
    let(:account) { subject.call('https://example.com/alice') }
    let(:account) { subject.call('https://example.com/alice') }
    let(:account) { subject.call(public_key_id) }
          id: 'http://example.com/foo',
        it { is_expected.to eq ['http://example.com/foo', { prefetched_body: body }] }
        it { is_expected.to eq ['http://example.com/foo', { prefetched_body: body }] }
        it { is_expected.to eq ['http://example.com/foo', { prefetched_body: json }] }
        it { is_expected.to eq ['http://example.com/foo', { prefetched_body: json }] }
        stub_request(:get, uri).to_return(body: body, headers: { 'Content-Type' => 'application/activity+json' })