CrackedRuby CrackedRuby

Overview

Security testing examines software systems for vulnerabilities, misconfigurations, and weaknesses that could compromise confidentiality, integrity, or availability. Unlike functional testing that validates features work correctly, security testing assumes adversarial conditions where attackers actively seek to break the system.

Security testing operates across multiple dimensions. Static analysis examines source code without execution, identifying patterns like SQL concatenation or weak cryptographic algorithms. Dynamic testing exercises the running application, probing for injection vulnerabilities, authentication bypasses, and authorization failures. Penetration testing simulates real attacks using the same techniques adversaries employ.

The discipline emerged from recognizing that functional correctness does not guarantee security. An application can pass all functional tests while containing critical vulnerabilities. A login form might correctly authenticate valid credentials but fail to prevent brute force attacks. An API might return proper data for authorized requests but leak information through timing differences or verbose error messages.

Security testing reveals several vulnerability classes. Input validation failures allow attackers to inject malicious content, from SQL commands that exfiltrate databases to JavaScript that hijacks user sessions. Authentication weaknesses enable unauthorized access through default credentials, session fixation, or cryptographic flaws. Authorization bugs permit privilege escalation where users access resources beyond their permissions. Configuration errors expose sensitive data, enable unnecessary services, or grant excessive permissions.

Ruby applications face specific security considerations. Dynamic typing obscures type-related vulnerabilities until runtime. Metaprogramming features create attack surfaces through unsafe evaluation or method manipulation. Rails conventions provide security by default but require understanding to avoid bypassing protections. The extensive gem ecosystem introduces supply chain risks where dependencies contain vulnerabilities.

# Vulnerable to SQL injection
User.where("email = '#{params[:email]}'")

# Safe parameterized query
User.where(email: params[:email])

The example shows how identical functionality differs in security characteristics. The first version concatenates user input directly into SQL, allowing injection attacks. The second version uses parameterized queries where the database driver handles escaping, preventing injection regardless of input content.

Key Principles

Security testing follows defense in depth, applying multiple protective layers rather than relying on single controls. A web application combines input validation, parameterized queries, and least privilege database accounts. If attackers bypass input validation, parameterized queries prevent SQL injection. If they find an injection vulnerability, limited database permissions reduce damage.

Fail securely ensures that failures default to safe states. Authentication errors deny access rather than granting it. Decryption failures reject data rather than returning partial results. Session validation failures destroy sessions rather than continuing with uncertain state. Ruby applications implement this through explicit guards:

def authorized_user
  user = User.find_by(token: request.headers['Authorization'])
  return nil unless user
  return nil unless user.active?
  return nil unless user.has_permission?(required_permission)
  user
end

def show
  user = authorized_user
  return head :unauthorized unless user
  render json: resource
end

Each validation point returns nil on failure. The controller checks for nil and denies access. Missing any check results in denial, not access.

Least privilege limits each component to minimum required permissions. Database users receive only needed operations on specific tables. File system access restricts to necessary directories with minimal permissions. API tokens grant scoped permissions for intended operations. Service accounts run with constrained capabilities. This principle reduces blast radius when components are compromised.

Complete mediation validates every access to protected resources. Caching authorization decisions creates windows where permissions change but cached results grant outdated access. Security testing verifies that permission checks occur on every request:

# Vulnerable - caches permission check
class DocumentsController
  before_action :set_document
  
  def show
    # Permission checked once in set_document
    render json: @document
  end
  
  def update
    # Assumes set_document permission still valid
    @document.update(document_params)
  end
  
  private
  
  def set_document
    @document = Document.find(params[:id])
    return head :forbidden unless current_user.can_access?(@document)
  end
end

This code checks permissions in set_document but the document is then used in multiple actions. If user permissions change between actions or if document access rules are complex, this creates vulnerabilities.

Open design assumes attackers know the system. Security depends on secrets like keys and passwords, not on obscurity of algorithms or architectures. Security testing examines whether systems remain secure when design details are public. This drives using proven cryptographic algorithms rather than custom schemes, publishing security documentation, and allowing code review.

Separation of duties prevents single points of trust. Code review separates author from approver. Deployment requires multiple authorized individuals. Sensitive operations need two-factor authentication. Security testing validates these controls by attempting to bypass them with single-actor scenarios.

Psychological acceptability ensures security controls remain usable. Overly complex security mechanisms encourage workarounds. Sixteen-character randomly generated passwords prompt users to write them down. Certificate validation that fails frequently trains users to click through warnings. Security testing includes usability assessment to identify controls that users will circumvent.

Security testing validates trust boundaries where data crosses security contexts. Network boundaries between public internet and private systems. Process boundaries between user code and kernel. User boundaries between different accounts. Component boundaries between microservices. Each boundary requires validation, authentication, and authorization. Ruby applications often cross boundaries:

