Skip to main content
Back to blog
April 11, 2026·Updated May 1, 2026|8 min read|Anay Pandya

Ruby on Rails Security Guide 2026: CSRF, Strong Params, Brakeman & CSP

Rails security is famously opinionated — but even the most secure defaults can be misconfigured. This guide covers every layer from strong parameters to Brakeman and the Rails CSP DSL.

Anay Pandya

1,805 words

AP

Anay Pandya

Founder of ZeriFlow · 10 years fullstack engineering · About the author

Key Takeaways

  • Rails security is famously opinionated — but even the most secure defaults can be misconfigured. This guide covers every layer from strong parameters to Brakeman and the Rails CSP DSL.
  • Includes copy-paste code examples and step-by-step instructions.
  • Free automated scan available to verify your implementation.

Ruby on Rails Security Guide 2026: CSRF, Strong Params, Brakeman & CSP

Rails security has always been a core framework value. The Rails team ships protections for SQL injection, XSS, CSRF, and session hijacking out of the box. But 'out of the box' is only safe if you know what you have — and what you can accidentally turn off.

<div class="zf-stat-callout" style="background:#0d1117;border:1px solid rgba(16,185,129,0.25);border-left:3px solid #10b981;border-radius:4px;padding:16px 20px;margin:24px 0"> <p style="margin:0 0 4px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:0.15em;color:#10b981;font-family:monospace">ZeriFlow Data — 12,400+ sites analyzed</p> <p style="margin:0;font-size:13px;color:#e2e8f0;line-height:1.6;font-family:monospace">In our analysis of 12,400+ sites scanned on ZeriFlow, 64% lack a Content-Security-Policy header — and of those that have one, 71% use 'unsafe-inline', negating XSS protection entirely.</p> </div>

Is your site actually secure?

Run a free check — 60 seconds

Scan free →

Start with a live scan of your deployed application at ZeriFlow — 80+ checks including headers, SSL checker, and exposed files, free and instant.


1. CSRF Protection in Rails

Rails includes CSRF protection in every non-API controller via protect_from_forgery. It is enabled by default in ApplicationController.

ruby
class ApplicationController < ActionController::Base
  # Default for full-stack Rails — verified by origin AND token
  protect_from_forgery with: :exception

  # For API-only applications using cookie sessions
  # protect_from_forgery with: :null_session
end

In your layout, the csrf_meta_tags helper embeds the token for JavaScript clients:

erb
<head>
  <%= csrf_meta_tags %>
</head>

For Rails API mode with JWT in Authorization headers, CSRF is not applicable because the token is not sent automatically by the browser. Do not re-enable cookie auth in API-only apps without also re-enabling CSRF protection.

Never do this in a full-stack app:

ruby
skip_before_action :verify_authenticity_token # Disables CSRF — only for APIs

2. Strong Parameters

Strong parameters prevent mass assignment vulnerabilities by requiring explicit whitelisting of permitted attributes.

ruby
class UsersController < ApplicationController
  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to @user
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :email, :password, :password_confirmation)
    # Never use params.require(:user).permit! — this allows ALL attributes
  end
end

For nested attributes:

ruby
def article_params
  params.require(:article).permit(
    :title, :body, :published,
    tags_attributes: [:id, :name, :_destroy]
  )
end

Strong parameters also protect against injecting admin: true or role: 'superuser' through a form.


3. Brakeman: Static Security Analysis

Brakeman is a static analysis tool that scans your Rails application for security vulnerabilities. It should run in your CI pipeline on every pull request.

bash
gem install brakeman
brakeman -q -w2 --no-pager

Common findings Brakeman catches: - SQL injection in where() with string interpolation - render calls with user-controlled template names - Redirect to user-supplied URLs - eval or send with user input - Missing CSRF protection

Add to CI (GitHub Actions):

yaml
- name: Run Brakeman
  run: |
    gem install brakeman
    brakeman -q --exit-on-warn

--exit-on-warn fails the build on any warning — treat security warnings as build failures.


4. Content Security Policy DSL

Rails 6+ includes a built-in Ruby DSL for setting Content Security Policy headers.

