Sitemap
Bertrand Bichat

Bertrand Bichat

Ruby on Rails developer — France — https://linktr.ee/bertrand.bichat

How to optimize the security of a website created with Ruby on Rails ? 🔒

8 min readJan 6, 2026

--

Press enter or click to view image in full size
Security is not just in code, but also in your team training
Security is not just in code, but also in your team training.

Securing a Ruby on Rails application is important to protect your users’ data, your business reputation, and your team’s peace of mind.

In this article, we’ll walk through the main areas you should review to secure a Rails application in production.

Start with Strong Authentication

Use Devise as a Solid Foundation

For most Rails applications, Devise is the standard for user authentication. It handles registration, login, password resets, account locking, and more, with secure defaults.

Key points when configuring Devise:

  • Enforce strong passwords (length and complexity).
  • Enable account lock strategies after several failed login attempts.

Add Two-Factor Authentication (2FA)

Passwords alone are not enough. To significantly reduce the impact of stolen credentials, add two‑factor authentication:

  • Use the gem devise-two-factor to integrate time-based one-time passwords (TOTP), usually through an authenticator app.
  • Offer backup codes so users can recover access if they lose their device.
  • Make 2FA mandatory for privileged accounts (admins, superusers).

For APIs: Use JWT with Devise

If you expose an API, consider token-based auth with JWT rather than cookie-based sessions:

  • Use the gem devise-jwt to integrate JWT with Devise
  • Set short token lifetimes and implement token revocation (blacklist / denylist).
  • Always transmit tokens over HTTPS and never store them in insecure places (e.g. localStorage combined with vulnerable frontends).

Authorize Every Action with Clear Rules

Authentication answers “who are you ?”; authorization answers “what are you allowed to do ?”.

Use Pundit or CanCanCan

Two popular options:

  • Pundit: policy objects per model, with plain Ruby methods.
  • CanCanCan: central Ability class describing what each role can do.

Good practices:

  • Define explicit rules for every resource.
  • Avoid authorization logic scattered across controllers and views.
  • Add tests to ensure that users with different roles cannot access unauthorized actions.

Protect Personal and Sensitive Data

Only Store What You Really Need

The most effective way to reduce risk is to limit what you store:

  • Do you really need to store dates of birth, addresses, or phone numbers?
  • Can you anonymize or pseudonymize some data?
  • Can you delete data after a certain retention period?

Less data means a smaller impact if you ever have a breach, and it helps with GDPR and other privacy regulations.

Encrypt Sensitive Fields

For data that must remain confidential (access tokens, secrets, sensitive data) :

  • you can use ActiveSupport::MessageEncryptor to encrypt and decrypt values.
  • Store encryption keys in Rails credentials or environment variables, never in source code.
  • Rotate keys periodically (with a re-encryption strategy when needed).

Secure Sessions and Cookies

Prefer Server-Side Session Storage

By default, many Rails apps store session data in cookies. A safer approach for more sensitive apps is to:

  • Use ActiveRecord or Redis-based session stores to keep the session on the server.
  • Store only a session identifier in the cookie.

If you do use cookie-based sessions:

  • Ensure cookies are encrypted and signed (Rails does this for session cookies by default).
  • Set secure: true (HTTPS only) and httponly: true.
  • Use SameSite attributes to reduce CSRF risks where appropriate.

Enforcing HTTPS in Production

Your application should never send sensitive data (passwords, session cookies, personal information) over plain HTTP. Rails lets you enforce HTTPS globally in production:

# config/environments/production.rb
config.force_ssl = true

Once this is enabled, Rails will redirect all HTTP requests to HTTPS and mark cookies as secure, meaning they will only be sent over encrypted connections. This is a simple configuration change that drastically improves the security of your app.

Lock Down Your APIs: JWT and CORS

Authenticate Every API Request

For APIs, every request should be authenticated:

  • Use JWT tokens stored server-side.
  • Avoid long-lived tokens; refresh or rotate them regularly.
  • Implement proper logout (token invalidation) where needed.