# User boundary - different user accounts
def transfer_funds(from_account_id, to_account_id, amount)
  from = Account.find(from_account_id)
  raise Forbidden unless current_user.owns?(from)
  # Transfer logic
end

# Component boundary - calling external service
def fetch_user_data(user_id)
  response = HTTP.auth("Bearer #{service_token}")
                 .get("#{api_url}/users/#{user_id}")
  # Process response
end

Security Implications

SQL injection remains prevalent despite being well understood. Attackers inject SQL commands through unvalidated input, exfiltrating data, modifying records, or executing stored procedures. Ruby applications are vulnerable when constructing queries through string interpolation or concatenation:

# Vulnerable to SQL injection
email = params[:email]
User.where("email = '#{email}'")
# Input: ' OR '1'='1
# Query: SELECT * FROM users WHERE email = '' OR '1'='1'

# Safe using placeholders
User.where("email = ?", params[:email])

# Safe using hash conditions
User.where(email: params[:email])

ActiveRecord provides protection through parameterized queries, but applications using raw SQL or string interpolation remain vulnerable. Security testing probes all database queries with injection payloads including quotes, comment markers, union statements, and time-based blind injection markers.

Cross-site scripting (XSS) injects malicious JavaScript into pages viewed by other users. Stored XSS persists in databases, affecting all users who view the content. Reflected XSS appears in immediate responses, often through URL parameters. DOM-based XSS manipulates client-side JavaScript. Rails escapes output by default, but manual HTML generation or use of html_safe creates vulnerabilities:

# Vulnerable - marks user content as safe
def show
  @comment = Comment.find(params[:id])
end

# In view
<div><%= @comment.content.html_safe %></div>

# Safe - automatic escaping
<div><%= @comment.content %></div>

# Safe when HTML needed - sanitize first
<div><%= sanitize @comment.content %></div>

Security testing injects XSS payloads including script tags, event handlers, and data URIs. Testing verifies escaping occurs and that whitelisting approaches block malicious constructs.

Cross-site request forgery (CSRF) tricks users into executing unwanted actions. An attacker's site includes requests to the target application, which the browser sends with the user's cookies. Rails includes CSRF protection through tokens, but JSON APIs often disable it:

class ApiController < ApplicationController
  skip_before_action :verify_authenticity_token
end

This creates vulnerabilities when APIs accept cookie authentication. Security testing verifies CSRF protection for state-changing operations and confirms APIs use token-based rather than cookie-based authentication.

Authentication vulnerabilities enable unauthorized access. Weak password policies allow brute force attacks. Missing account lockout permits unlimited attempts. Insecure password storage exposes credentials when databases leak. Session fixation allows attackers to hijack sessions by setting session IDs. Timing attacks infer valid usernames through response time differences:

# Vulnerable to timing attack
def authenticate(username, password)
  user = User.find_by(username: username)
  return false unless user
  return false unless user.password_matches?(password)
  true
end

The early return when the user does not exist creates a timing difference. Finding a valid username produces longer response times because password checking occurs. Security testing measures response times across many requests to identify timing channels.

Authorization failures allow privilege escalation. Insecure direct object references permit accessing resources by guessing IDs. Missing function-level access control allows calling administrative functions. Forced browsing accesses URLs directly without following intended navigation. Horizontal privilege escalation accesses other users' data. Vertical privilege escalation gains administrative privileges:

# Vulnerable to insecure direct object reference
def show
  @document = Document.find(params[:id])
  render json: @document
end

# Safe - scopes to current user
def show
  @document = current_user.documents.find(params[:id])
  render json: @document
end

Security testing attempts accessing resources with different user accounts, without authentication, and with tampered parameters.

Sensitive data exposure occurs through inadequate encryption, insecure transmission, or information leakage. Applications transmit sensitive data over HTTP rather than HTTPS. Error messages reveal stack traces, database schemas, or internal paths. API responses include unnecessary sensitive fields. Logs record passwords or credit card numbers:

# Vulnerable - logs sensitive data
logger.info "User login: #{params[:username]}, #{params[:password]}"

# Safe - logs only necessary data
logger.info "User login attempt: #{params[:username]}"

# Vulnerable - exposes sensitive fields
def show
  render json: @user
end

# Safe - serializes specific fields
def show
  render json: @user.as_json(only: [:id, :username, :created_at])
end

Security testing examines network traffic for unencrypted transmission, reviews logs for sensitive data, and analyzes API responses for excessive information disclosure.

XML External Entity (XXE) attacks exploit XML parsers that process external entity references. Attackers reference local files, internal network resources, or cause denial of service. Ruby XML parsers require configuration to prevent XXE:

