Web Application Security in Ruby on Rails: A Comprehensive Guide 2024

Chetan MittalChetan Mittal
12 min read

Web application security is the cornerstone of protecting your Rails applications from malicious attacks and unauthorized access.

Think of it as building a fortress around your application – you need strong walls (authentication), guards at the gate (authorization), security cameras (monitoring), and a well-trained security team (security tools and practices).

Why Web Application Security Matters

Every day, web applications process sensitive information like credit card details, personal information, and business data.

Without proper security measures, this data is vulnerable to various attacks:

  1. SQL Injection - Attackers trying to manipulate your database queries

  2. Cross-Site Scripting (XSS) - Malicious scripts injected into your pages

  3. Cross-Site Request Forgery (CSRF) - Unauthorized commands from trusted users

  4. Session Hijacking - Stealing user sessions to impersonate them

  5. Man-in-the-Middle Attacks - Intercepting data in transit

  6. Denial of Service (DoS) - Overwhelming your servers

  7. Security Misconfigurations - Vulnerabilities from improper setup

Rails' Built-in Security Features

Ruby on Rails comes with several security features out of the box.

Let's explore each one and understand how to use them effectively.

1. CSRF Protection

Cross-Site Request Forgery protection prevents attackers from tricking your users into executing unwanted actions.

Rails automatically includes CSRF tokens in your forms.

# This is automatically included in ApplicationController
class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception
  protect_from_forgery with: :exception

  # You can also use :null_session instead of :exception
  # protect_from_forgery with: :null_session
end

# In your views, Rails automatically adds CSRF tokens to forms
<%= form_for @user do |f| %>
  <%= f.text_field :name %>
  <%= f.submit %>
<% end %>

2. SQL Injection Prevention

SQL injection is one of the most common attack vectors.

Rails' Active Record provides safe methods to prevent SQL injection:

class UsersController < ApplicationController
  def search
    # BAD - Vulnerable to SQL injection
    # User.where("name = '#{params[:name]}'")

    # GOOD - Safe parameterized query
    @users = User.where(name: params[:name])

    # GOOD - If you need complex queries
    @users = User.where("name LIKE ?", "%#{params[:name]}%")

    # GOOD - Multiple conditions
    @users = User.where(
      "created_at >= ? AND role = ?",
      params[:start_date],
      params[:role]
    )
  end
end

3. XSS Protection

Cross-Site Scripting attacks inject malicious scripts into your pages.

Rails provides several helpers to prevent XSS:

# In your views
<% # BAD - Vulnerable to XSS %>
<%= @user.profile_html %>

<% # GOOD - Escapes HTML %>
<%= sanitize @user.profile_html %>

<% # GOOD - Rails automatically escapes html by default %>
<%= @user.name %>

<% # GOOD - Using safe HTML with specific tags %>
<%= sanitize @user.bio, tags: %w(p br strong em), attributes: %w(id class) %>

Implementing Authentication

Authentication verifies user identity.

While Rails pre-8 versions don't include authentication by default, Devise is the most popular solution:

# Gemfile
gem 'devise'

# Generate devise configuration
# $ rails generate devise:install

# app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :lockable, :timeoutable, :trackable

  # Add custom fields
  validates :username, presence: true, uniqueness: true

  # Add security features
  def lock_access!
    self.failed_attempts = 0
    super
  end
end

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:username])
  end
end

Authorization - Controlling Access

Authorization determines what authenticated users can do.

Pundit provides an elegant way to handle authorization:

# Gemfile
gem 'pundit'

# app/policies/application_policy.rb
class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end
end

# app/policies/article_policy.rb
class ArticlePolicy < ApplicationPolicy
  def update?
    user.admin? || record.author == user
  end

  def destroy?
    user.admin? || record.author == user
  end
end

# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  before_action :set_article, only: [:show, :edit, :update, :destroy]

  def update
    authorize @article
    if @article.update(article_params)
      redirect_to @article, notice: 'Updated successfully'
    else
      render :edit
    end
  end

  private

  def set_article
    @article = Article.find(params[:id])
  end
end

Secure File Uploads

File uploads can be dangerous if not handled properly.

Here's how to implement secure file uploads:

