Ultimate Rails Security Guide: Best Practices for Ruby on Rails Applications in 2025

BestWeb VenturesBestWeb Ventures
10 min read

The digital ecosystem is becoming increasingly vulnerable to cyberattacks.

With the proliferation of web applications, security risks such as data breaches, injection attacks, and exploitation of misconfigured systems are escalating.

Rails, as a robust web application framework, has maintained its reputation for being "secure by default."

However, securing a Rails application requires more than relying on its built-in features—it necessitates proactive strategies, continuous learning, and adherence to best practices.

The latest releases of Ruby 3.3.6 and Rails 8 introduce cutting-edge enhancements that provide developers with tools to build applications that are both functional and secure.

These updates emphasize encryption, improved session management, and query sanitization, addressing modern threats comprehensively.

This guide equips developers with strategies to harness Rails' security features effectively.


1. Common Vulnerabilities

Understanding vulnerabilities that Rails applications may face is a critical first step in securing them.


1.1 Command Injection

Command injection occurs when user input is executed as part of a system-level command. It can compromise the entire server hosting the application.

Why It Happens

Methods like system, exec, or backticks (``) allow direct interaction with the operating system shell, making them inherently risky if user inputs are not sanitized.

Examples and Best Practices

A typical insecure implementation:

# Vulnerable code
system("ping #{params[:target]}")

If params[:target] is unvalidated, an attacker could inject a command like ; rm -rf / to delete server files.

To secure this:

  1. Use safer syntax: The array format prevents shell metacharacters from being interpreted:

     system("ping", params[:target])
    
  2. Validate Inputs: Allow only safe and expected values:

     valid_targets = %w[localhost 127.0.0.1]
     raise ArgumentError, "Invalid target" unless valid_targets.include?(params[:target])
    

1.2 SQL Injection

SQL injection exploits vulnerabilities in database query formation, allowing attackers to manipulate SQL statements.

Rails Protections

Rails mitigates SQL injection by default through parameterized queries and Active Record's query interface.

An insecure example:

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

A secure implementation:

User.where(email: params[:email])

Rails 8 introduces sanitize_sql_like, a new method for escaping special characters in inputs for LIKE queries:

search_term = "#{params[:search]}%"
User.where("name LIKE ?", User.sanitize_sql_like(search_term))

Additional Best Practices

  1. Use Prepared Statements: Always prefer Rails' query methods like find_by or where.

  2. Avoid Raw SQL: If raw SQL is necessary, sanitize inputs thoroughly.


1.3 Cross-Site Scripting (XSS)

XSS allows attackers to inject malicious scripts into webpages that users trust, potentially stealing session tokens or other sensitive information.

Rails' Built-In XSS Protection

By default, Rails escapes all user inputs in views, preventing most XSS attacks.

However, carelessness with methods like html_safe can lead to vulnerabilities:

# Vulnerable code
<%= user_input.html_safe %>

To safely allow some HTML, use the sanitize helper:

<%= sanitize user_input, tags: %w(strong em a), attributes: %w(href) %>

Content Security Policy (CSP)

Implement a CSP to restrict script sources:

Rails.application.config.content_security_policy do |policy|
  policy.script_src :self, :https
end

1.4 Cross-Site Request Forgery (CSRF)

CSRF tricks users into performing actions they didn’t intend, using their authentication status against them.

Rails CSRF Protection

Rails includes CSRF protection by default, embedding authenticity tokens into forms:

<%= form_with model: @user do |f| %>
  <%= f.text_field :name %>
<% end %>

Best Practices

  1. Enable protect_from_forgery in controllers:

     protect_from_forgery with: :exception
    
  2. For APIs, use secure headers to authenticate requests instead of CSRF tokens.


1.5 Mass Assignment

Mass assignment vulnerabilities occur when untrusted user inputs modify sensitive attributes.

Strong Parameters

Rails uses Strong Parameters to control which attributes can be mass-assigned:

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

2. Framework Security Features

Rails offers a rich set of security tools to minimize vulnerabilities. However, using these tools effectively requires a solid understanding of their configuration and application.


2.1 HTTP Security Headers

Rails’ built-in support for HTTP security headers makes it easier to protect applications from browser-based attacks like clickjacking, MIME-type sniffing, and mixed content. Here's a deeper look:

  • Strict-Transport-Security (HSTS):
    Forces browsers to use HTTPS and avoid insecure HTTP requests.
    Example configuration:

      config.force_ssl = true
    
  • Content Security Policy (CSP):
    Restricts the sources of scripts, styles, and other assets, reducing the risk of XSS.
    Example:

      config.content_security_policy do |policy|
        policy.default_src :self
        policy.script_src :self, :https
        policy.style_src :self, :https
      end
    
  • X-Frame-Options and X-XSS-Protection:
    These headers mitigate clickjacking and XSS attacks by default in Rails.

2.2 Logging Sensitive Data

Rails offers middleware to filter sensitive data, such as passwords or tokens, from logs. Misconfigured logging can expose private information.