# Vulnerable - default parsing
require 'nokogiri'
doc = Nokogiri::XML(params[:xml])

# Safe - disable external entities
doc = Nokogiri::XML(params[:xml]) do |config|
  config.nonet.noent
end

Security testing submits XML with external entity references pointing to sensitive files and internal URLs.

Deserialization vulnerabilities occur when applications deserialize untrusted data. Ruby's Marshal and YAML can execute code during deserialization. Attackers craft malicious serialized objects that execute commands:

# Vulnerable - deserializes untrusted data
data = Marshal.load(params[:data])

# Safer - use JSON
data = JSON.parse(params[:data])

# Safe when YAML needed - use safe_load
data = YAML.safe_load(params[:data], permitted_classes: [Symbol])

Security testing attempts deserializing malicious payloads and verifies applications use safe deserialization methods.

Ruby Implementation

Security testing in Ruby combines multiple approaches. Unit tests verify individual security controls. Integration tests examine security across components. Security-focused tests probe for specific vulnerability classes. Static analysis identifies code patterns indicating vulnerabilities.

RSpec provides the foundation for security tests. Tests verify that controls prevent attacks rather than just testing happy paths:

RSpec.describe UsersController, type: :request do
  describe 'SQL injection prevention' do
    it 'prevents SQL injection in email parameter' do
      payload = "' OR '1'='1"
      get '/users', params: { email: payload }
      
      expect(response).to have_http_status(:success)
      users = JSON.parse(response.body)
      expect(users.length).to eq(0)
    end
    
    it 'handles single quotes safely' do
      user = create(:user, email: "test@example.com")
      get '/users', params: { email: "test@example.com'" }
      
      expect(response).to have_http_status(:success)
      users = JSON.parse(response.body)
      expect(users.length).to eq(0)
    end
  end
  
  describe 'XSS prevention' do
    it 'escapes HTML in user input' do
      user = create(:user, name: '<script>alert("xss")</script>')
      get "/users/#{user.id}"
      
      expect(response.body).not_to include('<script>')
      expect(response.body).to include('&lt;script&gt;')
    end
    
    it 'sanitizes HTML in markdown fields' do
      post = create(:post, content: '<img src=x onerror=alert(1)>')
      get "/posts/#{post.id}"
      
      expect(response.body).not_to include('onerror=')
    end
  end
  
  describe 'authorization' do
    let(:user1) { create(:user) }
    let(:user2) { create(:user) }
    let(:document) { create(:document, user: user1) }
    
    it 'prevents accessing other users documents' do
      sign_in user2
      get "/documents/#{document.id}"
      
      expect(response).to have_http_status(:forbidden)
    end
    
    it 'prevents unauthorized document modification' do
      sign_in user2
      patch "/documents/#{document.id}", params: { title: 'hacked' }
      
      expect(response).to have_http_status(:forbidden)
      expect(document.reload.title).not_to eq('hacked')
    end
    
    it 'prevents mass assignment attacks' do
      sign_in user1
      patch "/users/#{user1.id}", params: { admin: true }
      
      expect(user1.reload.admin).to eq(false)
    end
  end
end

These tests verify that security controls work under attack conditions. SQL injection tests use actual injection payloads. XSS tests check that dangerous content gets escaped. Authorization tests attempt accessing resources without permissions.

Testing authentication requires verifying both positive and negative cases:

RSpec.describe 'Authentication' do
  describe 'password requirements' do
    it 'rejects weak passwords' do
      post '/users', params: {
        user: { email: 'test@example.com', password: '12345' }
      }
      
      expect(response).to have_http_status(:unprocessable_entity)
      expect(response.body).to include('too short')
    end
    
    it 'requires passwords with mixed case and numbers' do
      post '/users', params: {
        user: { email: 'test@example.com', password: 'password' }
      }
      
      expect(response).to have_http_status(:unprocessable_entity)
    end
  end
  
  describe 'timing attack prevention' do
    it 'takes similar time for valid and invalid usernames' do
      valid_user = create(:user, username: 'valid')
      
      valid_times = 10.times.map do
        start = Time.now
        post '/login', params: { username: 'valid', password: 'wrong' }
        Time.now - start
      end
      
      invalid_times = 10.times.map do
        start = Time.now
        post '/login', params: { username: 'invalid', password: 'wrong' }
        Time.now - start
      end
      
      valid_avg = valid_times.sum / valid_times.size
      invalid_avg = invalid_times.sum / invalid_times.size
      difference = (valid_avg - invalid_avg).abs
      
      expect(difference).to be < 0.01
    end
  end
  
  describe 'session security' do
    it 'regenerates session ID on login' do
      user = create(:user)
      
      get '/login'
      old_session_id = session.id
      
      post '/login', params: {
        username: user.username,
        password: user.password
      }
      
      expect(session.id).not_to eq(old_session_id)
    end
    
    it 'sets secure and httponly cookie flags' do
      user = create(:user)
      post '/login', params: {
        username: user.username,
        password: user.password
      }
      
      cookie = response.headers['Set-Cookie']
      expect(cookie).to include('secure')
      expect(cookie).to include('httponly')
    end
  end
