flawopen.com/Path Traversal/Ruby

Path Traversal in Ruby

High Severity CWE-22
ELI5

You ask a librarian for a book in the children's section. You hand her a card saying 'children/../../classified'. Without checking the final room, she walks right into the government archives.

Key terms on this page
File.expand_path
Converts a path to an absolute path, resolving relative dots and tildes (~).
File.realpath
Resolves all symbolic links and relative path tokens, raising an error if the path does not exist.

What's happening

In Ruby, File.join(STORAGE_DIR, params[:file]) does not resolve ... If the user submits ../../etc/passwd, File.read() reads outside the target folder.

Real-world impact

In 2021, path traversal in Rails action dispatch and asset pipelines allowed remote attackers to read arbitrary server configuration files.

CISA Cybersecurity Advisory & MITRE CVE repository.

Vulnerable vs. fixed

VULNERABLE
# VULNERABLE: File.join does not resolve traversal dots
class DownloadsController < ApplicationController
  BASE_DIR = Rails.root.join('storage', 'public').to_s

  def show
    # Attacker input: "../../config/master.key"
    file_path = File.join(BASE_DIR, params[:file])
    
    # Leaks private Rails master encryption key!
    send_file file_path
  end
end
FIXED
# HARDENED: Canonicalize with File.realpath and enforce prefix
class DownloadsController < ApplicationController
  BASE_DIR = File.realpath(Rails.root.join('storage', 'public').to_s)

  def show
    user_file = params[:file].to_s
    return head :bad_request if user_file.blank?

    # 1. Expand path relative to BASE_DIR
    target_path = File.expand_path(user_file, BASE_DIR)

    # 2. Strict boundary check with trailing separator
    allowed_prefix = BASE_DIR.end_with?('/') ? BASE_DIR : "#{BASE_DIR}/"
    unless target_path.start_with?(allowed_prefix)
      return render plain: 'Forbidden: Path Traversal detected', status: :forbidden
    end

    return head :not_found unless File.file?(target_path)

    send_file target_path
  end
end

Why the fix works

File.expand_path(user_file, BASE_DIR) anchors the resolution to BASE_DIR and evaluates all .. segments. Verifying start_with?(allowed_prefix) guarantees containment.

Gotchas

File.expand_path without second argument

Calling File.expand_path(path) without passing BASE_DIR resolves relative to Dir.pwd, which is usually the app root, not the storage directory.

Common misconceptions

"Rails send_file is safe by default"

send_file will serve any readable path on the server if passed an absolute traversal path.

How to check if you're affected

bundle exec brakeman -w2

Prevention checklist

FAQ

How does rubyzip handle Zip Slip?

Older rubyzip versions allowed relative paths by default. Modern versions enforce validation or require explicit flags.

References