# Ruby on Rails Security Audit Guide

> Ruby on Rails Framework Security Audit Module
> Applicable to: Rails 6.x/7.x/8.x, ActiveRecord, ActionPack, ActiveStorage

## Identification Patterns

```ruby
# Rails project identification
require "rails"
Rails.application
class ApplicationController < ActionController::Base

# File structure
├── Gemfile / Gemfile.lock
├── config/
│   ├── routes.rb
│   ├── application.rb
│   ├── environments/ (production.rb, development.rb)
│   └── initializers/
├── app/
│   ├── controllers/
│   ├── models/
│   ├── views/ (*.html.erb)
│   └── jobs/
├── db/
│   ├── schema.rb
│   └── migrate/
└── config.ru
```

---

## SQL Injection Detection

```ruby
# Dangerous: where() string interpolation
User.where("name = '#{params[:name]}'")           # ❌ Critical
User.where("role = '#{role}' AND active = true")  # ❌

# Dangerous: find_by_sql concatenation
User.find_by_sql("SELECT * FROM users WHERE id = #{params[:id]}")  # ❌ Critical

# Dangerous: order() with user input
User.order(params[:sort])  # ❌ High: ORDER BY injection (CASE blind injection possible)
# Attack: ?sort=CASE WHEN (SELECT 1 FROM users WHERE name='admin' AND password LIKE 'a%') THEN name ELSE email END

# Dangerous: pluck/select concatenation
User.pluck(params[:column])                 # ❌
User.select("#{params[:field]} as value")   # ❌

# Dangerous: group / having / joins
User.group(params[:group_by])               # ❌
User.joins(params[:table])                  # ❌
User.having("count > #{params[:count]}")    # ❌

# Dangerous: execute direct execution
ActiveRecord::Base.connection.execute("DELETE FROM logs WHERE id = #{id}")  # ❌

# Audit regex
\.where\s*\(.*#\{|\.where\s*\(.*\+
find_by_sql\s*\(.*#\{|find_by_sql\s*\(.*\+
\.order\s*\(params|\.order\s*\(.*#\{
\.pluck\s*\(params|\.select\s*\(.*#\{
\.execute\s*\(.*#\{|\.execute\s*\(.*\+

# Safe: Parameterized queries
User.where("name = ?", params[:name])              # ✓
User.where(name: params[:name])                    # ✓ Hash condition
User.find_by_sql(["SELECT * FROM users WHERE id = ?", params[:id]])  # ✓
User.order(Arel.sql(sanitize_sql_for_order(params[:sort])))          # ✓ Whitelist

# order safe: Whitelist
ALLOWED_SORT = %w[name email created_at].freeze
sort_col = ALLOWED_SORT.include?(params[:sort]) ? params[:sort] : "id"
User.order(sort_col)  # ✓
```

---

## Mass Assignment (Strong Parameter Bypass)

```ruby
# Dangerous: permit! allows all parameters
def user_params
  params.require(:user).permit!  # ❌ Critical: Equivalent to no protection
end

# Dangerous: Overly broad permit
params.require(:user).permit(:name, :email, :role, :is_admin)  # ❌ role/is_admin should not be allowed

# Dangerous: Using params hash directly
User.create(params[:user])             # ❌ Rails 4+ will error, but legacy code may bypass
User.update(params.permit(:name).to_h) # ❌ If other places add fields

# Audit regex
\.permit!|\.permit\(.*:role|\.permit\(.*:admin|\.permit\(.*:is_admin
params\[:.*\]\.to_unsafe_h|params\.to_unsafe_hash

# Safe: Strict strong parameters
def user_params
  params.require(:user).permit(:name, :email, :bio)  # ✓ Only allow safe fields
end
```

---

## CSRF Protection Bypass

```ruby
# Dangerous: Skipping CSRF verification
class ApiController < ApplicationController
  skip_before_action :verify_authenticity_token  # ❌ High: Entire controller skips CSRF
end

# Dangerous: Globally disabled
class ApplicationController < ActionController::Base
  protect_from_forgery with: :null_session  # ❌ Medium: Need to confirm in API mode
end

# Dangerous: Specific action skip
skip_before_action :verify_authenticity_token, only: [:webhook]  # Need to confirm if reasonable

# Audit regex
skip_before_action\s*:verify_authenticity_token
protect_from_forgery.*:null_session|protect_from_forgery.*:exception

# Safe: Enabled by default
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception  # ✓ CSRF failure throws exception
end
```

---

## Unsafe Deserialization

```ruby
# Dangerous: Marshal.load with user input
data = Marshal.load(params[:data])       # ❌ Critical: RCE
data = Marshal.load(Base64.decode64(cookie_value))  # ❌

# Dangerous: YAML.load (Ruby < 3.1 not safe by default)
config = YAML.load(user_input)           # ❌ Critical: RCE (Psych < 4.0)
YAML.load(File.read(user_controlled))    # ❌

# Dangerous: JSON.parse with custom classes
data = JSON.parse(input, create_additions: true)  # ❌ Allows instantiation of arbitrary classes

# Audit regex
Marshal\.load|Marshal\.restore
YAML\.load\s*\((?!.*permitted_classes)
JSON\.parse\s*\(.*create_additions.*true

# Safe: Safe alternatives
YAML.safe_load(input)                           # ✓ Only allows basic types
YAML.safe_load(input, permitted_classes: [Date]) # ✓ Explicit whitelist
JSON.parse(input)                               # ✓ Safe by default (no create_additions)
```

---

## Command Injection

