flawopen.com/Path Traversal/Ruby
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.
In Ruby, File.join(STORAGE_DIR, params[:file]) does not resolve ... If the user submits ../../etc/passwd, File.read() reads outside the target folder.
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: 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
# 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
File.expand_path(user_file, BASE_DIR) anchors the resolution to BASE_DIR and evaluates all .. segments. Verifying start_with?(allowed_prefix) guarantees containment.
Calling File.expand_path(path) without passing BASE_DIR resolves relative to Dir.pwd, which is usually the app root, not the storage directory.
send_file will serve any readable path on the server if passed an absolute traversal path.
bundle exec brakeman -w2
File.expand_path(input, base_dir)target_path.start_with?(base_dir + '/')File.file?(target_path) before sendingOlder rubyzip versions allowed relative paths by default. Modern versions enforce validation or require explicit flags.