end

Custom security test helpers encapsulate common testing patterns:

module SecurityTestHelpers
  def inject_sql(field:, payload:)
    get request_path, params: { field => payload }
    expect(response).to be_successful
    expect(extract_sql_error).to be_nil
  end
  
  def inject_xss(field:, payload:)
    post request_path, params: { field => payload }
    get response_path
    expect(response.body).not_to include_unescaped(payload)
  end
  
  def attempt_csrf_attack(method:, path:, params:)
    # Send request without CSRF token
    public_send(method, path, params: params)
    expect(response).to have_http_status(:forbidden)
  end
  
  def verify_rate_limiting(path:, limit:)
    (limit + 5).times { get path }
    expect(response).to have_http_status(:too_many_requests)
  end
  
  def test_authorization_matrix(resource:, user_types:, actions:)
    user_types.each do |user_type, should_allow|
      actions.each do |action|
        user = create(:user, role: user_type)
        sign_in user
        
        send(action, resource)
        
        if should_allow.include?(action)
          expect(response).not_to have_http_status(:forbidden)
        else
          expect(response).to have_http_status(:forbidden)
        end
      end
    end
  end
end

These helpers make security tests more readable and maintainable. The authorization matrix helper tests multiple user types against multiple actions, ensuring complete coverage.

Testing encryption and cryptography requires verifying correct algorithm usage:

RSpec.describe 'Cryptographic security' do
  describe 'password storage' do
    it 'uses bcrypt for password hashing' do
      user = create(:user, password: 'secure_password')
      expect(user.password_digest).to start_with('$2a$')
    end
    
    it 'uses sufficient work factor' do
      user = create(:user, password: 'secure_password')
      cost = user.password_digest.split('$')[2].to_i
      expect(cost).to be >= 12
    end
  end
  
  describe 'data encryption' do
    it 'encrypts sensitive fields at rest' do
      user = create(:user, ssn: '123-45-6789')
      
      raw_value = User.connection.select_value(
        "SELECT ssn FROM users WHERE id = #{user.id}"
      )
      
      expect(raw_value).not_to eq('123-45-6789')
      expect(user.ssn).to eq('123-45-6789')
    end
    
    it 'uses AES-256-GCM for encryption' do
      encrypted_value = User.encrypt('sensitive')
      algorithm = extract_algorithm_from_encrypted(encrypted_value)
      expect(algorithm).to eq('aes-256-gcm')
    end
  end
  
  describe 'token generation' do
    it 'generates cryptographically secure tokens' do
      tokens = 100.times.map { User.generate_token }
      
      # Check randomness
      expect(tokens.uniq.length).to eq(100)
      
      # Check entropy
      token = tokens.first
      expect(token.length).to be >= 32
      expect(token).to match(/\A[a-zA-Z0-9_-]+\z/)
    end
  end
end

Testing Approaches

Static Application Security Testing (SAST) analyzes source code without execution. Tools scan for vulnerability patterns like SQL injection, XSS, insecure cryptography, and hardcoded secrets. SAST integrates into development workflows through pre-commit hooks, CI/CD pipelines, or IDE plugins.

Brakeman provides SAST for Ruby on Rails applications:

# Gemfile
group :development, :test do
  gem 'brakeman'
end

# Rakefile
require 'brakeman'

task :security_scan do
  result = Brakeman.run(
    app_path: '.',
    output_formats: [:json],
    output_files: ['tmp/brakeman.json'],
    quiet: true
  )
  
  if result.errors.any? || result.warnings.any?
    puts "Security issues found!"
    exit 1
  end
end

Configure Brakeman to match project needs:

# config/brakeman.yml
---
:skip_checks:
  - DetailedExceptions  # Allow detailed errors in development
:check_arguments: true
:safe_methods:
  - safe_yaml_load
:output_formats:
  - json
  - html
:output_files:
  - brakeman-report.json
  - brakeman-report.html

Dynamic Application Security Testing (DAST) tests running applications. DAST tools send malicious payloads, fuzz inputs, and attempt common attacks. Unlike SAST which examines code, DAST validates runtime behavior including configuration, deployment, and infrastructure.

OWASP ZAP provides automated DAST scanning:

require 'zap'

