Ruby On Rails Security Best Practices (2026 Guide)
Learn Ruby on Rails security best practices to protect your apps from common vulnerabilities and secure production deployments.
• 21 min read
• 21 min read
Learn Ruby on Rails security best practices to protect your apps from common vulnerabilities and secure production deployments.
• 21 min read
• 21 min read
Security is one of those things developers love to postpone. It feels like a task for "later," something you will handle once the features are done and the deadline pressure eases up. The problem is that attackers do not wait for your roadmap. Every public-facing Rails application is a potential target the moment it goes live, regardless of how small or niche it is.
The good news is that Ruby on Rails was built with security in mind from the start. Many of the protections you need are already active by default: CSRF tokens, parameterized queries through Active Record, automatic HTML escaping in views, and encrypted credentials management.
But secure defaults are not the same as a secure application. Developers still make decisions every day that can quietly undo those protections: disabling CSRF checks on an API endpoint, using html_safe on user input, skipping authorization checks, or hardcoding an API key because it was faster in the moment. Security is not something Rails does for you. It is something Rails helps you do correctly, as long as you understand how the pieces fit together.
This article walks through the practical, production-ready techniques you need to secure a Rails 8 application. We will cover SQL injection, XSS, CSRF, authentication, authorization, secure sessions, credentials management, Content Security Policy, file uploads, API security, security headers, deployment hardening, and testing tools like Brakeman. If you are still deciding whether Rails is the right framework for your next project, its mature approach to security is one of many reasons Ruby on Rails is still worth learning in 2026.
Rails earned its security reputation the hard way, through years of real-world usage across thousands of production applications. A few design decisions explain why.
Secure defaults. New Rails applications ship with CSRF protection enabled, SQL injection safeguards baked into Active Record, and automatic output escaping in ERB templates. You have to deliberately opt out of these protections, which creates helpful friction before something unsafe happens.
Convention over Configuration. Because Rails encourages a standard way of structuring controllers, models, and routes, security reviews become more predictable, and it is easier for new team members to spot when something deviates from the norm.
Built-in protections. Rails includes strong parameter filtering, encrypted credentials, secure cookie handling, and sanitization helpers as part of the framework itself, not as optional third-party add-ons.
Active community and frequent security updates. The Rails core team responds quickly to disclosed vulnerabilities, and the community around gems like Devise, Pundit, and Brakeman actively maintains them. Compare this to how different frameworks approach security defaults in our Ruby on Rails vs Laravel comparison, where secure-by-default philosophy is one of the bigger differentiators.
None of this means you can be careless. It means you are building on a solid foundation instead of starting from zero.
An outdated Rails application is one of the most common ways teams get breached, not because of some exotic zero-day exploit, but because a known vulnerability sat unpatched for months.
Check your current version and compare it against the latest stable release:
bin/rails --version
gem list rails
When you upgrade, do it incrementally rather than jumping several major versions at once. Read the release notes for each version bump, since Rails often includes security-relevant changes in patch releases, not just major ones.
Keep Bundler itself updated:
gem update bundler
bundle update --bundler
Ruby releases regular security patches too. Running an old Ruby patch version (like an outdated 3.2.x) alongside the latest Rails defeats the purpose of staying current. Check your .ruby-version file and confirm it points to a maintained release.
Subscribe to the official Rails security announcements list and watch the rubysec/ruby-advisory-db project. A five-minute weekly check can save you from shipping a known vulnerability to production.
The bundler-audit gem checks your Gemfile.lock against a database of known vulnerabilities:
gem install bundler-audit
bundle audit check --update
Add this as a step in your CI pipeline so it runs automatically on every push, not just when someone remembers to run it manually.
Every gem you add to your Gemfile is code you are trusting to run inside your application. Before adding a dependency, check how actively it is maintained and whether it is still receiving updates. A gem that has not been touched in three years is a liability, even if it works fine today.
SQL injection remains one of the most damaging vulnerabilities on the OWASP Top 10, and it is still shockingly common in applications that build queries by concatenating strings.
SQL injection happens when untrusted input gets inserted directly into a SQL query without proper escaping. An attacker can manipulate that input to change the meaning of the query, potentially reading, modifying, or deleting data they should never have access to.
# Dangerous: string interpolation directly in a query
User.where("email = '#{params[:email]}'")
If someone passes ' OR '1'='1 as the email parameter, the resulting query returns every user in the table instead of the one you intended to look up.
Active Record's query interface handles escaping automatically when you use its standard methods:
# Safe: Active Record escapes the value for you
User.where(email: params[:email])
When you need more complex conditions, use parameter binding with placeholders instead of interpolation:
# Safe: parameterized query
User.where("email = ? AND active = ?", params[:email], true)
Sometimes you need raw SQL for performance or complex reporting queries. When you do, always use sanitize_sql_array or parameter binding, never raw interpolation:
# Safe raw SQL with sanitization
ActiveRecord::Base.connection.execute(
ActiveRecord::Base.sanitize_sql_array(["SELECT * FROM users WHERE email = ?", params[:email]])
)
Understanding how Active Record builds these queries under the hood also helps you write safer code by default. Our guide on how Active Record associations work in Ruby on Rails is a good companion read if you want a deeper look at the query layer.
XSS lets an attacker inject malicious JavaScript into pages viewed by other users, often to steal session cookies or perform actions on their behalf.
Rails escapes HTML output in ERB templates by default. If you write <%= @comment.body %>, any HTML tags in @comment.body get converted to their escaped entities instead of being rendered as live HTML. This alone prevents the vast majority of XSS attacks.
Sometimes you need to allow a limited subset of HTML, like when rendering rich text from a blog post editor. Use the sanitize helper to strip dangerous tags and attributes while keeping safe formatting:
<%= sanitize @post.body, tags: %w(p strong em a), attributes: %w(href) %>
html_safe tells Rails to trust a string completely and skip escaping. This is one of the most common ways developers accidentally introduce XSS vulnerabilities.
# Dangerous: marks unescaped user input as safe
<%= params[:message].html_safe %>
Never call html_safe on anything derived from user input. If you need to render formatted content, use sanitize instead so dangerous tags get stripped rather than blindly trusted.
# Safe: sanitize strips scripts and dangerous attributes
<%= sanitize(user_generated_content, tags: %w(p br strong em)) %>
CSRF tricks a logged-in user's browser into submitting a request to your application without their knowledge, usually by embedding a malicious form or link on another site. Since the browser automatically includes the user's session cookie, the request looks legitimate to your server.
Rails protects against this automatically with protect_from_forgery, which is enabled by default in ApplicationController:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
Every form Rails generates includes a hidden authenticity token that must match the one stored in the user's session. If the token is missing or does not match, Rails rejects the request. This is why form_with and form_for automatically embed this token without you needing to think about it.
For JSON APIs that use token-based authentication instead of cookies, CSRF protection typically is not needed in the same way, since there is no session cookie for an attacker to exploit. Many teams set:
class Api::BaseController < ActionController::API
# Token-based APIs generally do not need CSRF protection
skip_before_action :verify_authenticity_token, raise: false
end
Be careful here. Only skip CSRF protection on endpoints that genuinely use token-based auth without session cookies. If your API shares a session with a browser-based part of your app, you still need CSRF protection on those routes.
Before strong parameters existed, Rails apps were vulnerable to mass assignment attacks, where an attacker could set attributes on a model that were never meant to be user-editable, like an admin flag or a role field.
Strong parameters require you to explicitly whitelist which attributes are allowed to be assigned from user input:
def user_params
params.require(:user).permit(:name, :email, :password)
end
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
redirect_to @user, notice: "Account created."
else
render :new, status: :unprocessable_entity
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation)
end
end
The most common mistake is permitting more attributes than necessary just to make a form work, especially sensitive fields like admin, role, or account_id. If a form field maps to something a regular user should never be able to set, do not include it in permit, even if that means handling it separately in the controller with explicit logic.
Devise remains the most widely used authentication solution in the Rails ecosystem, and for good reason. It handles password hashing, session management, password resets, account confirmation, and lockable accounts out of the box.
bundle add devise
rails generate devise:install
rails generate devise User
If you want something lighter than Devise, Rails includes has_secure_password, built on bcrypt, for simpler authentication needs:
class User < ApplicationRecord
has_secure_password
validates :email, presence: true, uniqueness: true
end
Enforce reasonable password requirements at the model level:
validates :password, length: { minimum: 12 }, if: -> { password.present? }
Length matters more than complexity requirements. A long passphrase is generally more secure and more usable than a short password stuffed with symbols.
Never store passwords in plain text or with weak hashing like MD5 or SHA1. Both Devise and has_secure_password use bcrypt, which is specifically designed to be slow and resistant to brute-force attacks.
For applications handling sensitive data, add MFA using a gem like devise-two-factor. This adds a second verification step, typically a time-based one-time code, on top of the password.
Make sure reset tokens expire quickly (Devise defaults to 6 hours) and are single-use. Never include the actual password in reset emails, and always send reset confirmations so users notice if someone else triggered a reset on their account.
Authentication confirms who a user is. Authorization confirms what they are allowed to do. It is entirely possible to have solid authentication and still expose sensitive actions because you forgot to check permissions before executing them.
Pundit uses plain Ruby policy objects to define authorization rules, which keeps logic organized and easy to test:
class PostPolicy < ApplicationPolicy
def update?
user.admin? || record.user_id == user.id
end
end
class PostsController < ApplicationController
def update
@post = Post.find(params[:id])
authorize @post
@post.update(post_params)
end
end
CanCanCan takes a slightly different approach, defining abilities in a single Ability class:
class Ability
include CanCan::Ability
def initialize(user)
can :read, Post
can :manage, Post, user_id: user.id if user
can :manage, :all if user&.admin?
end
end
Whichever library you choose, avoid scattering if current_user.admin? checks throughout your views and controllers. Centralizing authorization logic in policy classes makes it far easier to audit who can do what, and far harder to accidentally leave a gap.
Rails encrypts and signs cookies by default, but you still need to configure them correctly for production.
Rails.application.config.session_store :cookie_store,
key: "_myapp_session",
secure: Rails.env.production?,
httponly: true,
same_site: :lax
The httponly flag prevents JavaScript from reading the session cookie, which blocks a major avenue for cookie theft through XSS.
Setting same_site: :lax or :strict restricts when cookies get sent along with cross-site requests, adding another layer of CSRF defense.
The secure: true setting ensures cookies are only sent over HTTPS connections, never plain HTTP, which matters a lot once you deploy to production.
Set a reasonable session timeout, especially for applications handling sensitive data:
Rails.application.config.session_store :cookie_store,
expire_after: 2.hours
Rails signs cookies with a secret key, which means any tampered value fails verification and gets rejected automatically. Just make sure your secret_key_base is properly generated and never exposed.
Rails 8 ships with a built-in encrypted credentials system, which is the recommended way to store API keys, database passwords, and other secrets:
bin/rails credentials:edit
This opens an encrypted file where you can store secrets in YAML format, decrypted only with the master.key file that should never be committed to version control.
Rails.application.credentials.stripe[:secret_key]
For deployment platforms that rely on environment variables, use the dotenv-rails gem locally and set variables directly on your production host or through your deployment tool.
ENV.fetch("STRIPE_SECRET_KEY")
Using fetch instead of direct access with [] ensures your app raises a clear error immediately if a required variable is missing, instead of failing silently somewhere downstream.
Rotate API keys periodically, especially after a team member leaves or if you suspect a key may have leaked. Store keys per environment so a leaked development key does not compromise production.
Add config/master.key and any .env files to your .gitignore immediately when starting a project. If a secret does get committed accidentally, rotating it is not optional. Removing it from git history alone is not enough, since it may already exist in forks, caches, or old clones.
Content Security Policy is a browser-enforced security layer that restricts which sources of scripts, styles, images, and other resources your pages are allowed to load. It acts as a safety net even if an XSS vulnerability slips through your sanitization.
Without CSP, a successful XSS injection can load and execute arbitrary scripts from anywhere. With a properly configured CSP, the browser blocks scripts from unapproved sources, even if they somehow got injected into the page.
Rails includes a CSP configuration DSL out of the box:
# config/initializers/content_security_policy.rb
Rails.application.configure do
config.content_security_policy do |policy|
policy.default_src :self, :https
policy.script_src :self, :https
policy.style_src :self, :https, :unsafe_inline
policy.img_src :self, :https, :data
policy.font_src :self, :https
policy.connect_src :self, :https
end
end
If you're managing a frontend built with Turbo and Stimulus versus a heavier JavaScript framework, your CSP configuration will look noticeably different. This is one of the practical tradeoffs we cover in Hotwire vs React: which should you pick in 2026, since a simpler frontend stack generally means a simpler, tighter CSP.
Start with policy.script_src :self and add exceptions only as needed for third-party services like analytics or payment providers. Avoid unsafe_inline for scripts wherever possible, since it significantly weakens the protection CSP is meant to provide.
Rails' built-in Active Storage handles file uploads well, but you still need to configure validation rules yourself.
class Document < ApplicationRecord
has_one_attached :file
validate :acceptable_file
private
def acceptable_file
return unless file.attached?
acceptable_types = ["application/pdf", "image/png", "image/jpeg"]
unless acceptable_types.include?(file.content_type)
errors.add(:file, "must be a PDF, PNG, or JPEG")
end
end
end
def acceptable_file
return unless file.attached?
if file.byte_size > 10.megabytes
errors.add(:file, "is too large, must be under 10MB")
end
end
For applications accepting uploads from untrusted users, consider integrating a virus scanning service or gem like clamav-client before making uploaded files accessible to other users.
Never trust the file's original filename directly, and avoid storing uploads in a publicly accessible directory that bypasses your application's authorization checks. Active Storage's default behavior of serving files through signed, expiring URLs handles most of this for you, as long as you do not override it with public bucket access.
For API-only Rails applications, token-based authentication is standard. A common pattern uses has_secure_token combined with a bearer token check:
class ApiController < ActionController::API
before_action :authenticate_request
private
def authenticate_request
token = request.headers["Authorization"]&.split(" ")&.last
@current_user = User.find_by(api_token: token)
render json: { error: "Unauthorized" }, status: :unauthorized unless @current_user
end
end
JSON Web Tokens work well for stateless authentication, but they come with tradeoffs. Once issued, a JWT cannot be easily revoked before expiration unless you maintain a blocklist, so keep expiration times short and consider refresh tokens for longer sessions.
Use rack-attack to throttle requests and protect against brute-force and abuse:
Rack::Attack.throttle("requests by ip", limit: 100, period: 1.minute) do |req|
req.ip
end
If your API serves requests from a separate frontend domain, configure CORS explicitly rather than allowing all origins:
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins "https://app.example.com"
resource "*", headers: :any, methods: [:get, :post, :put, :patch, :delete]
end
end
Validate every parameter your API accepts, not just the ones you expect to be problematic. Attackers frequently probe endpoints with unexpected types, oversized payloads, or malformed JSON looking for a crash or an information leak.
Security headers instruct the browser on how to handle your content safely. Rails 8 makes several of these easy to configure directly.
nosniff to stop browsers from guessing content types in ways that can lead to XSS.Referer header when users click links away from your site.http:// manually.# config/application.rb
config.action_dispatch.default_headers = {
"X-Frame-Options" => "SAMEORIGIN",
"X-Content-Type-Options" => "nosniff",
"Referrer-Policy" => "strict-origin-when-cross-origin",
"Permissions-Policy" => "geolocation=(), microphone=(), camera=()"
}
For HSTS, Rails provides a dedicated configuration option:
config.force_ssl = true
config.ssl_options = { hsts: { subdomains: true, expires: 1.year } }
config.force_ssl = true redirects all HTTP traffic to HTTPS and enables secure cookies automatically. There is rarely a good reason to skip this in a production Rails 8 application.
If you deploy behind Kamal or a reverse proxy like Nginx or Traefik, make sure TLS termination is configured correctly and that your proxy forwards the X-Forwarded-Proto header so Rails knows the original request was HTTPS. Keep TLS versions modern (1.2 and above), disable weak ciphers, and set reasonable timeout values to reduce exposure to slow-loris style attacks.
Restrict database access to only the application servers that need it, use strong unique credentials per environment, and never expose your database port publicly. If you are running SQLite in production, which has become increasingly viable with Rails 8, review our guide on optimizing SQLite for Rails 8 production for configuration details that also affect security, like file permissions and backup handling.
Automate backups, encrypt them at rest, and periodically test that you can actually restore from one. An untested backup is not a real backup.
Avoid logging sensitive data like passwords, tokens, or full credit card numbers. Rails filters common parameter names by default, but double-check your config.filter_parameters list includes everything sensitive to your application:
Rails.application.config.filter_parameters += [:password, :ssn, :credit_card, :api_key]
Set up monitoring and alerting so unusual activity, like repeated failed logins or spikes in 500 errors, gets flagged quickly instead of discovered days later.
Brakeman is a static analysis security scanner built specifically for Rails applications. It scans your codebase without executing it, looking for common vulnerability patterns like SQL injection, XSS, mass assignment issues, and unsafe redirects.
gem install brakeman
brakeman -A
Run it regularly, and take its warnings seriously even when they seem like false positives. It is far better to spend five minutes confirming a warning is safe than to miss a real issue.
As mentioned earlier, bundle audit check --update checks your dependencies against known vulnerability databases and should run in CI on every build.
Familiarize yourself with the OWASP Top 10, the industry-standard list of the most critical web application security risks. Most of what this article covers, injection, broken authentication, security misconfiguration, maps directly onto this list.
For applications handling sensitive data, consider periodic professional penetration testing. Automated tools like Brakeman catch a lot, but they cannot replace a skilled tester actively probing your application for logic flaws.
Add both Brakeman and bundle audit to your continuous integration pipeline so every pull request gets scanned automatically:
- name: Security scan
run: |
bundle exec brakeman -A --no-pager
bundle exec bundle-audit check --update
Use this before deploying any Rails application to production.
bundle audit and brakeman -A run clean or all warnings are reviewedconfig.force_ssl = true is set in productionsecure, httponly, and appropriate same_site settingsmaster.key and .env files are excluded from version controlIs Ruby on Rails secure? Rails is a secure framework by default, with protections like CSRF tokens, output escaping, and parameterized queries built in. But no framework can make an application secure on its own. Developers still need to follow best practices around authentication, authorization, and secrets management.
Does Rails prevent SQL injection? Active Record's query methods protect against SQL injection automatically when used correctly. Raw SQL with string interpolation bypasses this protection, so always use parameter binding or Active Record's built-in query methods instead.
Is Devise enough for authentication? Devise handles the fundamentals well: password hashing, session management, and account recovery. For applications with sensitive data, pair it with multi-factor authentication using a gem like devise-two-factor.
Should I use Pundit or CanCanCan? Both are solid choices. Pundit favors small, explicit policy classes per model, which many teams find easier to test and reason about. CanCanCan centralizes rules in a single Ability class, which some teams prefer for simpler applications. Either works well when used consistently.
How do I store API keys securely? Use Rails encrypted credentials or environment variables, never hardcode them in your codebase. Rotate keys periodically and use different keys per environment.
How often should I update Rails? Apply patch and security releases as soon as they are available. Plan for major version upgrades at least once a year to avoid falling too far behind, which makes eventual upgrades much harder.
What is Brakeman? Brakeman is a static analysis tool that scans Rails codebases for common security vulnerabilities without executing the application. It is free, open source, and should be part of every Rails team's CI pipeline.
What are the biggest Rails security risks? In practice, most real-world incidents come from outdated dependencies, missing authorization checks, and mishandled secrets, not exotic framework-level exploits. The fundamentals matter more than most developers expect.
Rails gives you a strong security foundation: CSRF protection, safe query building, automatic output escaping, and encrypted credentials are all there from the moment you run rails new. But security is not something you inherit passively. It is a habit you build into how you write code every day, from how you handle params to how you think about who should be allowed to do what.
Start early. Bake security checks into your development workflow with tools like Brakeman and bundle audit, review authorization logic as carefully as you review functionality, and treat every piece of user input as untrusted until proven otherwise. None of this needs to slow you down once it becomes routine.
Security is not a checkbox you complete before launch. It is an ongoing process that continues for as long as your application is running in production. Build that mindset in from day one, and the rest becomes much easier to maintain.