Posted over 2 years ago. Visible to the public. Repeats.

Self-expiring URLs with Apache

When delivering non-public uploaded files (images, documents etc), one has to decide whether and how to do authorization. The usual approaches are:

  • Using send_file with a regular controller. This is secure, but potentially slow, especially for large collections of images.
  • Using unguessable URLs. This is fast (because Apache can deliver assets without going through Rails), but less secure.

When going with the "unguessable URL" approach, it is possible to somewhat increase security by using expiring URLs. The idea is to encode the expiry date into the URL in such a way that an attacker cannot modify that expiry date.

It turns out this can be done with a vanilla Apache installation in a relatively secure fashion. We encode URLs as

/system/<normal url>?t=<expiry date>&s=<signature>

Apache config

In the apache vhost, add the following Location (assuming we want to secure all of /public/system):

<Location "/system"> Define SIGNATURE_SECRET "<the same secret as in the secrets.yml>" RewriteEngine On # timestamp in the future and correctly signed? RewriteCond expr "%{QUERY_STRING} =~ /(^|&)t=(\w+)&s=(\w+)(&|$)/ && $2 > %{TIME} && sha1('${SIGNATURE_SECRET}' . sha1('${SIGNATURE_SECRET}' . '%{REQUEST_URI}?$2')) == tolower($3)" # yes, continue RewriteRule "" "-" [L] # otherwise, forbid RewriteRule "" "-" [F] </Location>

Generate the secret with SecureRandom.hex(32), and also put it as url_signature_secret into your secrets.yml.

Rails helper

To generate an expiring URL, use the following helper:

def sign_public_path(path, expires_at = 2.hours.from_now, allow_fuzzy_expiry: true, secret: Rails.application.secrets.url_signature_secret) return unless path uri = URI.parse(path) if allow_fuzzy_expiry expires_at = expires_at.end_of_hour + 1 end timestamp = expires_at.localtime.strftime('%Y%m%d%H%M%S') # this is similar to an hmac, but not quite the same, since apache functions do not allow to implement # a canonical hmac signature = Digest::SHA1.hexdigest(secret.to_s + Digest::SHA1.hexdigest("#{secret}#{CGI.unescape(uri.path)}?#{timestamp}")) "#{uri.path}?#{uri.query}&t=#{timestamp}&s=#{signature}" end

The helper has an option allow_fuzzy_expiry that will by default cause the expiry to round up to the next full hour. This is so URLs do not change with every single request, to allow some minimal browser caching.

A note about security

The correct way to calculate the signature is to use an HMAC. Unfortunately, I don't believe you can calculate a proper HMAC in a regular Apache installation. Instead, we do something that is very similar, but could potentially not be absolutely cryptographically sound. Do not use it for extremely sensitive content.

Flaky tests are tests that sometimes fail for no obvious reason. They are the plague of many end-to-end (E2E) test suites that automate the browser through tools like Capybara and Selenium.

Join our free training event and learn to fix any flaky test suite, even in large legacy applications.

Owner of this card:

Tobias Kraze
Last edit:
over 1 year ago
by Michael Leimstädtner
expire, carrierwave
About this deck:
We are makandra and do test-driven, agile Ruby on Rails software development.
License for source code
Posted by Tobias Kraze to makandra dev
This website uses short-lived cookies to improve usability.
Accept or learn more