diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d7df4f26da301..5ab9eb10dfeffe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,26 @@ Changelog All notable changes to this project will be documented in this file. +## [4.1.5] - 2023-07-21 + +### Added + +- Add check preventing Sidekiq workers from running with Makara configured ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25850)) + +### Changed + +- Change request timeout handling to use a longer deadline ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26055)) + +### Fixed + +- Fix moderation interface for remote instances with a .zip TLD ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25886)) +- Fix remote accounts being possibly persisted to database with incomplete protocol values ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/25886)) +- Fix trending publishers table not rendering correctly on narrow screens ([vmstan](https://github.com/mastodon/mastodon/pull/25945)) + +### Security + +- Fix CSP headers being unintentionally wide ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26105)) + ## [4.1.4] - 2023-07-07 ### Fixed diff --git a/app/lib/request.rb b/app/lib/request.rb index c76ec6b6427e74..9c02410a52e52a 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -4,14 +4,22 @@ require 'socket' require 'resolv' -# Monkey-patch the HTTP.rb timeout class to avoid using a timeout block +# Use our own timeout class to avoid using HTTP.rb's timeout block # around the Socket#open method, since we use our own timeout blocks inside # that method # # Also changes how the read timeout behaves so that it is cumulative (closer # to HTTP::Timeout::Global, but still having distinct timeouts for other # operation types) -class HTTP::Timeout::PerOperation +class PerOperationWithDeadline < HTTP::Timeout::PerOperation + READ_DEADLINE = 30 + + def initialize(*args) + super + + @read_deadline = options.fetch(:read_deadline, READ_DEADLINE) + end + def connect(socket_class, host, port, nodelay = false) @socket = socket_class.open(host, port) @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay @@ -24,7 +32,7 @@ def reset_counter # Read data from the socket def readpartial(size, buffer = nil) - @deadline ||= Process.clock_gettime(Process::CLOCK_MONOTONIC) + @read_timeout + @deadline ||= Process.clock_gettime(Process::CLOCK_MONOTONIC) + @read_deadline timeout = false loop do @@ -33,7 +41,8 @@ def readpartial(size, buffer = nil) return :eof if result.nil? remaining_time = @deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC) - raise HTTP::TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout || remaining_time <= 0 + raise HTTP::TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout + raise HTTP::TimeoutError, "Read timed out after a total of #{@read_deadline} seconds" if remaining_time <= 0 return result if result != :wait_readable # marking the socket for timeout. Why is this not being raised immediately? @@ -46,7 +55,7 @@ def readpartial(size, buffer = nil) # timeout. Else, the first timeout was a proper timeout. # This hack has to be done because io/wait#wait_readable doesn't provide a value for when # the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks. - timeout = true unless @socket.to_io.wait_readable(remaining_time) + timeout = true unless @socket.to_io.wait_readable([remaining_time, @read_timeout].min) end end end @@ -57,7 +66,7 @@ class Request # We enforce a 5s timeout on DNS resolving, 5s timeout on socket opening # and 5s timeout on the TLS handshake, meaning the worst case should take # about 15s in total - TIMEOUT = { connect: 5, read: 10, write: 10 }.freeze + TIMEOUT = { connect_timeout: 5, read_timeout: 10, write_timeout: 10, read_deadline: 30 }.freeze include RoutingHelper @@ -69,6 +78,7 @@ def initialize(verb, url, **options) @http_client = options.delete(:http_client) @allow_local = options.delete(:allow_local) @options = options.merge(socket_class: use_proxy? || @allow_local ? ProxySocket : Socket) + @options = @options.merge(timeout_class: PerOperationWithDeadline, timeout_options: TIMEOUT) @options = @options.merge(proxy_url) if use_proxy? @headers = {} @@ -129,7 +139,7 @@ def valid_url?(url) end def http_client - HTTP.use(:auto_inflate).timeout(TIMEOUT.dup).follow(max_hops: 3) + HTTP.use(:auto_inflate).follow(max_hops: 3) end end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index 2da9096c734d99..087c23fa7e2446 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -76,6 +76,9 @@ def create_account @account.suspended_at = domain_block.created_at if auto_suspend? @account.suspension_origin = :local if auto_suspend? @account.silenced_at = domain_block.created_at if auto_silence? + + set_immediate_protocol_attributes! + @account.save end diff --git a/app/views/admin/trends/links/preview_card_providers/index.html.haml b/app/views/admin/trends/links/preview_card_providers/index.html.haml index c3648c35e97bd3..025270c128fbc9 100644 --- a/app/views/admin/trends/links/preview_card_providers/index.html.haml +++ b/app/views/admin/trends/links/preview_card_providers/index.html.haml @@ -29,7 +29,7 @@ - Trends::PreviewCardProviderFilter::KEYS.each do |key| = hidden_field_tag key, params[key] if params[key].present? - .batch-table.optional + .batch-table .batch-table__toolbar %label.batch-table__toolbar__select.batch-checkbox-all = check_box_tag :batch_checkbox_all, nil, false diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index 1f66155c463cd4..ccce6d71eb713f 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -3,7 +3,7 @@ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy def host_to_url(str) - "http#{Rails.configuration.x.use_https ? 's' : ''}://#{str}".split('/').first if str.present? + "http#{Rails.configuration.x.use_https ? 's' : ''}://#{str.split('/').first}" if str.present? end base_host = Rails.configuration.x.web_domain diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 9d2abf0745eca9..b847e654692d16 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -3,6 +3,11 @@ require_relative '../../lib/mastodon/sidekiq_middleware' Sidekiq.configure_server do |config| + if Rails.configuration.database_configuration.dig('production', 'adapter') == 'postgresql_makara' + STDERR.puts 'ERROR: Database replication is not currently supported in Sidekiq workers. Check your configuration.' + exit 1 + end + config.redis = REDIS_SIDEKIQ_PARAMS config.server_middleware do |chain| diff --git a/config/routes.rb b/config/routes.rb index 7958c6db8a668e..49508dc39c5ece 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -292,7 +292,7 @@ end end - resources :instances, only: [:index, :show, :destroy], constraints: { id: /[^\/]+/ } do + resources :instances, only: [:index, :show, :destroy], constraints: { id: /[^\/]+/ }, format: 'html' do member do post :clear_delivery_errors post :restart_delivery diff --git a/docker-compose.yml b/docker-compose.yml index f603c2f7e2e6a8..e3fa9ae1e2e507 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,7 +56,7 @@ services: web: build: . - image: ghcr.io/mastodon/mastodon + image: ghcr.io/mastodon/mastodon:v4.1.5 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 + image: ghcr.io/mastodon/mastodon:v4.1.5 restart: always env_file: .env.production command: node ./streaming @@ -95,7 +95,7 @@ services: sidekiq: build: . - image: ghcr.io/mastodon/mastodon + image: ghcr.io/mastodon/mastodon:v4.1.5 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 451c936f8b494b..e1b0afb7e05f92 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ def minor end def patch - 4 + 5 end def flags diff --git a/spec/requests/content_security_policy_spec.rb b/spec/requests/content_security_policy_spec.rb new file mode 100644 index 00000000000000..7eb27d61d615ca --- /dev/null +++ b/spec/requests/content_security_policy_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'Content-Security-Policy' do + it 'sets the expected CSP headers' do + allow(SecureRandom).to receive(:base64).with(16).and_return('ZbA+JmE7+bK8F5qvADZHuQ==') + + get '/' + expect(response.headers['Content-Security-Policy'].split(';').map(&:strip)).to contain_exactly( + "base-uri 'none'", + "default-src 'none'", + "frame-ancestors 'none'", + "font-src 'self' https://cb6e6126.ngrok.io", + "img-src 'self' https: data: blob: https://cb6e6126.ngrok.io", + "style-src 'self' https://cb6e6126.ngrok.io 'nonce-ZbA+JmE7+bK8F5qvADZHuQ=='", + "media-src 'self' https: data: https://cb6e6126.ngrok.io", + "frame-src 'self' https:", + "manifest-src 'self' https://cb6e6126.ngrok.io", + "form-action 'self'", + "child-src 'self' blob: https://cb6e6126.ngrok.io", + "worker-src 'self' blob: https://cb6e6126.ngrok.io", + "connect-src 'self' data: blob: https://cb6e6126.ngrok.io https://cb6e6126.ngrok.io ws://localhost:4000", + "script-src 'self' https://cb6e6126.ngrok.io 'wasm-unsafe-eval'" + ) + end +end