Feedback needed on patching ActiveStorage to allow expiring URLs

If I load a page containing a URL to an ActiveStorage attachment (*), wait over 5 minutes, copy the URL from the source code and paste it in a browser, it loads fine although I would expect it to fail as the expiry time has been exceeded. This seems to be because the base64-encoded part of the URL contains "exp"=>nil:

url = app.url_for(post.cover_image)
params = url[/blobs\/(.*)--/,1]
JSON.parse(Base64.decode64(params))
=> {"_rails"=>{"message"=>"BAhpJA==", "exp"=>nil, "pur"=>"blob_id"}}

Only after querying that URL does it perform a 302 redirect which leads to a new URL which itself does contains an expiry of 5 minutes in the future.

How to protect images and other types of sensitive content (e.g. subscription-only) with ActiveStorage? How to ensure URLs for attachments and their variants will not be downloadable in the future?

(*) Tested with a Rails 6.0.2.2 app with Posts configured with has_one_attached :cover_image and using ActiveStorage disk service. config.active_storage.service_urls_expire_in was not defined and so defaults to 5 minutes. Posts were rendered with style="background-image: url(#{url_for post.cover_image)})" (not using variants for the sake of simplicity). The URL of the background image in the page source has the structure /rails/active_storage/blobs/XXX--YYY/ZZZ.jpg where XXX is the Base64-encoded part.

See original unanswered StackOverflow question.

1 Like

OK so url_for(post.cover_image) indirectly calls route_for(:rails_service_blob, post.cover_image.blob.signed_id, post.cover_image.blob.filename), where signed_id is the part containing exp=nil. It is defined as ActiveStorage.verifier.generate(id, purpose: :blob_id) (see ActiveStorage::Blob#signed_id) which itself is defined in ActiveSupport::MessageVerifier#generate. That method has an argument expires_in whose default value is nil.

So although the ActiveStorage configuration allows to specify an expiry duration (config.active_storage.service_urls_expire_in) for ActiveStorage::Blob#url, ActiveStorage::Blob#service_url_for_direct_upload and ActiveStorage::Variant#url, there is apparently no such thing for the URL which actually ends in the HTML response.

Below is a monkey-patch I added in my code which does what I want (applied with ActiveStorage::Blob.include(ActiveStorageBlobMonkeyPatch). It seems to work with both image and audio attachments (or supposedly anything else), for both original attachments and variants, and for both my local disk service as well as our Cloudinary service.

module ActiveStorageBlobMonkeyPatch
  extend ActiveSupport::Concern

  included do
    def signed_id
      ActiveStorage.verifier.generate(id, purpose: :blob_id, expires_in: 5.minutes)
    end
  end
end

Following the Rails contributing guidelines, I haven’t submitted an issue or a PR to the repo at this point as some feedback would be helpful.

What do you think about this patch? How have other ActiveStorage users been dealing with sensitive content so far? What was the idea behind the expiration mechanisms already in place? Has the above already been considered but discarded for some reason (e.g. performance, caching, browser compatibility)? I am not at all familiar with the genesis of ActiveStorage …

1 Like

Here is a test gist illustrating the issue:

I would like the URL to expire as it does with AWS presigner.

I submitted a pull request: Add expires_in to ActiveStorage::Blob#signed_id by sedubois · Pull Request #38843 · rails/rails · GitHub

1 Like