Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/controllers/concerns/active_storage_authentication.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module ActiveStorageAuthentication
extend ActiveSupport::Concern
include Authentication::SessionLookup

private
def require_active_storage_authentication
head :unauthorized unless find_session_by_cookie
end
end
16 changes: 16 additions & 0 deletions config/initializers/active_storage_authentication.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Active Storage mounts its direct-upload write endpoints
# (POST /rails/active_storage/direct_uploads and the disk-service PUT) on
# framework controllers that inherit from ActiveStorage::BaseController, so
# they never pass through ApplicationController's require_authentication.
# Writebook uploads attachments through ActionText::Markdown::UploadsController
# as an ordinary multipart POST and does not use direct uploads at all, leaving
# these endpoints reachable by anyone who can read a public page. Require a
# valid Writebook session before an anonymous caller can allocate a Blob or
# persist bytes to disk.
Rails.application.config.to_prepare do
ActiveStorage::DirectUploadsController.include ActiveStorageAuthentication
ActiveStorage::DirectUploadsController.before_action :require_active_storage_authentication

ActiveStorage::DiskController.include ActiveStorageAuthentication
ActiveStorage::DiskController.before_action :require_active_storage_authentication, only: :update
end
80 changes: 80 additions & 0 deletions test/controllers/active_storage_authentication_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
require "test_helper"

class ActiveStorageAuthenticationTest < ActionDispatch::IntegrationTest
test "direct upload metadata endpoint rejects anonymous callers" do
get new_session_path
assert_response :success

assert_no_difference -> { ActiveStorage::Blob.count } do
post rails_direct_uploads_path, params: blob_params, as: :json
end

assert_response :unauthorized
end

test "direct upload metadata endpoint allows authenticated users" do
sign_in :david

assert_difference -> { ActiveStorage::Blob.count }, 1 do
post rails_direct_uploads_path, params: blob_params, as: :json
end

assert_response :success
end

test "disk service upload endpoint rejects anonymous callers" do
sign_in :david
post rails_direct_uploads_path, params: blob_params, as: :json
assert_response :success
upload_path = URI.parse(response.parsed_body.dig("direct_upload", "url")).request_uri

anonymous = open_session
anonymous.put upload_path,
params: attachment_bytes,
headers: { "Content-Type" => "application/octet-stream" }

assert_equal 401, anonymous.status
Comment thread
jeremy marked this conversation as resolved.
end

test "disk service upload endpoint allows authenticated callers" do
sign_in :david
post rails_direct_uploads_path, params: blob_params, as: :json
assert_response :success
upload_path = URI.parse(response.parsed_body.dig("direct_upload", "url")).request_uri

put upload_path,
params: attachment_bytes,
headers: { "Content-Type" => "application/octet-stream" }

assert_response :no_content
end

test "disk service download endpoint stays public" do
ActiveStorage::Current.url_options = { host: "www.example.com", protocol: "https" }
blob = ActiveStorage::Blob.create_and_upload! \
io: StringIO.new(attachment_bytes), filename: "hi.txt", content_type: "text/plain"
Comment thread
jeremy marked this conversation as resolved.
download_path = URI.parse(blob.url).request_uri

anonymous = open_session
anonymous.get download_path

assert_equal 200, anonymous.status
assert_equal attachment_bytes, anonymous.response.body
ensure
blob&.purge
end

private
def attachment_bytes
"hello!"
end

def blob_params
{ blob: {
filename: "quota.bin",
byte_size: attachment_bytes.bytesize,
checksum: Digest::MD5.base64digest(attachment_bytes),
content_type: "application/octet-stream"
} }
end
end
Loading