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
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.
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
endIn your layout, the csrf_meta_tags helper embeds the token for JavaScript clients:
<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:
skip_before_action :verify_authenticity_token # Disables CSRF — only for APIs2. Strong Parameters
Strong parameters prevent mass assignment vulnerabilities by requiring explicit whitelisting of permitted attributes.
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
endFor nested attributes:
def article_params
params.require(:article).permit(
:title, :body, :published,
tags_attributes: [:id, :name, :_destroy]
)
endStrong 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.
gem install brakeman
brakeman -q -w2 --no-pagerCommon 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):
- 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.
# 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
endUse content_security_policy at the controller level to override per-action:
class InlineEditorController < ApplicationController
content_security_policy do |p|
p.style_src :self, :unsafe_inline
end
end5. Force SSL and Secure Cookies
In config/environments/production.rb:
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
endThe __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
gem install bundler-audit
bundle-audit update # Update vulnerability database
bundle-audit checkAdd to CI:
- name: Bundle Audit
run: |
gem install bundler-audit
bundle-audit update
bundle-audit check --ignore CVE-XXXX-XXXX # Only ignore with justificationAlso consider ruby_audit for Ruby interpreter vulnerabilities:
gem install ruby_audit
ruby-audit checkCombine 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:
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 checkbundler-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:
# 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
endFor integration tests, use Rails built-in ActionDispatch::Integration::Session to assert response headers directly:
# 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
endRun 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:
# 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:
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!
# 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
# 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?
end3. Rendering user content as HTML
<%# 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
# 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
Related tools
Website Vulnerability Scanner
Run a broader website security audit across headers, TLS, DNS, cookies, SEO, and disclosure checks.
Security Headers Checker
Check CSP, HSTS, X-Frame-Options, and other response headers.
SSL Checker
Review TLS certificate, HTTPS, and transport security signals.
DMARC Checker
Validate email authentication records for domain spoofing protection.
CSP Checker
Review Content-Security-Policy coverage and common gaps.