If you've ever managed private files in web applications, you've probably encountered the security distribution problem. It's a vulnerability as common as it is underestimated, one that can transform into a serious risk for privacy and data security.

The Direct Access Vulnerability: A Universal Problem

Picture this scenario: you have an application managing confidential documents - contracts, financial reports, medical data. Your files are accessible through direct URLs like https://app.com/uploads/confidential-document.pdf. An authorized user gets this link, shares it (perhaps by mistake) via email or chat, and suddenly anyone with that URL can download the document.

The mechanism is as simple as it is dangerous:

  1. The application generates static URLs for files
  2. URLs are "guessable" or easily shareable
  3. There's no access control at the request level
  4. Anyone with the URL can access the file

The impact is significant: sensitive data exfiltration, privacy violations, potential legal consequences. You don't need to be a hacker, just a copy-paste at the wrong moment.

This vulnerability isn't specific to any framework or technology - you'll find it in PHP, Python, Node.js, Rails applications. Anywhere files are served directly from the web server without application-level controls.

Solution Strategies: Architectural Principles

There are several approaches to solve this problem, each with its own trade-offs. Let's analyze the main methods, regardless of implementation technology.

1. Backend Proxy Pattern

Concept: All file requests pass through the application, which acts as an authorization proxy.

How it works:

  • User requests a file through an application endpoint (e.g., /documents/123/download)
  • Application verifies authentication and authorization
  • If authorized, the application serves the file

Advantages:

  • Total control over authentication and authorization
  • Complete auditing of all accesses
  • Complex business logic (e.g., "only owner on business days")

Disadvantages:

  • Potential performance issues
  • Higher load on application servers

2. Web Server Offloading

Concept: The application authorizes, but delegates actual transfer to the web server (Nginx, Apache).

How it works:

  • Application verifies permissions
  • Sends a special header to the web server (e.g., X-Accel-Redirect for Nginx)
  • Web server handles file transfer

Advantages:

  • Excellent performance
  • Immediate application worker liberation
  • Security controls maintained

Disadvantages:

  • Only works with files on local filesystem
  • Requires specific web server configuration

3. Signed URLs

Concept: The application generates temporary cryptographically signed URLs pointing directly to storage.

How it works:

  • Application verifies authorization
  • Generates a signed URL with temporal expiration
  • User downloads directly from storage (S3, GCS, etc.)

Advantages:

  • Maximum scalability
  • Zero server load for transfer
  • Global performance with CDN

Disadvantages:

  • Dependency on cloud services
  • More complex configuration

4. Advanced Proxy with Auth Request

Concept: The web server acts as an intelligent proxy, delegating only authorization to the application.

How it works:

  • Web server receives file request
  • Makes a sub-request to the application for authorization
  • If authorized, serves file directly from storage

Advantages:

  • Optimal performance
  • Clean separation between authorization and transfer

Disadvantages:

  • Very complex configuration
  • Requires advanced web server modules

Implementation with Rails and Active Storage

Now let's see how to implement these strategies using Rails and Active Storage, which has become the de facto standard for file management in Rails.

Base Setup with Active Storage

First, let's ensure Active Storage is configured correctly:

class Document < ApplicationRecord
  has_one_attached :file
  belongs_to :user

  # Avoid public URLs by default
  def self.attachment_definitions
    { file: { public: false } }
  end
end

Strategy 1: Backend Proxy with Active Storage

Let's implement the proxy pattern using Active Storage:

Rails.application.routes.draw do
  resources :documents, only: [:show, :index] do
    member do
      get :download
    end
  end
end
class DocumentsController < ApplicationController
  before_action :authenticate_user!

  def download
    @document = Document.find(params[:id])

    # Authorization (using Pundit or similar)
    authorize @document, :download?

    # Log access for auditing
    AccessLog.create!(
      user: current_user,
      document: @document,
      ip_address: request.remote_ip,
      accessed_at: Time.current
    )

    # Serve file through Active Storage
    redirect_to rails_blob_path(@document.file, disposition: "attachment")
  end

  private

  def authorize_download!
    # Implement your authorization logic
    # e.g., only owner can download
    unless @document.user == current_user || current_user.admin?
      raise Pundit::NotAuthorizedError
    end
  end
end

Strategy 2: Web Server Offloading with Nginx

For local disk files, we can use X-Accel-Redirect:

class DocumentsController < ApplicationController
  def download
    @document = Document.find(params[:id])
    authorize @document, :download?

    # Instead of serving through Rails, use Nginx
    if Rails.env.production?
      # Internal path for Nginx (configured on server)
      internal_path = "/internal/storage/#{@document.file.blob.key}"
      response.headers['X-Accel-Redirect'] = internal_path
      response.headers['Content-Type'] = @document.file.content_type
      response.headers['Content-Disposition'] = "attachment; filename=\"#{@document.file.filename}\""
      head :ok
    else
      # In development, use standard behavior
      redirect_to rails_blob_path(@document.file, disposition: "attachment")
    end
  end
end

And the corresponding Nginx configuration:

# In nginx.conf
location /internal/storage/ {
  internal;
  alias /app/storage/;
}

Strategy 3: Signed URLs for Cloud Storage

If you use S3 or Google Cloud Storage, signed URLs are the most elegant solution:

class DocumentsController < ApplicationController
  def download
    @document = Document.find(params[:id])
    authorize @document, :download?

    # Generate signed URL valid for 1 hour
    signed_url = @document.file.url(expires_in: 1.hour)

    # Log access
    AccessLog.create!(
      user: current_user,
      document: @document,
      ip_address: request.remote_ip
    )

    # Redirect to signed URL
    redirect_to signed_url, allow_other_host: true
  end
end

Recommendations for Choosing the Approach

For applications with local disk files: use web server offloading (Strategy 2). It's the best compromise between performance and simplicity.

For cloud-native applications: go straight to signed URLs (Strategy 3). Maximum scalability and global performance.

For prototypes or very small files: backend proxy (Strategy 1) is fine to start, but plan the evolution immediately.

For complex architectures with mixed storage: advanced proxy (Strategy 4) offers maximum flexibility, but requires advanced skills.

The fundamental principle always remains the same: never expose sensitive files through direct URLs. The application must always be the guardian that decides, request by request, who can access what.

File security isn't an addition, it's a fundamental architectural requirement. Implementing it from the beginning is always easier than retrofitting it onto an existing system.


Useful Resources: