By Harsh Patel, Cyber security Enthusiast & Senior Ruby on Rails Dev
Ruby on Rails has empowered countless developers to build web applications rapidly and elegantly. However, with great power comes great responsibility. In today’s threat landscape, securing your Rails application isn’t optional—it’s an absolute necessity. In this comprehensive guide, I’ll walk you through essential security best practices that every Rails developer, from intermediate to advanced, should understand and implement immediately to protect their applications against common vulnerabilities and emerging threats.
Table of Contents
- Introduction and Context
-
Key Security Best Practices in Ruby on Rails
- Protection Against SQL Injection
- Mitigation of Cross-Site Scripting (XSS)
- Secure Management of Secrets, Credentials, and Sensitive Configuration Data
- Proper Session Management and Secure Cookies
- Cross-Site Request Forgery (CSRF) Protection Strategies
- Secure File Uploads and Handling Attachments
- Robust Authentication and Authorization
- Continuous Security Monitoring and Auditing
- Additional Gems, Libraries, and Tools
- Security Best Practices Checklist
- Conclusion
Introduction and Context
In the world of web development, security is a cornerstone of sustainable and trustworthy software. With Rails’ “convention over configuration” philosophy, many security features are built-in—but this does not mean you can afford to be complacent. Web applications are constantly targeted by attackers exploiting weaknesses such as:
- SQL Injection: Malicious queries that manipulate your database.
- Cross-Site Scripting (XSS): Injection of malicious scripts into webpages.
- Session Hijacking: Stealing or manipulating session data.
- Cross-Site Request Forgery (CSRF): Unauthorized commands transmitted from a user that the web application trusts.
- Insecure File Uploads: Exploiting file attachments to execute arbitrary code.
- Exposure of Sensitive Data: Leaking secrets and configuration details.
For a production-grade Rails application, a proactive, layered security approach is essential. The following best practices cover common vulnerabilities and provide actionable code examples and explanations that you can integrate into your projects immediately.
Key Security Best Practices in Ruby on Rails
1. Protection Against SQL Injection
Definition:
SQL Injection occurs when user input is improperly sanitized, allowing attackers to modify SQL queries. This can lead to data theft, data loss, or unauthorized administrative actions.
How Vulnerabilities Arise:
Using string interpolation to build SQL queries:
# Vulnerable code example:
name = params[:name]
@projects = Project.where("name LIKE '#{name}'")
Best Practice Implementation:
Use ActiveRecord’s parameterized queries and built-in sanitization methods to prevent SQL injection.
# Secure code example:
@projects = Project.where("name LIKE ?", "%#{ActiveRecord::Base.sanitize_sql_like(params[:name])}%")
Explanation:
- Parameterized queries automatically escape dangerous characters.
- sanitize_sql_like is particularly useful when using wildcards, ensuring that special SQL characters are handled safely.
2. Mitigation of Cross-Site Scripting (XSS)
Definition:
XSS vulnerabilities occur when attackers inject malicious JavaScript or HTML into your web pages, potentially stealing user data or executing unauthorized actions in the user’s browser.
How Vulnerabilities Arise:
Directly outputting user input without proper sanitization:
<!-- Vulnerable ERB template example: -->
<%= raw @product.description %>
Best Practice Implementation:
Leverage Rails’ automatic HTML escaping and use sanitization helpers when you need to allow limited HTML.
<!-- Secure ERB template example: -->
<%= @product.description %>
If you must allow HTML:
# Using sanitize helper to allow specific tags:
allowed_tags = %w[b i u p br]
@safe_content = sanitize(params[:content], tags: allowed_tags)
Explanation:
- Rails auto-escapes by default to prevent XSS.
- The sanitize helper can be used to whitelist safe HTML tags when user-generated content is required.
3. Secure Management of Secrets, Credentials, and Sensitive Configuration Data
Definition:
Storing sensitive information such as API keys, database credentials, and secret tokens in plaintext or source control exposes your application to severe risks.
How Vulnerabilities Arise:
Including secrets directly in configuration files:
# Bad practice: config/secrets.yml containing plain-text credentials
production:
secret_key_base: "super_secret_key"
Best Practice Implementation:
Utilize Rails’ encrypted credentials and environment variables to store sensitive data securely.
# To edit credentials in Rails 6+:
EDITOR="vim" rails credentials:edit
Within the encrypted credentials file, store secrets securely:
# config/credentials.yml.enc (example content)
aws:
access_key_id: your_access_key_id
secret_access_key: your_secret_access_key
secret_key_base: your_production_secret_key
And access them in your application:
# Accessing credentials securely:
Rails.application.credentials.aws[:access_key_id]
Rails.application.credentials.secret_key_base
Explanation:
- Rails encrypted credentials ensure that sensitive data remains encrypted at rest and is only accessible with the master key, which should never be committed to source control.
- Environment variables and tools like dotenv-rails can further manage configuration outside your codebase.
4. Proper Session Management and Secure Cookies
Definition:
Sessions track user state across requests, but improperly managed sessions can lead to hijacking or replay attacks.
How Vulnerabilities Arise:
Using default cookie settings without security enhancements:
# Default session store (potential risk):
YourApp::Application.config.session_store :cookie_store, key: '_your_app_session'
Best Practice Implementation:
Enhance session security by using server-side session stores and setting secure cookie options.
ActiveRecord Session Store:
# In Gemfile:
gem 'activerecord-session_store'
# Generate migration and migrate:
rails generate active_record:session_migration
rails db:migrate
# Configure session store:
YourApp::Application.config.session_store :active_record_store, key: '_your_app_session'
Cookie Security Enhancements:
# config/environments/production.rb:
Rails.application.configure do
config.force_ssl = true # Force HTTPS
config.session_store :cookie_store, key: '_your_app_session', secure: Rails.env.production?, httponly: true, same_site: :lax
end
Explanation:
- Server-side session stores reduce exposure as session data is kept on the server.
- Secure cookie flags (
secure
,httponly
, andsame_site
) ensure cookies are only sent over HTTPS, not accessible via JavaScript, and provide cross-site request protections.
5. Cross-Site Request Forgery (CSRF) Protection Strategies
Definition:
CSRF attacks trick authenticated users into submitting unwanted actions on web applications without their consent.
How Vulnerabilities Arise:
An attacker may craft a malicious request that leverages a user’s authenticated session.
Best Practice Implementation:
Rails includes built-in CSRF protection. Ensure it is enabled and properly configured in your controllers.
# In ApplicationController:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception # Raises an exception if CSRF token verification fails
end
Example Form with CSRF Token:
<%= form_with model: @project do |form| %>
<%= form.label :name %>
<%= form.text_field :name %>
<%= form.submit "Save" %>
<% end %>
Explanation:
- protect_from_forgery automatically inserts an authenticity token into forms, which Rails verifies on submission.
- This built-in mechanism defends against CSRF by ensuring that form submissions originate from trusted sources.
6. Secure File Uploads and Handling Attachments
Definition:
File uploads can be exploited to execute arbitrary code if not properly validated, potentially leading to server compromise.
How Vulnerabilities Arise:
Accepting file uploads without validating file types, sizes, or scanning for malware:
# Vulnerable approach: blindly saving uploaded files
def upload
File.open(Rails.root.join('public', 'uploads', params[:file].original_filename), 'wb') do |file|
file.write(params[:file].read)
end
end
Best Practice Implementation:
Use well-maintained libraries such as ActiveStorage or Shrine to handle file uploads securely.
ActiveStorage Example:
- Setup ActiveStorage:
rails active_storage:install
rails db:migrate
- Attach Files to Models:
class User < ApplicationRecord
has_one_attached :avatar
end
- Controller Example with Validation:
class UsersController < ApplicationController
def update
@user = current_user
if params[:user][:avatar]
# Validate file type and size here (e.g., using custom validations or ActiveStorage validations)
@user.avatar.attach(params[:user][:avatar])
end
if @user.save
redirect_to @user, notice: 'Profile updated successfully.'
else
render :edit
end
end
end
Explanation:
- ActiveStorage abstracts file handling securely by storing files externally (e.g., Amazon S3, Google Cloud Storage) and providing validations.
- Always validate file type and size before accepting uploads.
7. Robust Authentication and Authorization
Definition:
Ensuring that users are who they claim to be (authentication) and that they have permissions to access certain resources (authorization) is fundamental to application security.
How Vulnerabilities Arise:
Weak authentication mechanisms or misconfigured access control can lead to unauthorized access.
Best Practice Implementation:
Leverage industry-standard gems like Devise for authentication and Pundit or Cancancan for authorization.
Devise Installation and Setup:
# In Gemfile:
gem 'devise'
# Install Devise:
rails generate devise:install
rails generate devise User
rails db:migrate
Route Protection Example:
Rails.application.routes.draw do
authenticate :user do
resources :projects # Only authenticated users can access these routes
end
devise_for :users
root to: 'home#index'
end
Pundit for Authorization Example:
- Install Pundit:
# In Gemfile:
gem 'pundit'
- Include in ApplicationController:
class ApplicationController < ActionController::Base
include Pundit
# Rescue from unauthorized access
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
flash[:alert] = "You are not authorized to perform this action."
redirect_to(request.referrer || root_path)
end
end
- Define a Policy:
# app/policies/project_policy.rb
class ProjectPolicy < ApplicationPolicy
def update?
user.admin? || record.owner == user
end
end
Explanation:
- Devise provides secure authentication flows.
- Pundit (or Cancancan) enables granular control over resource access, ensuring that users only perform actions for which they have explicit permission.
8. Continuous Security Monitoring and Auditing
Definition:
Security is not a one-time task. Continuous monitoring, auditing, and automated security testing help identify vulnerabilities early and ensure ongoing protection.
How Vulnerabilities Arise:
Without regular audits, even secure applications can accumulate vulnerabilities over time due to outdated dependencies or evolving threats.
Best Practice Implementation:
Static Analysis Tools:
Use tools like Brakeman to scan your codebase for common vulnerabilities.Dependency Auditing:
Utilize bundler-audit to monitor for vulnerabilities in your Gemfile.lock.Security Headers:
Use the secure_headers gem to ensure that HTTP security headers are correctly configured.
Detailed Overview: Brakeman
Brakeman is a static analysis tool designed specifically for Rails applications. It scans your source code without executing it, identifying common security vulnerabilities such as SQL injection, XSS, and unsafe mass assignment.
Installation and Usage
- Install Brakeman:
gem install brakeman
- Run Brakeman in your Rails project:
brakeman
This command analyzes your code and outputs a report of potential vulnerabilities.
- Integrate with CI/CD: You can add Brakeman to your CI pipeline (e.g., in a GitHub Actions workflow) to ensure continuous monitoring:
# .github/workflows/security.yml
name: Security Scan
on: [push, pull_request]
jobs:
brakeman:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
- name: Install dependencies
run: bundle install --jobs 4 --retry 3
- name: Run Brakeman
run: bundle exec brakeman -q -o brakeman-report.json
Explanation:
- Brakeman helps you identify vulnerabilities early in development.
- Regular scans ensure that new vulnerabilities introduced by code changes are caught before deployment.
Detailed Overview: bundler-audit
bundler-audit checks your Gemfile.lock for gems with known vulnerabilities. It compares your dependencies against a database of advisories and alerts you if any are found.
Installation and Usage
- Install bundler-audit:
gem install bundler-audit
- Run bundler-audit in your Rails project:
bundler-audit check --update
This command will:
- Update the advisory database.
- Scan your Gemfile.lock for known vulnerabilities.
- Print a report with recommendations on how to update or patch the affected gems.
- Integrate with CI/CD: For continuous monitoring, include bundler-audit in your CI pipeline:
# .github/workflows/dependency_audit.yml
name: Dependency Audit
on: [push, pull_request]
jobs:
bundler_audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.0'
- name: Install dependencies
run: bundle install --jobs 4 --retry 3
- name: Run bundler-audit
run: bundle exec bundler-audit check --update
Explanation:
- bundler-audit ensures your project dependencies remain secure by flagging outdated or vulnerable gems.
- Integration with your CI pipeline automates the process and keeps you informed of any new vulnerabilities.
Example of Secure Headers Configuration:
# Gemfile:
gem 'secure_headers'
# config/initializers/secure_headers.rb:
SecureHeaders::Configuration.default do |config|
config.hsts = "max-age=31536000; includeSubdomains"
config.x_frame_options = "SAMEORIGIN"
config.x_content_type_options = "nosniff"
config.x_xss_protection = "1; mode=block"
config.csp = {
default_src: %w('self'),
script_src: %w('self' https: 'unsafe-inline'),
style_src: %w('self' https: 'unsafe-inline'),
img_src: %w('self' data:),
object_src: %w('none')
}
end
Explanation:
- Regular security scans and dependency audits help you stay ahead of potential vulnerabilities.
- Automated tools integrated into your CI/CD pipeline can provide continuous feedback on the security posture of your application.
Additional Gems, Libraries, and Tools
Enhance your Rails security posture with these recommended tools:
Brakeman:
A static analysis tool designed specifically for Rails, Brakeman scans your codebase for vulnerabilities without executing your application.
Official Documentationbundler-audit:
This gem checks your Gemfile.lock for known vulnerabilities in your dependencies, helping you maintain an up-to-date and secure set of gems.
Official Documentationsecure_headers:
A gem that simplifies the management of HTTP security headers, reducing risks from clickjacking, MIME-sniffing, and XSS attacks.
GitHub RepositoryPundit:
A lightweight authorization library that enforces access control on a per-model basis, ensuring that only permitted users can perform specific actions.
Official DocumentationActiveStorage:
Rails’ built-in solution for file uploads, designed with security and scalability in mind.
Rails Guides on ActiveStorage
Security Best Practices Checklist
Before deploying or auditing your Rails application, ensure you have implemented the following:
-
SQL Injection:
- Use parameterized queries and ActiveRecord methods.
- Avoid raw SQL concatenation.
-
XSS Mitigation:
- Rely on Rails’ auto-escaping features.
- Use sanitization helpers (e.g.,
sanitize
,h
) where necessary.
-
Secrets & Credentials Management:
- Store sensitive data in Rails encrypted credentials or environment variables.
- Never hard-code secrets in your source code.
-
Session Management:
- Configure secure session stores (consider ActiveRecord store).
- Enforce secure, httponly, and same_site cookie flags.
-
CSRF Protection:
- Enable
protect_from_forgery
in ApplicationController. - Ensure authenticity tokens are present in forms.
- Enable
-
Secure File Uploads:
- Use ActiveStorage or Shrine for file handling.
- Validate file types, sizes, and scan for potential threats.
-
Authentication & Authorization:
- Implement Devise for authentication.
- Use Pundit or Cancancan for resource authorization.
-
Continuous Monitoring & Auditing:
- Integrate Brakeman and bundler-audit into your CI/CD pipeline.
- Regularly review and update security headers using secure_headers.
Conclusion
Securing your Ruby on Rails application is a continuous, multi-layered process that requires diligence, the right tools, and a proactive mindset. By implementing these best practices—from defending against SQL injection and XSS to managing sessions securely and safeguarding your secrets—you not only protect your users’ data but also enhance your application’s reliability and trustworthiness.
Remember, security isn’t a one-time checkbox; it’s an ongoing commitment. Stay informed of the latest security developments, regularly audit your codebase, and continuously integrate modern security practices into your development workflow. Your future self—and your users—will thank you.
Top comments (2)
There are a quite a few things you are missing and there are some incorrect statements in this article.
@gregmolnar Sure, if you could highlight those parts, i can rework on that and improve blog and my knowledge.