# app/models/document.rb
class Document < ApplicationRecord
  has_one_attached :file

  # Define allowed content types
  ALLOWED_TYPES = %w[
    application/pdf
    application/msword
    image/jpeg
    image/png
  ].freeze

  # Validations
  validate :acceptable_file

  private

  def acceptable_file
    return unless file.attached?

    unless file.byte_size <= 10.megabyte
      errors.add(:file, "is too large (max 10MB)")
    end

    unless ALLOWED_TYPES.include?(file.content_type)
      errors.add(:file, "must be a PDF, Word, JPEG, or PNG")
    end
  end
end

# app/controllers/documents_controller.rb
class DocumentsController < ApplicationController
  def create
    @document = current_user.documents.build(document_params)

    if @document.save
      process_file_in_background(@document)
      redirect_to @document, notice: 'File uploaded successfully'
    else
      render :new
    end
  end

  private

  def document_params
    params.require(:document).permit(:file)
  end

  def process_file_in_background(document)
    ProcessDocumentJob.perform_later(document)
  end
end

Security Tools for Rails in 2024

Let's dive deep into the essential security tools available for Ruby on Rails applications. Each tool serves a specific purpose in your security arsenal.

1. Brakeman - Static Analysis Security Scanner

Brakeman is your first line of defense in security testing. It performs static analysis of your Rails application code to identify potential security vulnerabilities before they reach production.

# Add to Gemfile
group :development, :test do
  gem 'brakeman', require: false
end

# Create a custom configuration file (.brakeman.yml)
:skip_files:
  - 'vendor/**/*'
  - 'node_modules/**/*'
:ignore_model_output: false
:exit_on_warn: true
:output_files:
  - 'tmp/brakeman.html'
  - 'tmp/brakeman.json'

Key Features:

  • Identifies SQL injection risks

  • Detects XSS vulnerabilities

  • Finds unprotected mass assignments

  • Checks for session setting issues

  • Scans for unsafe redirects

  • Identifies unsafe rendering

Example usage and output:

# Basic scan
$ brakeman

# Scan specific files or directories
$ brakeman app/controllers app/models

# Generate a detailed HTML report
$ brakeman -o brakeman-report.html

# Example output:
# >> Security Warnings:
# >> Cross-Site Scripting: ... [High]
# >> SQL Injection: ... [High]
# >> Mass Assignment: ... [Medium]

2. Bundle Audit - Dependency Security Scanner

Bundle Audit helps you identify known security vulnerabilities in your application's dependencies by checking against the Ruby Advisory Database.

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

# Create a rake task (lib/tasks/security.rake)
namespace :security do
  desc 'Run bundle-audit'
  task :audit do
    require 'bundler/audit/cli'
    Bundler::Audit::CLI.start(['check', '--update'])
  end

  desc 'Update vulnerability database'
  task :update do
    require 'bundler/audit/cli'
    Bundler::Audit::CLI.start(['update'])
  end
end

Key Features:

  • Scans Gemfile.lock for vulnerable versions

  • Checks for insecure gem sources

  • Provides detailed vulnerability information

  • Regular database updates

Example usage:

# Update vulnerability database
$ bundle audit update

# Check for vulnerabilities
$ bundle audit check

# Example output:
# Name: actionpack
# Version: 6.0.0
# Advisory: CVE-2020-8164
# Criticality: High
# URL: https://groups.google.com/forum/#!topic/rubyonrails-security/f6ioe4sdpbY
# Title: Possible Strong Parameters Bypass in ActionPack
# Solution: upgrade to ~> 6.0.3.1

3. Rack::Attack - Request Throttling and Blocking

Rack::Attack is a rack middleware for blocking and throttling abusive requests. It's essential for protecting your application from DDoS attacks and brute force attempts.

# Add to Gemfile
gem 'rack-attack'