RSpec.describe 'DAST security scan', type: :system do
  before(:all) do
    @zap = Zap::Client.new(
      host: 'localhost',
      port: 8080
    )
    
    # Start application for scanning
    @server = start_test_server
  end
  
  after(:all) do
    @server.stop
  end
  
  it 'passes automated security scan' do
    target_url = 'http://localhost:3000'
    
    # Spider the application
    scan_id = @zap.spider.scan(target_url)
    wait_for_scan_completion(@zap, scan_id)
    
    # Active scan for vulnerabilities
    scan_id = @zap.ascan.scan(target_url)
    wait_for_scan_completion(@zap, scan_id)
    
    # Get results
    alerts = @zap.core.alerts(baseurl: target_url)
    
    # Filter out acceptable risks
    critical_alerts = alerts.select { |a| a['risk'] == 'High' }
    
    expect(critical_alerts).to be_empty, 
      "Found critical vulnerabilities: #{critical_alerts.map { |a| a['alert'] }}"
  end
end

Dependency scanning identifies vulnerable libraries. Ruby applications depend on gems that may contain security vulnerabilities. Bundler-audit checks gem dependencies against known vulnerability databases:

# Gemfile
group :development, :test do
  gem 'bundler-audit'
end

# Rakefile
require 'bundler/audit/task'
Bundler::Audit::Task.new

# Run as: rake bundle:audit

# In CI/CD pipeline
task :security_check do
  require 'bundler/audit/cli'
  Bundler::Audit::CLI.start(['check', '--update'])
end

Automate dependency scanning in CI:

# .github/workflows/security.yml
name: Security Checks
on: [push, pull_request]

jobs:
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: 3.2
      - run: gem install bundler-audit
      - run: bundle-audit check --update

Fuzz testing sends random or malformed inputs to find crashes and unexpected behavior. Fuzzing reveals input validation issues, crashes, and edge cases:

require 'fuzzbert'

RSpec.describe 'Input fuzzing' do
  it 'handles fuzzed email inputs safely' do
    fuzzer = FuzzBert::Fuzzer.new
    
    100.times do
      fuzzed_input = fuzzer.fuzz_string
      
      expect {
        post '/users', params: { user: { email: fuzzed_input } }
      }.not_to raise_error
      
      expect(response.status).to be_between(200, 499)
    end
  end
  
  it 'handles fuzzed JSON safely' do
    100.times do
      json = generate_fuzzed_json
      
      expect {
        post '/api/data', 
             params: json,
             headers: { 'Content-Type': 'application/json' }
      }.not_to raise_error
    end
  end
end

def generate_fuzzed_json
  mutations = [
    '{"key": ' + 'x' * 10000 + '}',  # Large string
    '{"key": ' + '[' * 1000,          # Unbalanced brackets
    '{"key": null}' + "\x00" * 100,   # Null bytes
    '{"' + 'x' * 1000 + '": "val"}',  # Large key
    '[' + '1,' * 1000 + ']'           # Large array
  ]
  
  mutations.sample
end

Penetration testing simulates real attacks using the same techniques malicious actors employ. Penetration tests combine automated scanning with manual testing. Testers attempt privilege escalation, lateral movement, and data exfiltration.

Security regression testing verifies that security fixes remain effective. When vulnerabilities are discovered and fixed, regression tests prevent reintroduction:

RSpec.describe 'Security regressions' do
  # CVE-2023-XXXX - SQL injection in search
  it 'prevents SQL injection regression (CVE-2023-XXXX)' do
    payload = "'; DROP TABLE users; --"
    get '/search', params: { q: payload }
    
    expect(response).to be_successful
    expect { User.count }.not_to raise_error
  end
  
  # SEC-2023-001 - Authorization bypass in admin panel
  it 'prevents admin authorization bypass (SEC-2023-001)' do
    regular_user = create(:user, admin: false)
    sign_in regular_user
    
    get '/admin/users'
    expect(response).to have_http_status(:forbidden)
    
    # Attempted bypass through parameter manipulation
    get '/admin/users', params: { admin: true }
    expect(response).to have_http_status(:forbidden)
  end
end

Tools & Ecosystem

Brakeman scans Rails applications for security vulnerabilities through static analysis. It identifies SQL injection, XSS, command injection, unsafe redirects, and mass assignment vulnerabilities:

# Install
gem install brakeman

# Basic scan
brakeman

# Detailed scan with all checks
brakeman -A

# Output to file
brakeman -o report.html

# Scan specific paths
brakeman -p app/controllers,app/models

# Compare against previous scan
brakeman -o current.json
brakeman --compare previous.json

Integrate Brakeman into development workflow:

# config/brakeman.ignore
{
  "ignored_warnings": [
    {
      "warning_type": "SQL Injection",
      "fingerprint": "abc123...",
      "note": "False positive - input sanitized upstream"
    }
  ]
}

