Skip to content
mediumCVSS 5.3CWE-22A01:2021 – Broken Access Control

Ruby on Rails

Rails raises on a traversal in a render argument, which removes the obvious bug. What is left is send_file, File.read, and render with a user-supplied template name.

Rails does more for you here than most

Rails is the least exposed of the seven runtimes on the cheatsheet, for two reasons.

render file: has required an absolute path since Rails 5, and ActionView raises ActionView::MissingTemplate — or in newer versions rejects outright — for a template argument containing traversal. The historically dangerous render params[:page] shape does not silently work.

And File.join does not discard the base the way Python's and .NET's do:

File.join('templates', '/etc/passwd')   # => "templates/etc/passwd"

That single difference removes the whole no-traversal-needed class of bug from Ruby.

What remains is the ordinary shape: user input concatenated into a path handed to File.read or send_file.

The mistakes that remain

Rubythe sinksVulnerable
# 1. send_file with a request-derived path. The most common Rails instance.
def download
  send_file Rails.root.join('storage', params[:name])
end
#   ?name=../../config/master.key   -> the key that decrypts credentials.yml.enc

# 2. Plain File.read.
def show
  render plain: File.read("#{Rails.root}/reports/#{params[:id]}.txt")
end

# 3. render with a template name from params. Rails raises for a traversal,
#    but the attacker still chooses among the templates that DO exist —
#    including partials that were never meant to render standalone.
def page
  render template: params[:page]
end

# 4. render inline: — this is code execution, not inclusion. ERB is
#    evaluated, so it is server-side template injection.
def preview
  render inline: params[:body]        # <- never do this
end

# 5. The Ruby-specific one: a null byte used to be an issue in older
#    versions. Modern Ruby raises ArgumentError on a NUL in a path, so
#    this is closed.

The fix

RubySecure
REPORTS = {
  'q1' => 'q1-2026.pdf',
  'q2' => 'q2-2026.pdf',
}.freeze

def download
  name = REPORTS[params[:id]]
  return head :not_found unless name

  send_file Rails.root.join('storage', name)
end


# For templates, an explicit set rather than a path:
PAGES = %w[home about contact].freeze

def page
  name = PAGES.include?(params[:page]) ? params[:page] : 'home'
  render template: "pages/#{name}"
end

What to read on a Rails target

Rails has an unusually predictable layout, which makes a read primitive efficient. In order of value:

  • config/master.key (or config/credentials/production.key). This decrypts config/credentials.yml.enc, which holds every secret the application has. It is the single highest-value file in a Rails application and it is exactly one traversal from the storage directory.
  • config/credentials.yml.enc — useless without the key above, so grab both.
  • config/database.yml — on older applications, plaintext database credentials.
  • .env — if the application uses dotenv rather than credentials.
  • config/secrets.yml — pre-5.2 applications. secret_key_base forges session cookies, and a forged session is often a direct route to administrator.
  • Gemfile.lock — exact gem versions, which maps to known CVEs and deserialization gadget chains.

Rails stores sessions in a signed and encrypted cookie by default, so secret_key_base is genuinely the crown jewel: with it you can mint any session you like without touching the server again.

What to grep for

Bash
# Read sinks with request data
grep -rnE 'File\.(read|open|binread)|IO\.read|send_file|send_data' app/ lib/ \
  | grep -E 'params|request'

# render with a dynamic argument
grep -rnE 'render\s+(file|template|partial|inline):' app/ | grep -E 'params|request'

# The dangerous one on its own — always a finding
grep -rn 'render inline:' app/

# Path building from params
grep -rnE 'Rails\.root\.join|File\.join|expand_path' app/ lib/ | grep params

# Archive extraction
grep -rnE 'Zip::File|rubyzip|Gem::Package|extract' Gemfile app/ lib/

# The correct calls, to confirm they are present
grep -rn 'realpath\|start_with?' app/ lib/