Secure Your Rails App – Security Best Practices
1. Start with secure defaults and environment hygiene
Rails ships with sensible security defaults, but they only work if you keep them enabled and avoid leaking secrets. Treat configuration as code and environments as isolated blast zones.
Use credentials and environment variables correctly
Never hard‑code secrets (API keys, database passwords, tokens) in your codebase. Use
rails credentials:edit or environment variables managed by your deployment
platform.
# config/database.yml
production:
adapter: postgresql
url: <%= ENV.fetch("DATABASE_URL") %>
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
Keep Rails and dependencies up to date
Many real‑world breaches come from known vulnerabilities in old gems. Use tools like
bundle audit or GitHub Dependabot and schedule regular dependency reviews.
# Example: run regularly in CI
bundle exec bundle-audit check --update
2. Harden authentication and authorization
Authentication proves who a user is; authorization decides what they can do. Both must be explicit and consistent across your app.
Use proven authentication libraries
Instead of rolling your own login system, use mature gems like Devise or Sorcery. They handle password hashing, account locking, and session management with battle‑tested defaults.
# Example Devise configuration (config/initializers/devise.rb)
config.pepper = ENV["DEVISE_PEPPER"]
config.stretches = Rails.env.test? ? 1 : 12
config.timeout_in = 30.minutes
Enforce strong password and session policies
- Require minimum length and complexity for passwords.
- Use
has_secure_passwordor Devise’s bcrypt/argon2 support. - Enable session expiration and re‑authentication for sensitive actions.
Centralize authorization logic
Use Pundit or CanCanCan to keep authorization rules out of controllers and views. This reduces the risk of “forgot to check” vulnerabilities.
# Example Pundit policy
class ProjectPolicy < ApplicationPolicy
def update?
user.admin? || record.owner_id == user.id
end
end
3. Defend against injection and XSS
Any time user input touches a query, command, or HTML, you should assume it’s hostile. Rails gives you tools to neutralize that input—use them consistently.
Use parameterized queries and Active Record
Avoid string interpolation in SQL. Use Active Record or parameterized queries to prevent SQL injection.
# Safe
User.where(email: params[:email])
# Unsafe
User.where("email = '#{params[:email]}'")
Escape output by default
Rails ERB escapes output automatically. Only use raw or
.html_safe when you are absolutely sure the content is safe.
<!-- Safe -->
<%= @comment.body %>
<!-- Dangerous if @comment.body contains user input -->
<%= raw @comment.body %>
Validate and sanitize rich text
For user‑generated HTML (e.g., WYSIWYG editors), use sanitization:
# Example sanitization
sanitized = ActionController::Base.helpers.sanitize(
params[:content],
tags: %w[p b i strong em a ul ol li],
attributes: %w[href]
)
4. Protect against CSRF and session attacks
Cross‑Site Request Forgery (CSRF) abuses a user’s authenticated browser to perform unwanted actions. Rails has built‑in CSRF protection—don’t disable it casually.
Keep CSRF protection enabled
In ApplicationController, ensure CSRF protection is turned on:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
Rails automatically embeds authenticity tokens in forms and verifies them on submission.
If you expose JSON APIs, use protect_from_forgery with appropriate
strategies or separate API controllers using token‑based auth.
Harden cookies and sessions
- Set
secureandhttponlyflags on cookies. - Use
same_site: :laxor:strictwhere possible. - Rotate session secrets if compromised and invalidate old sessions.
# config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store,
key: "_myapp_session",
secure: Rails.env.production?,
httponly: true,
same_site: :lax
5. Enforce HTTPS and security headers
Transport security ensures data isn’t exposed in transit. Browser security headers add an extra layer of defense against common attacks.
Force HTTPS everywhere
Use config.force_ssl = true in production to redirect HTTP to HTTPS and set
secure cookies.
# config/environments/production.rb
config.force_ssl = true
Configure Content Security Policy (CSP)
CSP limits where scripts, styles, and other resources can be loaded from, significantly reducing XSS impact.
# config/initializers/content_security_policy.rb
Rails.application.configure do
config.content_security_policy do |policy|
policy.default_src :self
policy.script_src :self, :https
policy.style_src :self, :https, :unsafe_inline
policy.img_src :self, :https, :data
end
end
Set additional security headers
Use middleware or your reverse proxy (Nginx, Apache, CDN) to set headers like:
Strict-Transport-Security(HSTS)X-Frame-Options(clickjacking protection)X-Content-Type-Options: nosniff
6. Protect data at rest and handle file uploads safely
Sensitive data and user uploads are high‑value targets. Treat them as untrusted and compartmentalize aggressively.
Encrypt sensitive attributes
For fields like tokens, personal identifiers, or secrets, use Rails’ built‑in encryption
or gems like attr_encrypted (for older versions).
# Rails 7+ Active Record encryption
class User < ApplicationRecord
encrypts :ssn, :personal_token
end
Validate and scan file uploads
- Use Active Storage or Shrine with strict content‑type validation.
- Store uploads on S3 or similar, not on the app server’s public directory.
- Serve files via signed URLs and avoid executing uploaded content.
# Example validation
class Document < ApplicationRecord
has_one_attached :file
validate :acceptable_file
def acceptable_file
return unless file.attached?
unless file.byte_size <= 10.megabytes
errors.add :file, "is too big"
end
acceptable_types = ["application/pdf"]
unless acceptable_types.include?(file.content_type)
errors.add :file, "must be a PDF"
end
end
end
7. Log, monitor, and respond
Security is not a one‑time configuration; it’s an ongoing feedback loop. You need visibility into what your app is doing and how it’s being used.
Log security‑relevant events
- Log authentication failures and suspicious login patterns.
- Log permission denials and unexpected parameter usage.
- Redact sensitive fields (passwords, tokens, secrets) from logs.
# config/initializers/filter_parameter_logging.rb
Rails.application.config.filter_parameters += %i[
password password_confirmation token api_key
]
Set up alerts and rate limiting
Use tools like Rack::Attack to throttle abusive requests and integrate your logs with monitoring platforms (e.g., Datadog, Sentry, Logstash) for anomaly detection.
# Example Rack::Attack throttle
Rack::Attack.throttle("logins/ip", limit: 10, period: 60.seconds) do |req|
req.ip if req.path == "/users/sign_in" && req.post?
end
8. Make security part of your development workflow
The most secure Rails apps are built by teams that treat security as a habit, not a feature. Bake it into your process.
Integrate security into CI/CD
- Run static analysis and dependency checks on every merge.
- Fail builds on critical vulnerabilities.
- Automate basic checks (e.g., secret scanning, linting for unsafe patterns).
Review code with a security lens
During code review, explicitly ask: “What happens if this input is malicious?” and “What if this endpoint is called by someone who shouldn’t have access?”
Document and train
Maintain a short, living “Rails Security Checklist” for your team and revisit it regularly. New developers should learn your security expectations as part of onboarding.