Example:

config.filter_parameters += [:password, :credit_card_number]

2.3 Handling File Uploads Securely

Improperly handled file uploads can lead to vulnerabilities such as unrestricted file uploads or directory traversal. Use gems like CarrierWave or ActiveStorage and validate file types explicitly:

validates :avatar, content_type: ['image/png', 'image/jpg', 'image/jpeg']

3. Advanced Topics

Beyond the basics, advanced security techniques strengthen your application against evolving threats.


3.1 Managing Credentials and Secrets

Rails’ encrypted credentials system simplifies secret management. However, it's essential to keep encrypted files out of source control for enhanced security.

  • Environment-Specific Credentials:
    Separate production and development credentials to avoid exposing sensitive keys in non-production environments.

  • Rotate Keys Regularly:
    Rotate and revoke secrets periodically to minimize the risk of exposure.

3.2 Role-Based Access Control (RBAC)

Gems like Pundit and CanCanCan make it easier to implement role-based authorization, ensuring users can only access resources appropriate for their roles.

Example with Pundit:

class PostPolicy < ApplicationPolicy
  def update?
    user.admin? || record.author == user
  end
end

3.3 Secure APIs

When building APIs, use the following security practices:

  • Token-Based Authentication:
    Prefer Bearer tokens or OAuth for stateless authentication.

  • Rate Limiting:
    Implement rate limits to prevent abuse using middleware like Rack::Attack.

  • CORS Configuration:
    Restrict cross-origin requests to trusted domains using rack-cors.

3.4 Authentication Security Best Practices

Authentication is a critical security component that requires careful implementation and ongoing maintenance.

Password Security

Implement strong password policies using Rails' built-in validations:

class User < ApplicationRecord
  has_secure_password

  validates :password, 
    length: { minimum: 12 },
    format: { 
      with: /\A(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/,
      message: "must include uppercase, lowercase, number and special character"
    }
end

This code:

  • Enforces minimum password length of 12 characters

  • Requires mix of uppercase, lowercase, numbers, and special characters

  • Uses Rails' has_secure_password for secure password hashing

Session Management

Configure secure session settings to prevent session-based attacks:

# config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store,
  key: '_app_session',
  expire_after: 12.hours,
  secure: Rails.env.production?,
  same_site: :strict,
  httponly: true

This configuration:

  • Sets session expiration to 12 hours

  • Enforces secure flag in production

  • Implements SameSite strict policy

  • Enables HttpOnly flag to prevent XSS attacks

Multi-Factor Authentication (MFA)

Implement two-factor authentication using devise-two-factor:

class User < ApplicationRecord
  devise :two_factor_authenticatable,
         :two_factor_backupable

  def generate_two_factor_code
    self.otp_secret = User.generate_otp_secret
    self.otp_code = ROTP::TOTP.new(otp_secret).now
    save
  end
end

This implementation:

  • Adds two-factor authentication capability

  • Includes backup codes for account recovery

  • Uses time-based one-time passwords (TOTP)

Account Lockout

Protect against brute force attacks with account lockout:

class User < ApplicationRecord
  def failed_attempt
    self.failed_attempts += 1
    if failed_attempts > 5
      self.locked_at = Time.current
    end
    save
  end
end

This mechanism:

  • Tracks failed login attempts

  • Locks account after 5 failed attempts

  • Requires manual unlock or time-based automatic unlock

3.5 Enhanced API Security

JWT Implementation

Implement secure JWT handling for API authentication:

class JsonWebToken
  SECRET_KEY = Rails.application.credentials.secret_key_base

  def self.encode(payload, exp = 24.hours.from_now)
    payload[:exp] = exp.to_i
    JWT.encode(payload, SECRET_KEY)
  end

  def self.decode(token)
    decoded = JWT.decode(token, SECRET_KEY)[0]
    HashWithIndifferentAccess.new decoded
  rescue JWT::ExpiredSignature
    nil
  end
end

This JWT implementation:

  • Uses Rails' secret key base for token signing

  • Includes token expiration

  • Handles expired tokens gracefully

Rate Limiting

Protect your API from abuse with rate limiting:

# config/initializers/rack_attack.rb
class Rack::Attack
  Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new

  # Limit all API requests to 300 per 5 minutes
  throttle('req/ip', limit: 300, period: 5.minutes) do |req|
    req.ip if req.path.start_with?('/api/')
  end

  # Stricter limits for authentication endpoints
  throttle('logins/ip', limit: 5, period: 20.seconds) do |req|
    req.ip if req.path == '/api/login' && req.post?
  end
end

This configuration:

  • Sets general API rate limits

  • Implements stricter limits for sensitive endpoints

  • Uses IP-based throttling

3.6 WebSocket Security

When using Action Cable, implement proper authentication and authorization:

# app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    private
    def find_verified_user
      verified_user = User.find_by(id: cookies.signed['user.id'])
      verified_user || reject_unauthorized_connection
    end
  end
end

This setup:

  • Authenticates WebSocket connections

  • Ties connections to specific users

  • Rejects unauthorized connections

3.7 Secure File Upload

Implement secure file uploads with ActiveStorage:

class Document < ApplicationRecord
  has_one_attached :file

  validate :acceptable_file

  private

  def acceptable_file
    return unless file.attached?

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

    acceptable_types = ["application/pdf", "image/jpeg", "image/png"]
    unless acceptable_types.include?(file.content_type)
      errors.add(:file, "must be a PDF, JPEG or PNG")
    end
  end
end

This implementation:

  • Validates file size limits

  • Restricts file types

  • Prevents malicious file uploads

3.8 Database Encryption

Implement encryption for sensitive data:

class User < ApplicationRecord
  encrypts :social_security_number, deterministic: true
  encrypts :medical_notes
  encrypts :bank_account_number, downcase: true

  # Key rotation
  def rotate_encryption_key!
    self.class.encrypt_attributes.each do |attribute|
      clear_attribute_changes([attribute])
      send("#{attribute}_will_change!")
    end
    save!
  end
end

This setup:

  • Encrypts sensitive fields

  • Supports deterministic encryption where needed

  • Includes key rotation capability

3.9 Security Headers Configuration

# 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',
  'Permissions-Policy' => 'accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()'
}