Bundler-audit checks dependencies for known vulnerabilities by comparing Gemfile.lock against the Ruby Advisory Database:

# Install
gem install bundler-audit

# Update vulnerability database
bundle-audit update

# Check current dependencies
bundle-audit check

# Include development dependencies
bundle-audit check --verbose

Automate regular scanning:

# lib/tasks/security.rake
namespace :security do
  desc 'Run security checks'
  task audit: :environment do
    require 'bundler/audit/cli'
    
    puts "Updating vulnerability database..."
    Bundler::Audit::CLI.start(['update'])
    
    puts "Scanning dependencies..."
    Bundler::Audit::CLI.start(['check'])
  end
end

RuboCop Security provides security-focused linting rules. It detects insecure code patterns that might not constitute immediate vulnerabilities but indicate security concerns:

# Gemfile
gem 'rubocop-security', require: false

# .rubocop.yml
require:
  - rubocop-security

Security/Eval:
  Enabled: true

Security/JSONLoad:
  Enabled: true
  
Security/MarshalLoad:
  Enabled: true
  
Security/YAMLLoad:
  Enabled: true

Rack::Attack provides rate limiting and request throttling to prevent brute force attacks, denial of service, and API abuse:

# Gemfile
gem 'rack-attack'

# config/initializers/rack_attack.rb
class Rack::Attack
  # Throttle login attempts
  throttle('limit login attempts', limit: 5, period: 60.seconds) do |req|
    if req.path == '/login' && req.post?
      req.ip
    end
  end
  
  # Throttle API requests
  throttle('limit api requests', limit: 100, period: 1.hour) do |req|
    req.ip if req.path.start_with?('/api/')
  end
  
  # Block suspicious requests
  blocklist('block suspicious patterns') do |req|
    Rack::Attack::Fail2Ban.filter("suspicious-#{req.ip}", 
                                   maxretry: 3, 
                                   findtime: 10.minutes, 
                                   bantime: 1.hour) do
      req.path.include?('../') || 
      req.query_string.include?('<script') ||
      req.user_agent =~ /suspicious-pattern/
    end
  end
end

Test rate limiting:

RSpec.describe 'Rate limiting' do
  it 'blocks excessive login attempts' do
    6.times do |i|
      post '/login', params: { username: 'test', password: 'wrong' }
      
      if i < 5
        expect(response).to have_http_status(:unauthorized)
      else
        expect(response).to have_http_status(:too_many_requests)
      end
    end
  end
end

Secure Headers gem sets security-related HTTP headers:

# Gemfile
gem 'secure_headers'

# config/initializers/secure_headers.rb
SecureHeaders::Configuration.default do |config|
  config.x_frame_options = 'DENY'
  config.x_content_type_options = 'nosniff'
  config.x_xss_protection = '1; mode=block'
  config.x_download_options = 'noopen'
  config.x_permitted_cross_domain_policies = 'none'
  config.referrer_policy = 'strict-origin-when-cross-origin'
  
  config.csp = {
    default_src: ["'self'"],
    script_src: ["'self'", "'unsafe-inline'"],
    style_src: ["'self'", "'unsafe-inline'"],
    img_src: ["'self'", 'data:', 'https:'],
    font_src: ["'self'", 'data:'],
    connect_src: ["'self'"],
    frame_ancestors: ["'none'"]
  }
end

Devise provides authentication with security features built in:

# Gemfile
gem 'devise'

# Configure security settings
Devise.setup do |config|
  # Lockout after failed attempts
  config.lock_strategy = :failed_attempts
  config.unlock_strategy = :time
  config.maximum_attempts = 5
  config.unlock_in = 1.hour
  
  # Password requirements
  config.password_length = 12..128
  config.pepper = ENV['DEVISE_PEPPER']
  
  # Session security
  config.expire_auth_token_on_timeout = true
  config.timeout_in = 30.minutes
  config.remember_for = 2.weeks
end

Common Pitfalls

Testing only happy paths creates false security confidence. Tests verify features work correctly but miss attack scenarios. A login test confirms valid credentials grant access but fails to test rate limiting, timing attacks, or session fixation:

# Insufficient - only tests valid case
it 'logs in successfully' do
  user = create(:user)
  post '/login', params: { username: user.username, password: user.password }
  expect(response).to redirect_to(dashboard_path)
end

