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, <script>... (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 Risk | Rails Protection | Status |
|---|---|---|
| CSRF | protect_from_forgery + authenticity_token | On by default |
| SQL Injection | Parameterized queries via ActiveRecord | Safe when using Rails query methods |
| XSS | Automatic HTML escaping in ERB | On by default |
| Mass Assignment | Strong Parameters | Required in every controller |
| Sensitive Logs | filter_parameters config | Partial — add your own fields |
| Secrets in Code | Rails Credentials or ENV vars | Manual — 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.