Rails.application.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, :https
  policy.style_src   :self, :https
  policy.connect_src :self, :https
  policy.base_uri    :self
  policy.frame_ancestors :none
  policy.form_action :self
  policy.require_trusted_types_for 'script'
end

This comprehensive header configuration:

  • Prevents clickjacking attacks

  • Enables XSS protection

  • Prevents MIME type sniffing

  • Implements strict CSP rules

  • Controls browser feature permissions

3.10 Privacy Compliance

Implement privacy-related features for compliance with regulations like GDPR and CCPA:

class User < ApplicationRecord
  def data_export
    {
      personal_info: {
        email: email,
        name: name,
        created_at: created_at
      },
      activity_logs: activity_logs.as_json,
      preferences: user_preferences.as_json
    }.to_json
  end

  def forget_me!
    update(
      email: "redacted_#{SecureRandom.hex(6)}@deleted",
      name: "Redacted User",
      deleted_at: Time.current
    )
    activity_logs.destroy_all
  end
end

This implementation:

  • Provides data export functionality

  • Implements right to be forgotten

  • Maintains privacy audit trail


4. Testing and Auditing

Regular testing and auditing are essential for identifying and addressing vulnerabilities.


4.1 Automated Security Testing

Security testing tools integrate into CI/CD pipelines to identify vulnerabilities during development:

  • Brakeman: A static analysis tool for Rails applications that detects common security issues.

  • Bundler-Audit: Checks for known vulnerabilities in gem dependencies.

Example CI configuration:

jobs:
  security:
    steps:
      - uses: actions/checkout@v2
      - run: brakeman -A
      - run: bundle audit check --update

4.2 Manual Code Reviews

In addition to automated scans, regular peer code reviews can identify security risks that automated tools might miss.

4.3 Dependency Management

Use tools like dependabot to ensure all dependencies remain up to date with the latest security patches.


5. Staying Updated

Staying informed about security threats and updates is crucial for maintaining application security.


5.1 Monitoring and Alerts

Leverage monitoring tools like New Relic, Sentry, or Honeybadger to track anomalies in real-time.

We at Bestweb Ventures use Honeybadger for monitoring our own and client’s MVPs as it gives us the needed monitoring features in its free version.

5.2 Learning Resources

Keep up with Rails and security best practices:

5.3 Contribution to Security

Contributing to open-source security projects, reporting vulnerabilities, or participating in bug bounty programs helps strengthen the ecosystem.


6. Conclusion

Security in Rails applications is not just about fixing bugs—it’s about creating a culture of vigilance and continuous improvement.

Here’s how to achieve it:

  1. Embrace Rails’ Built-In Security Features: Rails provides strong defaults for protecting against common vulnerabilities. Ensure they are enabled and configured correctly.

  2. Implement Robust Access Control: Use role-based access control and strong parameter validation to prevent unauthorized access.

  3. Regularly Audit and Test: Adopt automated and manual testing to detect vulnerabilities early.

  4. Stay Proactive: Keep your dependencies, Rails version, and libraries updated. Monitor security trends and participate in the broader developer community.

  5. Educate and Empower Your Team: Security is a shared responsibility. Train your team to recognize and address potential risks proactively.

By integrating these practices into your development workflow, you not only protect your users but also enhance the resilience and reputation of your application.

Rails, combined with the right strategies, offers the foundation for building secure, high-quality web applications.

0
Subscribe to my newsletter

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

Written by

BestWeb Ventures
BestWeb Ventures

From cutting-edge web and app development, using Ruby on Rails, since 2005 to data-driven digital marketing strategies, we build, operate, and scale your MVP (Minimum Viable Product) for sustainable growth in today's competitive landscape.