# Comprehensive - tests security controls
describe 'login security' do
  it 'logs in with valid credentials' do
    user = create(:user)
    post '/login', params: { username: user.username, password: user.password }
    expect(response).to redirect_to(dashboard_path)
  end
  
  it 'rejects invalid credentials' do
    user = create(:user)
    post '/login', params: { username: user.username, password: 'wrong' }
    expect(response).to have_http_status(:unauthorized)
  end
  
  it 'prevents brute force attacks' do
    user = create(:user)
    10.times { post '/login', params: { username: user.username, password: 'wrong' } }
    expect(response).to have_http_status(:too_many_requests)
  end
  
  it 'prevents timing attacks' do
    # Test timing consistency
  end
  
  it 'regenerates session on login' do
    # Test session fixation prevention
  end
end

Relying on client-side validation creates vulnerabilities. JavaScript validation improves user experience but attackers bypass it through browser tools, intercepting proxies, or direct API calls. Server-side validation must duplicate all security checks:

# Vulnerable - assumes client sent valid data
def create
  @user = User.create(params[:user])
  render json: @user
end

# Secure - validates on server
def create
  @user = User.new(user_params)
  
  unless @user.valid?
    return render json: { errors: @user.errors }, status: :unprocessable_entity
  end
  
  @user.save
  render json: @user
end

private

def user_params
  params.require(:user).permit(:email, :username, :password)
end

Insufficient authorization testing misses horizontal and vertical privilege escalation. Tests verify authorized users can access resources but fail to confirm unauthorized users cannot:

# Insufficient
it 'shows user profile' do
  sign_in @user
  get "/users/#{@user.id}"
  expect(response).to be_successful
end

# Comprehensive
describe 'profile access' do
  let(:user1) { create(:user) }
  let(:user2) { create(:user) }
  
  it 'shows own profile' do
    sign_in user1
    get "/users/#{user1.id}"
    expect(response).to be_successful
  end
  
  it 'prevents viewing other profiles' do
    sign_in user1
    get "/users/#{user2.id}"
    expect(response).to have_http_status(:forbidden)
  end
  
  it 'prevents unauthenticated access' do
    get "/users/#{user1.id}"
    expect(response).to have_http_status(:unauthorized)
  end
end

Trusting decoded JWTs without validation enables token manipulation. JWTs contain encoded data that clients can read and modify. Applications must verify signatures before trusting claims:

# Vulnerable - trusts JWT without verification
def current_user
  token = request.headers['Authorization']&.split(' ')&.last
  payload = JWT.decode(token, nil, false).first
  User.find(payload['user_id'])
end

# Secure - verifies signature
def current_user
  token = request.headers['Authorization']&.split(' ')&.last
  payload = JWT.decode(token, ENV['JWT_SECRET'], true, algorithm: 'HS256').first
  User.find(payload['user_id'])
rescue JWT::DecodeError, JWT::ExpiredSignature
  nil
end

Using eval or instance_eval with user input enables code injection. Ruby's dynamic features allow executing arbitrary code. Applications must never evaluate user-controlled strings:

# Vulnerable
def calculate
  result = eval(params[:expression])
  render json: { result: result }
end

# Safer - use safe evaluation library
def calculate
  parser = MathParser.new
  result = parser.evaluate(params[:expression])
  render json: { result: result }
end

Ignoring CSRF protection for JSON APIs creates vulnerabilities when APIs accept cookie authentication. CSRF tokens protect against cross-site requests but JSON APIs often disable them without implementing alternative protections:

# Vulnerable
class ApiController < ApplicationController
  skip_before_action :verify_authenticity_token
  
  # Uses cookie-based authentication
  def data
    render json: current_user.data
  end
end

# Secure - uses token authentication
class ApiController < ApplicationController
  skip_before_action :verify_authenticity_token
  before_action :authenticate_token
  
  def data
    render json: current_user.data
  end
  
  private
  
  def authenticate_token
    token = request.headers['Authorization']
    @current_user = User.find_by(api_token: token)
    head :unauthorized unless @current_user
  end
end

Mass assignment vulnerabilities occur when models accept attributes without whitelisting. Strong parameters protect against this but developers sometimes bypass them:

# Vulnerable
def update
  @user.update(params[:user])
end

# Secure
def update
  @user.update(user_params)
end

private

def user_params
  params.require(:user).permit(:name, :email)
end

# Also vulnerable - bypasses strong parameters
def update
  @user.attributes = params[:user].slice(:name, :email, :admin)
end

Logging sensitive data creates compliance violations and security risks. Logs persist in files, centralized logging systems, and backups. Logged passwords or tokens enable compromise:

# Vulnerable
logger.info "Processing payment: #{params.inspect}"
logger.debug "User authentication: #{username}, #{password}"

# Secure
logger.info "Processing payment for order #{order_id}"
logger.debug "User authentication attempt for: #{username}"

# Filter sensitive parameters
Rails.application.config.filter_parameters += [
  :password, :password_confirmation, :secret, :token,
  :api_key, :credit_card, :ssn
]