Configure CORS with rack-cors

To avoid unwanted cross‑origin access:

  • Use rack-cors to explicitly define which domains can call your API.
  • Restrict allowed methods (GET, POST, etc.) and headers to the minimum necessary.
  • Never use ‘*’ (any origin) in production unless you really know what you’re doing.

Block Abusive Traffic with Rate Limiting

Even a secure endpoint can be abused by brute force or denial-of-service attempts.

The Rack::Attack gem lets you define rules to:

  • Throttle authentication attempts by IP or email.
  • Limit overall requests per IP.
  • Block known malicious IPs or patterns.

Keep logs of these blocked attempts and monitor them; they are often early signs of abuse or scanning.

Harden HTTP with Security Headers

Browsers can help protect your users — if you set the right headers.

Get Bertrand Bichat’s stories in your inbox

Join Medium for free to get updates from this writer.

Use secure_headers gem to configure:

  • Content-Security-Policy (CSP): restrict where scripts, styles, images, and frames can be loaded from.
  • X-Frame-Options / frame-ancestors: prevent clickjacking.
  • X-Content-Type-Options, Referrer-Policy, X-XSS-Protection (or equivalent modern policies).

Start with a CSP in report-only mode, analyze violations, then enforce a stricter policy once you’re confident.

Enabling CSRF Protection

Rails includes built-in protection against Cross-Site Request Forgery (CSRF) attacks. To benefit from it, make sure your ApplicationController includes :

class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end

This ensures that any state-changing request (POST, PATCH, DELETE, etc.) must include a valid CSRF token. Requests that don’t have this token (or present an invalid one) will be rejected, which prevents attackers from tricking users’ browsers into performing unwanted actions on your site.

Don’t Forget Meta Tags in the layout

If your frontend makes AJAX requests (for example with fetch, Axios, Rails UJS or Turbo), make sure the head of your layout includes the CSRF meta tag. The CSP meta tag is also needed.

<%= csrf_meta_tags %>
<%= csp_meta_tag %>

Rails’ JavaScript helpers and many HTTP clients can automatically read this CSRF token and attach it to outgoing requests. Combined with protect_from_forgery, this gives you strong protection against CSRF attacks even in a modern, JavaScript-heavy interface.

Build Secure Forms

Rails already helps protect you against many common web vulnerabilities — use those tools instead of reinventing the wheel.

Always Use Rails Form Helpers

To prevent cross-site request forgery (CSRF):

  • Use form_with, form_for, or form_tag so Rails automatically includes an authenticity token.
  • If you prefer a nicer API, use simple_form, which still relies on Rails’ secure foundations.
  • Don’t create raw HTML forms that bypass Rails’ CSRF protection.

Secure File Uploads

Handling user uploads is a classic attack vector. So, choose a mature library:

Then:

  • Restrict allowed file types (MIME type and extension).
  • Set maximum file size limits.
  • Store files on external storage (S3, GCS, etc.) with the right ACLs.
  • Never execute or render uploads directly as code.

Manage Secrets and API Keys Properly

Hardcoding secrets in code or committing them to Git is one of the most common (and most dangerous) mistakes.

Use Rails Credentials or Environment Variables

  • Store API keys, database passwords, and encryption keys in Rails credentials or environment variables.
  • Limit who can access these values (developers, CI/CD, production servers).
  • Rotate secrets periodically and revoke them immediately if you suspect exposure.

Audit your Git history to ensure no secrets have ever been committed, and use tools that can detect accidental leaks.

Keep Dependencies and Stack Up to Date

A large part of your attack surface is in your dependencies.

  • Regularly update Rails, Ruby, and gems to incorporate security patches.
  • Use tools like Brakeman for static analysis of your Rails code.
  • Combine this with dependency scanners and CI checks that block vulnerable versions.

Don’t ignore security advisories; prioritize them in your backlog.

Track Changes and Improve Observability

You can’t secure what you don’t see.

Use paper_trail to:

  • Keep a history of changes to important models.
  • Restore previous versions if there is accidental (or malicious) modification.
  • Provide an audit trail for compliance and investigations.