# config/initializers/rack_attack.rb
class Rack::Attack
  ### Configure Cache ###
  Rack::Attack.cache.store = ActiveSupport::Cache::RedisCacheStore.new(url: ENV["REDIS_URL"])

  ### Throttle Spammy Clients ###
  throttle('req/ip', limit: 300, period: 5.minutes) do |req|
    req.ip unless req.path.start_with?('/assets')
  end

  ### Prevent Brute-Force Login Attacks ###
  throttle('logins/ip', limit: 5, period: 20.seconds) do |req|
    if req.path == '/users/sign_in' && req.post?
      req.ip
    end
  end

  ### Block Suspicious User Agents ###
  blocklist('block suspicious UA') do |req|
    req.user_agent =~ /^BadBot/ ||
    req.user_agent =~ /^BlackWidow/ ||
    req.user_agent =~ /^Bot\ mailto:craftbot@yahoo.com/ ||
    req.user_agent =~ /^ChinaClaw/ ||
    req.user_agent =~ /^Custo/ ||
    req.user_agent =~ /^DISCo/ ||
    req.user_agent =~ /^Download\ Demon/ ||
    req.user_agent =~ /^eCatch/ ||
    req.user_agent =~ /^EirGrabber/
  end

  ### Custom Throttle Response ###
  self.throttled_response = lambda do |env|
    [ 429, # status
      {'Content-Type' => 'application/json'}, # headers
      [{error: "Too many requests. Please try again later."}.to_json] # body
    ]
  end
end

Key Features:

  • Request rate limiting

  • IP-based blocking

  • User agent blocking

  • Custom throttling rules

  • Flexible response configuration

4. Dawn Scanner - Configuration Analysis

Dawn Scanner examines your Rails application's configuration and code for security issues, including OWASP Top 10 vulnerabilities.

# Add to Gemfile
group :development, :test do
  gem 'dawnscanner'
end

# Create a custom rake task (lib/tasks/security.rake)
namespace :security do
  desc 'Run Dawn security checks'
  task scan: :environment do
    system('dawn --html -F results.html .')  # Generate HTML report
    system('dawn --console -F yaml .')       # Console output
  end
end

Key Features:

  • Analyzes security configurations

  • Detects OWASP Top 10 vulnerabilities

  • Reviews Rails security best practices

  • Validates authentication setups

  • Examines session configurations

  • Checks cookie settings

  • Identifies database security issues

  • Provides detailed HTML reports

5. RuboCop - Code Quality and Security Linter

While primarily known as a style checker, RuboCop includes security-related checks through its Security department.

# Add to Gemfile
group :development, :test do
  gem 'rubocop'
  gem 'rubocop-rails'
end

# Create .rubocop.yml configuration
require:
  - rubocop-rails

AllCops:
  NewCops: enable

# Security-specific configurations
Security/Eval:
  Enabled: true

Security/JSONLoad:
  Enabled: true

Security/MarshalLoad:
  Enabled: true

Security/Open:
  Enabled: true

Key Features:

  • Identifies unsafe method usage

  • Checks for secure defaults

  • Validates proper encoding

  • Ensures safe deserialization

6. Secure Headers - HTTP Header Security

The Secure Headers gem helps you set security-related HTTP headers properly.

# Add to 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 = %w(origin-when-cross-origin strict-origin-when-cross-origin)

  # Content Security Policy
  config.csp = {
    default_src: %w('none'),
    script_src: %w('self' 'unsafe-inline'),
    style_src: %w('self' 'unsafe-inline'),
    img_src: %w('self' data:),
    connect_src: %w('self'),
    font_src: %w('self'),
    base_uri: %w('self'),
    form_action: %w('self'),
    frame_ancestors: %w('none'),
    block_all_mixed_content: true,
    upgrade_insecure_requests: true
  }
end

Key Features:

  • Sets security headers automatically

  • Configurable CSP policies

  • HSTS support

  • XSS protection headers

  • Clickjacking prevention

7. Devise Security - Enhanced Authentication Security

An extension to Devise that adds additional security features.

# Add to Gemfile
gem 'devise'
gem 'devise-security'

# Enable security extensions in user model
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :password_expirable, :password_archivable,
         :session_limitable, :security_questionable

  # Password strength validation
  validate :password_complexity

  private

  def password_complexity
    return if password.nil?

    unless password.match(/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/)
      errors.add :password, 'must include at least one uppercase letter, one lowercase letter, one number, and one special character'
    end
  end
end