Reference

Security Testing Checklist

Category Check Test Method
Input Validation SQL injection prevention Submit SQL metacharacters in all inputs
Input Validation XSS prevention Submit script tags and event handlers
Input Validation Command injection prevention Submit shell metacharacters
Input Validation Path traversal prevention Submit ../ sequences
Authentication Password strength enforcement Attempt weak passwords
Authentication Account lockout Exceed failed login threshold
Authentication Session fixation prevention Verify session ID changes on login
Authentication Logout functionality Confirm session invalidation
Authorization Horizontal privilege escalation Access other users' resources
Authorization Vertical privilege escalation Access admin functions as regular user
Authorization Direct object reference Access resources by ID manipulation
Session Management Secure cookie flags Verify secure and httponly flags
Session Management Session timeout Verify idle timeout enforced
Session Management Concurrent session limits Test multiple simultaneous logins
Cryptography Strong algorithms Verify AES-256, bcrypt usage
Cryptography Secure random generation Test token randomness
Cryptography Key management Verify keys not hardcoded
Data Protection HTTPS enforcement Attempt HTTP access
Data Protection Sensitive data encryption Verify encryption at rest
Data Protection Information disclosure Review error messages and responses
API Security Rate limiting Exceed request thresholds
API Security CSRF protection Submit requests without tokens
API Security Authentication token security Test token expiration and revocation
Configuration Security headers Verify CSP, HSTS, X-Frame-Options
Configuration Default credentials Test for default admin accounts
Configuration Debug mode disabled Verify debug off in production
Dependencies Vulnerable libraries Run bundler-audit
Dependencies Outdated dependencies Check for available updates

Common Vulnerability Patterns

Vulnerability Ruby Pattern Secure Alternative
SQL Injection User.where("name = '#{params[:name]}')") User.where(name: params[:name])
XSS raw(user_input) sanitize(user_input)
XSS user_input.html_safe Automatic escaping via <%= %>
Command Injection system("ls #{params[:dir]}") Open3.capture3("ls", params[:dir])
Path Traversal File.read(params[:file]) File.read(Rails.root.join('safe', basename(params[:file])))
Unsafe Deserialization Marshal.load(params[:data]) JSON.parse(params[:data])
Unsafe YAML YAML.load(user_input) YAML.safe_load(user_input, permitted_classes: [Symbol])
Mass Assignment User.new(params[:user]) User.new(user_params)
Weak Random rand(1000000) SecureRandom.random_number(1000000)
Timing Attack user && user.password == password ActiveSupport::SecurityUtils.secure_compare

Security Testing Tools

Tool Purpose Usage
Brakeman Static analysis for Rails brakeman -A
bundler-audit Dependency vulnerability scanning bundle-audit check
RuboCop Security Security linting rubocop --only Security
OWASP ZAP Dynamic application testing zap-cli quick-scan
Rack::Attack Rate limiting and throttling Middleware configuration
SecureHeaders HTTP security headers Rails initializer
Devise Secure authentication Authentication gem
bcrypt Password hashing User.has_secure_password

HTTP Security Headers

Header Purpose Recommended Value
Strict-Transport-Security Force HTTPS max-age=31536000; includeSubDomains
X-Frame-Options Prevent clickjacking DENY
X-Content-Type-Options Prevent MIME sniffing nosniff
X-XSS-Protection Enable XSS filter 1; mode=block
Content-Security-Policy Restrict resource loading default-src 'self'
Referrer-Policy Control referrer information strict-origin-when-cross-origin
Permissions-Policy Control browser features geolocation=(), microphone=()

Authentication Security Controls

Control Implementation Test Verification
Password Complexity Minimum length, character requirements Submit weak passwords
Account Lockout Lock after N failed attempts Exceed failure threshold
Session Timeout Idle and absolute timeouts Wait for timeout period
Multi-factor Authentication TOTP or SMS codes Test login flow
Password Reset Security Time-limited tokens Test token expiration
Session Regeneration New ID on privilege change Verify ID changes
Secure Password Storage bcrypt with cost >= 12 Inspect password digest

Common Security Test Assertions

Test RSpec Assertion
Successful response expect(response).to be_successful
Unauthorized access expect(response).to have_http_status(:unauthorized)
Forbidden access expect(response).to have_http_status(:forbidden)
Rate limited expect(response).to have_http_status(:too_many_requests)
Content escaped expect(response.body).to include('<script>')
Content not present expect(response.body).not_to include('')
Session regenerated expect(session.id).not_to eq(old_session_id)
Cookie flags set expect(response.headers['Set-Cookie']).to include('secure')
No error raised expect { action }.not_to raise_error
Value unchanged expect(model.reload.attribute).to eq(original_value)