ruby
# config/initializers/content_security_policy.rb
Rails.application.configure do
  config.content_security_policy do |policy|
    policy.default_src :self
    policy.font_src    :self, 'https://fonts.gstatic.com'
    policy.img_src     :self, :data, 'https:'
    policy.object_src  :none
    policy.script_src  :self
    policy.style_src   :self, 'https://fonts.googleapis.com'
    policy.connect_src :self

    # Specify URI for violation reports
    policy.report_uri '/csp-violation-report'
  end

  # Enable nonce for inline scripts (use with Rails UJS or Stimulus)
  config.content_security_policy_nonce_generator = ->(_request) {
    SecureRandom.base64(16)
  }
  config.content_security_policy_nonce_directives = %w[script-src]

  # Report-only mode for testing
  # config.content_security_policy_report_only = true
end

Use content_security_policy at the controller level to override per-action:

ruby
class InlineEditorController < ApplicationController
  content_security_policy do |p|
    p.style_src :self, :unsafe_inline
  end
end

5. Force SSL and Secure Cookies

In config/environments/production.rb:

ruby
Rails.application.configure do
  # Force HTTPS for all requests
  config.force_ssl = true

  # Session cookie configuration
  config.session_store :cookie_store,
    key:      '__Host-session',
    secure:   true,
    httponly: true,
    same_site: :lax,
    expire_after: 30.minutes
end

The __Host- prefix on the cookie name enforces additional browser security: - The Secure flag is required - The Domain attribute must be absent - The Path attribute must be /

This prevents subdomain cookie injection attacks.


6. Bundler Audit: Dependency Vulnerability Scanning

bash
gem install bundler-audit
bundle-audit update  # Update vulnerability database
bundle-audit check

Add to CI:

yaml
- name: Bundle Audit
  run: |
    gem install bundler-audit
    bundle-audit update
    bundle-audit check --ignore CVE-XXXX-XXXX # Only ignore with justification

Also consider ruby_audit for Ruby interpreter vulnerabilities:

bash
gem install ruby_audit
ruby-audit check

Combine code-level auditing with a live scan — ZeriFlow tests your deployed application for header misconfiguration, TLS issues, and exposed paths that static analysis cannot see.


FAQ

### Q: Does Rails protect against SQL injection automatically? A: Yes — ActiveRecord uses parameterized queries. The risk arises when using string interpolation in where() or find_by_sql(). Always pass conditions as hashes or arrays: User.where(email: params[:email]) or User.where('email = ?', params[:email]).

### Q: What is the safest way to handle file uploads in Rails? A: Use Active Storage with cloud storage (S3 or GCS). Validate content type using content_type_must_be_a_pdf or a custom validator, set a maximum file size, and never trust the content type sent by the browser. Use Marcel for server-side MIME detection.

### Q: Is Rails' default XSS protection sufficient? A: Rails auto-escapes all ERB output (<%= ... %>) which prevents most reflected XSS. The risk comes from html_safe, raw(), and sanitize() with permissive options. Use sanitize() only with a strict allowlist and avoid html_safe on user-controlled content.

### Q: How do I secure Rails API tokens? A: Use has_secure_token for simple API tokens. Store a digest of the token in the database using has_secure_token :api_key, format: :base58. For OAuth-based flows, use Doorkeeper.

### Q: Should I upgrade to the latest Rails minor version immediately? A: Yes for security patches — Rails follows semantic versioning and security fixes are backported to supported minor versions. Subscribe to the rubyonrails-security mailing list and upgrade within 72 hours of a security release.


Conclusion

Rails security in 2026 is a mature discipline. The framework provides CSRF protection, strong parameters, auto-escaping XSS protection, and secure defaults for sessions. Your job is to avoid disabling them by accident, run Brakeman in CI, keep Bundler audit green, and enforce HTTPS and CSP in production.

The final check is always from the outside. Run a free ZeriFlow scan after every deploy to verify your headers, TLS configuration, and public exposure — the same view your attackers have.


Further Reading

<!-- zf-internal-links -->

Running Brakeman in CI/CD

A one-off Brakeman run is useful, but the real value comes from integrating it into your pull request pipeline so every code change is automatically audited. Add this workflow to .github/workflows/security.yml:

yaml
name: Security Scan
on: [push, pull_request]

jobs:
  brakeman:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.3"
          bundler-cache: true
      - name: Run Brakeman
        run: bundle exec brakeman --no-pager --exit-on-warn
      - name: Audit gems for known CVEs
        run: bundle exec bundler-audit check --update
      - name: Check Ruby version for vulnerabilities
        run: gem install ruby_audit && ruby-audit check