Key Features:

  • Password expiration

  • Password history

  • Session limitations

  • Security questions

  • Strong password validation

8. Active Record Doctor - Database Security Analysis

This tool helps identify potential security issues in your database configuration and ActiveRecord usage.

# Add to Gemfile
group :development, :test do
  gem 'active_record_doctor'
end

# Run analysis
$ rake active_record_doctor:analyze

# Example output:
# Unindexed Foreign Keys:
# - comments.post_id
# Missing Foreign Key Constraints:
# - comments.user_id
# Extraneous Indexes:
# - index_users_on_email (can be replaced by unique_index_users_on_email)

Key Features:

  • Identifies missing indexes

  • Finds redundant indexes

  • Checks foreign key constraints

  • Validates database configuration

Security Best Practices

Keep Dependencies Updated

# Regular updates
$ bundle update --conservative

# Check for vulnerabilities
$ bundle audit

Secure Configuration

# config/environments/production.rb
Rails.application.configure do
  # Force SSL
  config.force_ssl = true

  # Secure cookies
  config.session_store :cookie_store,
    key: '_app_session',
    secure: true,
    httponly: true,
    same_site: :strict

  # Content Security Policy
  config.content_security_policy do |policy|
    policy.default_src :self
    policy.font_src    :self, :https, :data
    policy.img_src     :self, :https, :data
    policy.object_src  :none
    policy.script_src  :self
    policy.style_src   :self, :https
  end
end

Environment Variables

# Use dotenv for development
# Gemfile
gem 'dotenv-rails', groups: [:development, :test]

# .env
DATABASE_URL=postgresql://localhost/myapp
SECRET_KEY_BASE=your-secret-key
AWS_ACCESS_KEY=your-access-key

# config/initializers/secret_token.rb
MyApp::Application.config.secret_key_base = ENV['SECRET_KEY_BASE']

Security Monitoring

Implement logging and monitoring to detect security issues:

# config/initializers/lograge.rb
Rails.application.configure do
  config.lograge.enabled = true
  config.lograge.custom_options = lambda do |event|
    {
      user_id: event.payload[:user_id],
      ip: event.payload[:ip],
      params: event.payload[:params],
      exception: event.payload[:exception]&.first
    }
  end
end

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  def append_info_to_payload(payload)
    super
    payload[:user_id] = current_user&.id
    payload[:ip] = request.remote_ip
  end
end

[Previous sections remain the same...]

Bonus: New Security Tool for Rails 7+ Applications

Rails 7 and 8 introduce several new security tools and features that enhance application security. Here is a one notable addition:

ActiveRecord::Encryption

A new built-in encryption system for protecting sensitive database attributes without additional gems.

# app/models/user.rb
class User < ApplicationRecord
  encrypts :social_security_number, deterministic: true
  encrypts :personal_notes
  encrypts :credit_card_number, deterministic: true, downcase: true

  # Custom encryption key per attribute
  encrypts :health_data, key: :health_encryption_key

  private

  def health_encryption_key
    # Custom key derivation logic
    @health_key ||= Rails.application.credentials.health_data_key
  end
end

# config/credentials.yml.enc
active_record_encryption:
  primary_key: <%= SecureRandom.alphanumeric(32) %>
  deterministic_key: <%= SecureRandom.alphanumeric(32) %>
  key_derivation_salt: <%= SecureRandom.alphanumeric(32) %>

Key Features:

  • Built-in attribute encryption

  • Deterministic and non-deterministic encryption

  • Key rotation support

  • Transparent encryption/decryption

  • Search capability on encrypted fields

Conclusion

Security is not a one-time setup but a continuous process.

Regular security audits, keeping dependencies updated, and following security best practices are essential.

The tools and practices covered in this guide provide a solid foundation for securing your Rails applications.

Remember:

  • Use Rails' built-in security features

  • Implement authentication and authorization properly

  • Validate and sanitize all user input

  • Keep your dependencies updated

  • Monitor your application for security issues

  • Regular security audits

  • Use security tools and frameworks

Security is everyone's responsibility on the development team.

Make it a part of your development culture, not an afterthought.

Frequently Asked Questions (FAQs)

1. How often should I update my Rails application's dependencies?

