Files
moreminimore-chat/config/initializers/active_storage.rb
Vishnu Narayanan 17d927554d fix: block the unused Active Storage direct-upload route (#15329)
## Description

**Problem.** The default Active Storage upload route, `POST
/rails/active_storage/direct_uploads`, is mounted automatically by Rails
and requires no authentication. Chatwoot doesn't rely on it, our
dashboard and widget uploads all use scoped, authenticated endpoints, so
the route just sits there letting anyone create blobs anonymously.

**Fix.** Block the built-in route so it returns `403`. Chatwoot's own
upload controllers inherit from the same Rails class but are left
working, an `instance_of?` check makes the block apply only to the bare
route, not to the subclasses that call `super`.

Fixes https://linear.app/chatwoot/issue/INF-94

## Type of change

- [x] Bug fix (non-breaking change which fixes an issue)

## How Has This Been Tested?

Added a request spec asserting the bare route returns `403` and creates
no blob. Existing widget and conversation direct-upload specs still
pass, confirming the scoped endpoints are unaffected.

13 examples, 0 failures across the three direct-upload specs; rubocop
clean.

## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes

---------

Co-authored-by: Sony Mathew <sony@chatwoot.com>
2026-08-06 14:25:19 +05:30

77 lines
2.4 KiB
Ruby

# Allow audio attachments (call recordings, voice notes) to serve inline so the
# in-app <audio> player can stream them. Without this, ActiveStorage's blob model
# forces Content-Disposition: attachment for any MIME outside the default allowlist
# (images + PDF), which makes the browser download instead of play.
Rails.application.config.active_storage.content_types_allowed_inline += %w[
audio/webm
audio/ogg
audio/mpeg
audio/mp4
audio/x-m4a
audio/wav
audio/x-wav
]
module ActiveStorageDirectUploadMetadataFilter
INTERNAL_METADATA_KEYS = %w[identified analyzed composed].freeze
private
def blob_args
super.tap do |args|
args[:metadata]&.except!(*INTERNAL_METADATA_KEYS, *INTERNAL_METADATA_KEYS.map(&:to_sym))
end
end
end
module ActiveStorageProxyRangeLimit
STREAMING_MAX_RANGES = 1
STREAMING_CHUNK_MAX_SIZE = 100.megabytes
private
def send_blob_byte_range_data(blob, range_header, disposition: nil)
ranges = Rack::Utils.get_byte_ranges(range_header, blob.byte_size)
return head(:range_not_satisfiable) unless valid_ranges?(ranges)
super
end
def valid_ranges?(ranges)
ranges.present? &&
ranges.any?(&:present?) &&
ranges.length <= STREAMING_MAX_RANGES &&
ranges.sum { |range| range.end - range.begin } < STREAMING_CHUNK_MAX_SIZE
end
end
# Block the default Rails direct-upload route. Dashboard and widget uploads both go
# through the scoped, authenticated /api/v1/... endpoints, so the bare route has no
# legitimate caller; leaving it open allows anonymous blob creation. Scoped subclasses
# call super and are exempt via the instance_of? check.
module ActiveStorageBareDirectUploadGuard
extend ActiveSupport::Concern
included do
before_action :reject_bare_direct_upload
end
private
def reject_bare_direct_upload
head :forbidden if instance_of?(ActiveStorage::DirectUploadsController)
end
end
Rails.application.config.to_prepare do
unless ActiveStorage::DirectUploadsController < ActiveStorageDirectUploadMetadataFilter
ActiveStorage::DirectUploadsController.prepend(ActiveStorageDirectUploadMetadataFilter)
end
unless ActiveStorage::DirectUploadsController.include?(ActiveStorageBareDirectUploadGuard)
ActiveStorage::DirectUploadsController.include(ActiveStorageBareDirectUploadGuard)
end
ActiveStorage::Streaming.prepend(ActiveStorageProxyRangeLimit) unless ActiveStorage::Streaming < ActiveStorageProxyRangeLimit
end