```ruby
# Dangerous: system / exec / backticks
system("convert #{params[:file]} output.png")    # ❌ Critical
`ls #{params[:dir]}`                             # ❌ Critical
exec("ping #{params[:host]}")                    # ❌ Critical
%x(nslookup #{user_input})                       # ❌

# Dangerous: Open3
Open3.capture2("grep #{pattern} /var/log/app")   # ❌
IO.popen("cat #{filename}")                      # ❌

# Dangerous: Kernel.open (Ruby < 2.7 supports pipes)
open("|#{params[:cmd]}")                         # ❌ Critical: Pipe execution
open(params[:url])                               # ❌ Can trigger with |command

# Audit regex
system\s*\(.*#\{|`.*#\{.*`|exec\s*\(.*#\{
%x\(.*#\{|Open3\.(capture|popen)\s*\(.*#\{
IO\.popen\s*\(.*#\{|Kernel\.open\s*\(.*params

# Safe: Array form arguments (no shell)
system("convert", params[:file], "output.png")     # ✓ Array form
Open3.capture2("grep", pattern, "/var/log/app")    # ✓
```

---

## render() Content Type Injection

```ruby
# Dangerous: render inline with user input (SSTI)
render inline: params[:template]         # ❌ Critical: ERB code execution
render inline: user_input, type: :erb    # ❌ Critical

# Dangerous: render file path traversal
render file: params[:page]               # ❌ High: Arbitrary file read
render template: params[:tpl]            # ❌

# Dangerous: render body/html concatenation
render html: "<p>#{params[:msg]}</p>".html_safe  # ❌ XSS

# Audit regex
render\s+inline:|render\s+file:\s*params|render\s+template:\s*params
render\s+html:.*html_safe

# Safe: Use templates
render template: "pages/#{safe_page}"    # ✓ After whitelist validation
render json: { message: params[:msg] }   # ✓ JSON auto-encoded
```

---

## ActiveStorage Path Traversal

```ruby
# Dangerous: User-controlled content_disposition/filename
blob = ActiveStorage::Blob.find_signed(params[:id])
send_data blob.download,
  filename: params[:filename],              # ❌ May contain path traversal characters
  disposition: params[:disposition]          # ❌ Can inject response headers

# Audit regex
send_data.*params\[:.*filename|send_file.*params

# Safe: Use blob's own attributes
send_data blob.download,
  filename: blob.filename.to_s,            # ✓ Use filename from when stored
  disposition: "attachment"                 # ✓ Hardcoded
```

---

## Session Security

```ruby
# Dangerous: Session not reset (session fixation)
def login
  user = User.authenticate(params[:email], params[:password])
  session[:user_id] = user.id  # ❌ reset_session not called first
end

# Dangerous: Weak session store
Rails.application.config.session_store :cookie_store,
  key: '_app_session'  # Cookie stored client-side, must ensure secret_key_base strength

# Audit regex
session\[:.*\]\s*=(?!.*reset_session)
secret_key_base.*=.*['"][^'"]{0,30}['"]

# Safe: Reset session on login
def login
  user = User.authenticate(params[:email], params[:password])
  reset_session                    # ✓ Prevents session fixation
  session[:user_id] = user.id
end
```

---

## Search Pattern Summary

```regex
# SQL Injection
\.where\s*\(.*#\{|find_by_sql\s*\(.*#\{|\.order\s*\(params
\.execute\s*\(.*#\{|\.pluck\s*\(params|\.select\s*\(.*#\{

# Mass Assignment
\.permit!|params\[.*\]\.to_unsafe_h

# CSRF
skip_before_action.*verify_authenticity_token

# Deserialization
Marshal\.load|YAML\.load\s*\(|create_additions.*true

# Command Injection
system\s*\(.*#\{|`.*#\{.*`|Open3.*#\{|IO\.popen.*#\{

# render Injection
render\s+inline:|render\s+file:\s*params

# Session
session\[:.*=(?!.*reset_session)

# Open Redirect
redirect_to\s*params|redirect_to\s*.*request
```

---

## Quick Audit Checklist

```markdown
[ ] Check Rails version and known CVEs
[ ] Search for where/find_by_sql string interpolation (SQL injection)
[ ] Search for order(params[]) (ORDER BY injection)
[ ] Search for .permit! and broad permit lists (Mass Assignment)
[ ] Search for skip_before_action :verify_authenticity_token (CSRF bypass)
[ ] Search for Marshal.load / YAML.load (deserialization)
[ ] Search for system/backticks/Open3 with interpolation (command injection)
[ ] Search for render inline/file with user input (SSTI/file read)
[ ] Check send_data/send_file filename source
[ ] Search for reset_session in login flow
[ ] Check secret_key_base strength and storage
[ ] Search for html_safe / raw with user input (XSS)
[ ] Search for redirect_to params[] (open redirect)
[ ] Check config/environments/production.rb security configuration
```

---

## Minimum PoC Examples
```bash
# SQL Injection (where interpolation)
curl "http://localhost:3000/users?name=admin'OR'1'='1"

# Command Injection
curl "http://localhost:3000/ping?host=127.0.0.1;id"

# Path Traversal (render file)
curl "http://localhost:3000/page?file=../../../../etc/passwd"
```

---

## Reference Resources

- [Rails Security Guide (Official)](https://guides.rubyonrails.org/security.html)
- [OWASP Ruby on Rails Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Ruby_on_Rails_Cheat_Sheet.html)
- [Brakeman Scanner](https://brakemanscanner.org/)
- [Rails CVE List](https://www.cvedetails.com/vulnerability-list/vendor_id-12043/product_id-22568/Rubyonrails-Rails.html)