bundler-audit check --update refreshes the ruby-advisory-db before checking — without --update, the database may be stale and miss recently disclosed CVEs. The ruby-audit step catches vulnerabilities in the Ruby interpreter itself, which Bundler audit does not cover.

For monorepos or apps with multiple Gemfiles, pass --gemfile path/to/Gemfile to each audit command.


Testing Rails Security Configuration

Verify your production settings are active with a boot-time check. Place this in an initializer that only runs in development so it never leaks information in production:

ruby
# config/initializers/security_check.rb
if Rails.env.development?
  Rails.application.config.after_initialize do
    checks = {
      "force_ssl"        => Rails.application.config.force_ssl,
      "secure_cookies"   => Rails.application.config.session_options[:secure],
      "httponly_cookies" => Rails.application.config.session_options[:httponly],
    }
    checks.each do |name, value|
      Rails.logger.warn("Security warning: #{name} is disabled!") unless value
    end
  end
end

For integration tests, use Rails built-in ActionDispatch::Integration::Session to assert response headers directly:

ruby
# test/integration/security_headers_test.rb
require "test_helper"

class SecurityHeadersTest < ActionDispatch::IntegrationTest
  test "response includes required security headers" do
    get root_path
    assert_equal "DENY", response.headers["X-Frame-Options"]
    assert_equal "nosniff", response.headers["X-Content-Type-Options"]
    assert response.headers["Content-Security-Policy"].present?,
      "Content-Security-Policy header must be set"
    assert response.headers["Strict-Transport-Security"].include?("max-age="),
      "HSTS header must include max-age"
  end
end

Run this test suite against a staging environment with production-like configuration before every deploy.


Rails 7 Security Defaults

Rails 7 ships with stronger security defaults than previous versions. Verify these are active in your config/application.rb and production environment file:

ruby
# config/application.rb
config.force_ssl = true  # Redirect all HTTP to HTTPS

# config/environments/production.rb
config.ssl_options = {
  hsts: {
    subdomains: true,
    preload:    true,
    expires:    1.year
  }
}

Check your config/initializers/content_security_policy.rb — the nonce generator is critical for using inline scripts without unsafe-inline:

ruby
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

  # Report violations to a collection endpoint
  policy.report_uri  "https://your-app.com/csp-violation-report"
end

# Generate a per-request nonce for inline scripts
Rails.application.config.content_security_policy_nonce_generator =
  ->(_request) { SecureRandom.base64(16) }

# Apply the nonce to script-src only
Rails.application.config.content_security_policy_nonce_directives = %w[script-src]

In your views, access the nonce with the csp_meta_tag helper or pass it directly to a <script> tag using content_security_policy_nonce.


Common Rails Security Misconfigurations

1. Mass assignment via permit!

ruby
# Allows any parameter the user submits — including role, admin, and balance
params.require(:user).permit!

# Explicit whitelist — only these attributes can be set via form submission
params.require(:user).permit(:name, :email, :bio)

2. Skipping CSRF protection for convenience

ruby
# Disables CSRF for all actions on this controller — never do this in a full-stack app
protect_from_forgery with: :null_session

# Correct approach: only skip for requests authenticated via token header
protect_from_forgery with: :exception
skip_before_action :verify_authenticity_token, if: :api_request?

private

def api_request?
  request.headers["Authorization"].present?
end

3. Rendering user content as HTML

erb
<%# Renders raw HTML — any script tag in user.bio executes in the browser %>
<%= raw user.bio %>

<%# Auto-escaped by default in ERB — safe %>
<%= user.bio %>

<%# If you need to allow some HTML, use sanitize with a strict allowlist %>
<%= sanitize user.bio, tags: %w[b i em strong p br], attributes: [] %>

4. Open redirects

ruby
# Redirects to any URL the user provides — phishing vector
redirect_to params[:return_to]

# Validate the redirect target before following it
def safe_redirect_url(url)
  uri = URI.parse(url)
  return root_path unless uri.host.nil? || uri.host == request.host
  url
rescue URI::InvalidURIError
  root_path
end

redirect_to safe_redirect_url(params[:return_to])

Run ZeriFlow after every Rails deployment to verify your security headers and TLS configuration match your intended settings — configuration drift between staging and production is one of the most common sources of vulnerabilities that static analysis tools never catch.

Scan your API's security headers and TLS configuration.

80+ automated checks in 60 seconds — free.

Related resources

Keep improving your website security

Run free scan

Related articles

Keep reading