Rails Security Essentials — CSRF, SQL Injection, XSS, and Secure Headers

ruby dev.to

Every Rails app you deploy is a target. The moment it's on a VPS with a public IP, bots will probe it. This post covers the security essentials every Rails developer needs to know — not theory, but the actual attacks and the actual defenses.

CSRF — Cross-Site Request Forgery

Rails protects you by default, but you need to understand why.

CSRF tricks a logged-in user's browser into making requests to your app. Rails prevents this with authenticity tokens embedded in every form.

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
end
Enter fullscreen mode Exit fullscreen mode

Every form Rails generates includes a hidden token:

<%= form_with model: @post do |f| %>
  <%# Rails automatically inserts authenticity_token here %>
  <%= f.text_field :title %>
  <%= f.submit %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

If you're building an API, you skip CSRF protection (APIs use token auth instead):

class Api::BaseController < ActionController::API
  # No CSRF — API clients send auth tokens in headers
end
Enter fullscreen mode Exit fullscreen mode

The mistake people make: Disabling CSRF globally because "it was causing errors." Don't. Fix the actual issue instead.

SQL Injection

Active Record protects you — if you use it correctly.

# SAFE — parameterized query
User.where(email: params[:email])
# Generates: SELECT * FROM users WHERE email = $1

# SAFE — parameterized string
User.where("email = ?", params[:email])

# DANGEROUS — string interpolation
User.where("email = '#{params[:email]}'")
# Attacker sends: ' OR 1=1 --
# Generates: SELECT * FROM users WHERE email = '' OR 1=1 --'
Enter fullscreen mode Exit fullscreen mode

The rule is simple: never interpolate user input into SQL strings. Always use ? placeholders or hash conditions.

Check for injection vulnerabilities with Brakeman:

gem install brakeman
cd your_rails_app
brakeman
Enter fullscreen mode Exit fullscreen mode

Brakeman scans your codebase and flags dangerous patterns. Run it in CI. Every time.

# Gemfile (development/test)
group :development, :test do
  gem "brakeman", require: false
end
Enter fullscreen mode Exit fullscreen mode

XSS — Cross-Site Scripting

XSS lets attackers inject JavaScript into your pages. Rails escapes output by default in ERB:

<%# SAFE — auto-escaped %>
<p><%= @user.bio %></p>
<%# If bio contains <script>alert('xss')</script>, it renders as text %>

<%# DANGEROUS — raw output %>
<p><%= raw @user.bio %></p>
<p><%= @user.bio.html_safe %></p>
<%# These render the script tag as actual JavaScript %>
Enter fullscreen mode Exit fullscreen mode

Rules:

  1. Never use raw or html_safe on user input
  2. If you must render HTML, sanitize it first:
# In your view
<%= sanitize @user.bio, tags: %w[b i em strong p br], attributes: %w[class] %>
Enter fullscreen mode Exit fullscreen mode
  1. Use Content Security Policy headers to limit what scripts can run:
# config/initializers/content_security_policy.rb
Rails.application.configure do
  config.content_security_policy do |policy|
    policy.default_src :self
    policy.script_src  :self
    policy.style_src   :self, :unsafe_inline
    policy.img_src     :self, :data, :https
    policy.font_src    :self
    policy.connect_src :self
    policy.frame_src   :none
  end

  config.content_security_policy_nonce_generator = ->(request) {
    SecureRandom.base64(16)
  }
end
Enter fullscreen mode Exit fullscreen mode

Secure Headers

Beyond CSP, set these headers. The secure_headers gem makes it easy:

# Gemfile
gem "secure_headers"

# config/initializers/secure_headers.rb
SecureHeaders::Configuration.default do |config|
  config.x_frame_options = "DENY"
  config.x_content_type_options = "nosniff"
  config.x_xss_protection = "0" # Modern browsers use CSP instead
  config.referrer_policy = %w[strict-origin-when-cross-origin]
  config.hsts = "max-age=631138519; includeSubDomains"
end
Enter fullscreen mode Exit fullscreen mode

Or set them manually in Nginx on your VPS:

add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=631138519; includeSubDomains" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
Enter fullscreen mode Exit fullscreen mode

Strong Parameters

Never trust user input. Strong params whitelist what's allowed:

class PostsController < ApplicationController
  def create
    @post = Post.new(post_params)
    # ...
  end

  private

  def post_params
    params.require(:post).permit(:title, :body, :published)
    # Only these three fields pass through
    # Everything else is silently dropped
  end
end
Enter fullscreen mode Exit fullscreen mode

Common mistake: Permitting :admin or :role fields. An attacker just adds &user[admin]=true to the request.

Environment Secrets

Never commit secrets. Use Rails credentials:

# Edit encrypted credentials
EDITOR="vim" bin/rails credentials:edit

# Access in code
Rails.application.credentials.openai_api_key
Rails.application.credentials.dig(:aws, :secret_key)
Enter fullscreen mode Exit fullscreen mode

The config/credentials.yml.enc file is encrypted. The config/master.key decrypts it. Add master.key to .gitignore (Rails does this by default).

On your VPS, set the master key as an environment variable:

export RAILS_MASTER_KEY=your-master-key-here
Enter fullscreen mode Exit fullscreen mode

The Security Checklist

Run this before every deploy:

  1. Brakeman — static analysis for vulnerabilities
  2. bundle audit — check gems for known CVEs
  3. Strong params — every controller action
  4. CSP headers — configured and tested
  5. HTTPS only — force SSL in production
# config/environments/production.rb
config.force_ssl = true
Enter fullscreen mode Exit fullscreen mode
# Check gems for vulnerabilities
gem install bundler-audit
bundle audit check --update
Enter fullscreen mode Exit fullscreen mode

Security isn't a feature you add at the end. It's a habit you build from the start. Rails gives you excellent defaults — your job is to not turn them off.

Next up: background job patterns for AI workloads — retries, rate limiting, and dead letter queues.

Source: dev.to

arrow_back Back to Tutorials