Logging and Monitoring

  • Log logins, password changes, admin actions, and suspicious requests.
  • Centralize logs and use alerts for unusual patterns (spikes in 401/403, rate-limit hits, etc.).

Performance, Pagination, and Exposure

From a security perspective, performance matters too. An endpoint that returns “everything” can leak too much data and be easier to abuse by DDoS attack.

  • Use pagination with Kaminari, Will Paginate, or Pagy for large lists to reduce load time.
  • Avoid endpoints that return massive datasets or sensitive information without filters.
  • Combine pagination with proper authorization checks on every record.

Preventing SQL Injection with Safe Queries

A classic mistake is to interpolate user input directly into SQL strings:

# ❌ Vulnerable to SQL injection
User.where("name = '#{params[:name]}'")

If an attacker injects malicious SQL into params[:name], they can potentially read or modify data they should never see. Instead, always rely on parameterized queries:

# ✅ Safe, uses bound parameters
User.where(name: params[:name])

Here, Rails takes care of escaping the value properly and sends it to the database as a bound parameter. You should never manually concatenate raw parameters into SQL strings.

SQL Injection with LIKE queries

A very common anti-pattern in Rails is to build LIKE queries by interpolating user input directly into the SQL string.

# ❌ vulnerable to SQL injection
Topic.where("title LIKE '%#{params[:title]}%'")

Instead, always rely on parameter binding:

# ✅ Safe, uses sanitize_sql_like
sanitized_params = ApplicationRecord.sanitize_sql_like(params[:title])
Topic.where("title LIKE ?", "%#{sanitized_params}%")

The ? placeholder ensures the value is passed as a bound parameter, and sanitize_sql_like escapes wildcard characters so that user input cannot escape the intended pattern. With this pattern, you still get flexible search while keeping your queries safe from injection attacks.

Sanitizing User Input Before Rendering

Any time you store or display user-generated content (comments, bios, descriptions), you need to assume it may contain malicious HTML or JavaScript. Rails provides helpers to sanitize this content and strip dangerous tags:

allowed_tags = %w[b i p]
@safe_description = sanitize(params[:description], tags: allowed_tags)

In this example, only a small, controlled set of tags is allowed (<b>, <i>, <p>). Everything else is removed. This gives users some flexibility to format their content while protecting the rest of your page from XSS attacks.

Avoiding XSS by Escaping Output

Cross-Site Scripting (XSS) often happens when untrusted content is rendered directly into an HTML page without escaping. In ERB templates, avoid using raw (or .html_safe) on user data unless you are absolutely sure the content is safe :

# ❌ Dangerous: outputs raw user content
<%= raw user.bio %>
<%= user.bio.html_safe %>

Instead, let Rails escape the content by default:

# ✅ Safe: Rails escapes HTML and JS
<%= user.bio %>

# ✅ Safe: sanitize all HTLM tags by allowed only strong, em and a
<%= sanitize user.bio, tags: %w(strong em a), attributes: %w(href) %>

This ensures that any <script> tags or malicious HTML included in the bio will be rendered as text, not executed by the browser. You can also sanitize rich text fields coming from users.

Using Strong Parameters to Control Mass Assignment

Strong Parameters add another layer of protection by forcing you to explicitly whitelist which attributes can be mass-assigned:

# In the controller
def create
user = User.new(user_params)
...
end

private

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

Instead of trusting whatever comes from the client, you only permit the fields that make sense in that particular action. This prevents attackers from submitting unexpected attributes (like admin: true) and silently elevating their privileges.

Build a Security Culture (Most Important Point)

Even the best technical setup won’t save you from human error.

  • Train your team (developers, support, marketing, etc.) on basic cybersecurity and privacy principles (GDPR, phishing, password hygiene, data sharing).
  • Document your security practices and incident response process.
  • Encourage people to report suspicious behavior without fear.

Security is not a one‑time project; it’s an ongoing habit.

For deeper dives on specific topics, refer to the official Rails Security Guide.

--

--