RoR CSRF and Security Basics

Security is not optional. A Rails application handles real user data, and attackers look for weaknesses in every web app. Rails builds in several security protections by default. Understanding these protections helps you use them correctly and avoid accidentally disabling them.

CSRF — Cross-Site Request Forgery

CSRF is an attack where a malicious website tricks a logged-in user's browser into sending a request to your app without their knowledge.

CSRF Attack Scenario:

1. Alice logs into your banking app at bank.com
   Her browser stores a session cookie for bank.com

2. Alice visits evil.com in another tab
   evil.com contains hidden HTML:
   <form action="https://bank.com/transfer" method="post">
     <input name="amount" value="5000">
     <input name="to" value="hacker_account">
   </form>
   <script>document.forms[0].submit()</script>

3. Alice's browser sends the POST to bank.com
   The session cookie is included automatically
   bank.com sees a valid logged-in request and processes the transfer

How Rails Stops CSRF

Rails generates a unique secret token for each session and embeds it in every form. When a POST request arrives, Rails checks that this token matches. Requests from other sites do not have the token, so Rails rejects them.

In your layout (already included by Rails):
<%= csrf_meta_tags %>

Generates:
<meta name="csrf-param" content="authenticity_token">
<meta name="csrf-token" content="abc123uniquetoken...">

Rails form helpers automatically include:
<input type="hidden" name="authenticity_token" value="abc123uniquetoken...">

On every POST/PATCH/PUT/DELETE request:
Rails checks → token valid? → Continue
               token invalid or missing? → ActionController::InvalidAuthenticityToken error

This protection is enabled by default in ApplicationController:

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

Never disable this. For API controllers that use token-based auth instead of sessions, use protect_from_forgery with: :null_session.

SQL Injection

SQL injection happens when user input is inserted directly into a database query, allowing attackers to run arbitrary SQL.

UNSAFE — Never do this:
User.where("name = '#{params[:name]}'")

Attack: params[:name] = "' OR '1'='1"
Resulting SQL: SELECT * FROM users WHERE name = '' OR '1'='1'
This returns ALL users.

SAFE — Always use parameterized queries:
User.where("name = ?", params[:name])
User.where(name: params[:name])

Rails escapes the input before inserting it into the SQL.

XSS — Cross-Site Scripting

XSS happens when user-supplied content is rendered as HTML in a page, allowing attackers to inject and run JavaScript in other users' browsers.

Attack: User submits this as their username:
  <script>document.cookie = "hacked"</script>

If your app renders this without escaping:
  <p>Welcome, <script>document.cookie = "hacked"</script></p>

The script runs in every visitor's browser.

Rails escapes output by default when you use <%= %>:

<%= user.name %>
Renders:  Welcome, &lt;script&gt;...  (harmless text)

html_safe and raw bypass this protection:
<%= user.name.html_safe %>   ← DANGEROUS with user input
<%= raw user.name %>          ← DANGEROUS with user input

Never mark user-submitted content as html_safe. Only use html_safe on strings you fully control in your own code.

Mass Assignment Protection

Without strong parameters, an attacker could add extra fields to a form submission to overwrite sensitive model attributes like role or admin.

UNSAFE — Old Rails style (never do this):
User.create(params[:user])   ← allows any field

SAFE — Strong parameters:
User.create(user_params)

def user_params
  params.require(:user).permit(:name, :email, :password)
  ← only name, email, and password are allowed
  ← role, admin, and other fields are stripped out
end

Sensitive Data in Logs

Rails logs every request. Filter out sensitive fields so passwords and tokens do not appear in log files:

config/initializers/filter_parameter_logging.rb

Rails.application.config.filter_parameters += [
  :password,
  :password_confirmation,
  :credit_card_number,
  :token,
  :secret
]

Filtered values appear as [FILTERED] in logs instead of their actual content.

Secure Headers

Rails sets several HTTP security headers by default. Add the secure_headers gem for additional protection:

gem "secure_headers"
bundle install
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 = "1; mode=block"
  config.content_security_policy = {
    default_src: %w('self'),
    img_src: %w('self' data:),
    script_src: %w('self')
  }
end

Environment Variables for Secrets

Never hardcode API keys, passwords, or secrets in your code. Use environment variables:

BAD — hardcoded secret in code:
stripe_secret = "sk_live_abc123"

GOOD — environment variable:
stripe_secret = ENV["STRIPE_SECRET_KEY"]

Store secrets in Rails encrypted credentials:

rails credentials:edit

Inside the file:
  stripe:
    secret_key: sk_live_abc123
    publishable_key: pk_live_xyz789

Access in code:
  Rails.application.credentials.stripe[:secret_key]

Security Checklist

Security RiskRails ProtectionStatus
CSRFprotect_from_forgery + authenticity_tokenOn by default
SQL InjectionParameterized queries via ActiveRecordSafe when using Rails query methods
XSSAutomatic HTML escaping in ERBOn by default
Mass AssignmentStrong ParametersRequired in every controller
Sensitive Logsfilter_parameters configPartial — add your own fields
Secrets in CodeRails Credentials or ENV varsManual — developer responsibility

Rails handles most common web security threats by default. Your job is to keep these protections enabled, use strong parameters consistently, and never expose user data carelessly.

Leave a Comment

Your email address will not be published. Required fields are marked *