Answer: You should review and update dependencies at least monthly. However, security-related updates should be applied immediately. Set up automated security alerts using:

# Gemfile
gem 'bundler-audit'
gem 'ruby_audit'

# Add to your deployment pipeline
namespace :security do
  desc 'Run security audits'
  task :audit do
    sh 'bundle-audit check --update'
    sh 'ruby-audit check'
  end
end

2. What's the best way to handle user passwords in Rails?

Answer: Use has_secure_password with bcrypt and enforce strong password policies:

class User < ApplicationRecord
  has_secure_password

  # Password validation
  validate :password_complexity

  private

  def password_complexity
    return if password.blank?

    requirements = {
      digit: /\d/,
      uppercase: /[A-Z]/,
      lowercase: /[a-z]/,
      special: /[^A-Za-z0-9]/
    }

    requirements.each do |type, regex|
      unless password.match?(regex)
        errors.add(:password, "must include at least one #{type}")
      end
    end
  end
end

3. How can I protect my Rails API from rate limiting and abuse?

Answer: Implement rate limiting and API authentication:

# config/initializers/rack_attack.rb
class Rack::Attack
  # Rate limiting by IP
  throttle('api/ip', limit: 100, period: 1.minute) do |req|
    req.ip if req.path.start_with?('/api/')
  end

  # Rate limiting by API key
  throttle('api/key', limit: 1000, period: 1.hour) do |req|
    req.headers['X-API-Key']
  end
end

# app/controllers/api/base_controller.rb
module Api
  class BaseController < ApplicationController
    before_action :verify_api_key

    private

    def verify_api_key
      api_key = request.headers['X-API-Key']
      @api_client = ApiClient.find_by_active_key(api_key)

      head :unauthorized unless @api_client
    end
  end
end

4. What security headers should I enable in my Rails application?

Answer: Enable these essential security headers:

# config/initializers/security_headers.rb
Rails.application.config.action_dispatch.default_headers = {
  'X-Frame-Options' => 'SAMEORIGIN',
  'X-XSS-Protection' => '1; mode=block',
  'X-Content-Type-Options' => 'nosniff',
  'X-Download-Options' => 'noopen',
  'X-Permitted-Cross-Domain-Policies' => 'none',
  'Referrer-Policy' => 'strict-origin-when-cross-origin',
  'Content-Security-Policy' => [
    "default-src 'self'",
    "img-src 'self' data: https:",
    "style-src 'self' 'unsafe-inline'",
    "script-src 'self'",
    "connect-src 'self'",
    "frame-ancestors 'none'"
  ].join('; ')
}

5. How do I implement secure file uploads in Rails?

Answer: Implement proper validation, virus scanning, and storage:

class Document < ApplicationRecord
  has_one_attached :file

  validates :file,
    content_type: %w[application/pdf application/msword],
    size: { less_than: 10.megabytes }

  after_create :scan_for_viruses

  private

  def scan_for_viruses
    temp_file = file.download
    scan_result = VirusScanner.scan(temp_file)

    if scan_result.infected?
      file.purge
      raise SecurityError, "Infected file detected"
    end
  ensure
    temp_file.close
    temp_file.unlink
  end
end

6. What are the best practices for session management in Rails?

Answer: Implement secure session handling:

# config/initializers/session_store.rb
Rails.application.config.session_store :redis_store,
  servers: [ENV['REDIS_URL']],
  expire_after: 4.hours,
  key: '_secure_session_id',
  secure: Rails.env.production?,
  httponly: true,
  same_site: :strict

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :verify_session
  after_action :update_session_timestamp

  private

  def verify_session
    if session[:created_at].present? && session[:created_at] < 12.hours.ago
      reset_session
      redirect_to new_session_path
    end
  end

  def update_session_timestamp
    session[:last_activity] = Time.current
  end
end
0
Subscribe to my newsletter

Read articles from Chetan Mittal directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Chetan Mittal
Chetan Mittal

I stumbled upon Ruby on Rails beta version in 2005 and has been using it since then. I have also trained multiple Rails developers all over the globe. Currently, providing consulting and advising companies on how to upgrade, secure, optimize, monitor, modernize, and scale their Rails apps.