By framework
Ruby on Rails security — a production hardening reference
A Ruby on Rails production-hardening reference: a priority checklist plus secrets/credentials, config, gem CVEs, Strong Parameters, authorization, injection, and SSRF. Defensive, no attack steps.
For: anyone running a Ruby on Rails app. No attack steps here — this is a working reference for hardening: a priority-ordered checklist, per-area guidance, and self-verification. For the cross-framework picture, see the security-by-framework hub.
Priority-ordered hardening checklist
Do this table top-down. P0 is a prerequisite, P1 is the most frequent source of incidents, P2 is ongoing operational hygiene.
P0 ── Prerequisite (do first)
Secrets & credentials management / production config (no exception exposure, force_ssl) / gem CVEs patched fast
P1 ── Top source of incidents
Strong Parameters/Mass Assignment control / authorization (owner scope)
P2 ── Operational hygiene
Injection and dangerous methods / sessions, CSRF / SSRF, uploads
| Priority | Control | Specifics (Rails) |
|---|---|---|
| P0 | Secrets & credentials | Encrypted credentials + separate master key. Don't commit the master key. Rotate secret_key_base on leak |
| P0 | Production config | Don't expose exception detail; config.force_ssl; filter_parameters to keep secrets out of logs |
| P0 | gem (dependency) CVEs | Monitor with bundler-audit / osv-scanner; judge by running version, patch fast |
| P1 | Strong Parameters | Keep permit minimal. Don't use permit!. Never assign a privilege field |
| P1 | Explicit authorization | Pundit / CanCanCan + authorize + current_user scoping for ownership |
| P2 | Injection/dangerous methods | Bind in where. Don't pass input to send/constantize. Don't load untrusted YAML/Marshal |
| P2 | Sessions/CSRF | protect_from_forgery (default), cookie secure/httponly/samesite, regenerate on login |
| P2 | SSRF/uploads | Allowlist server-side fetches + block internal IPs. Validate uploads, store outside public/ |
1. Secrets and credentials (P0)
- Use Rails's encrypted credentials, and don't commit the master (decryption) key to the repo (inject it via env, etc.).
secret_key_baseunderpins signed/encrypted cookies and sessions. Rotate promptly if leaked (note this invalidates existing signatures/encryption).- Don't leave
.env, dumps, or backups in a public directory (→ keep secrets out of public directories).
2. Production config (P0)
- In production, don't expose exception detail (don't enable
consider_all_requests_local, etc.) — reduce internal-structure disclosure. config.force_sslto force HTTPS and mark cookies secure. Don't enable dev tools like the web console in production.- Use
filter_parametersto keep passwords and the like out of logs.
3. gem (dependency) CVEs (P0)
- Machine-monitor gem CVEs with bundler-audit or osv-scanner, judge by the running version (Gemfile.lock), and patch fast (→ monitoring dependency CVEs · the vulnerability-response playbook).
- Keep Rails and Ruby on supported versions and don't leave EOL versions in place.
4. Strong Parameters and Mass Assignment (P1)
- Keep
permitfields to the minimum, and avoidpermit!(allow-all). - Never assign a privilege field like
is_adminfrom user input. Drop "permit broadly because it's convenient."
5. Authorization (P1 — the top source)
Common (dangerous)
- no authorization — "logged in = can view/update"
- route-model binding fetches another user's ID
- relying on hidden routes / hard-to-guess IDs
- forgetting
authorizeon some actions
Correct
- policy explicit with Pundit / CanCanCan +
authorizeper action - reads too are owner-scoped by
current_user - verified on every read/update/delete path
- authorization built explicitly, not left to defaults
See what IDOR is. Authentication and authorization differ; authorize close to the data.
6. Injection and dangerous dynamic methods (P2)
- SQL: bind with placeholders in
where; never build queries by string concatenation (avoidwhere("... #{params}")) (→ what SQL injection is). - Dangerous dynamic methods: don't pass user input to
send/public_send/constantize. If you must, restrict strictly with an allowlist. - Deserialization: avoid loading untrusted
YAML/Marshal(can lead to code execution under the right conditions).
7. Sessions, cookies, CSRF (P2)
- Don't globally disable CSRF protection (
protect_from_forgery); use the form token (→ what CSRF is). - Set secure/httponly/samesite on cookies/sessions, and regenerate the session on login.
8. SSRF, uploads, headers (P2)
- Server-side fetching of user-supplied URLs should restrict targets to an allowlist and block reaching internal IPs/metadata (→ what SSRF is).
- Validate uploads (type/size), store outside
public/, and grant no execute permission. - Add security headers (check your own site with the security headers checker).
Verify: is your Rails actually hardened?
Building it isn't the end — it's done only once you've checked. These are defensive self-checks against your own environment.
Secrets aren't exposed/committed
master.key isn't in the repo, and that .env/dumps aren't fetchable by URL.No exception detail in production
Authorization holds
Dependencies and headers
bundler-audit/osv is clean, and that HTTPS/HSTS are present via the headers checker.This site's view: even guarded by conventions, authorization and dependencies are on you
Rails's good defaults erase a lot of risk, but authorization — who may do what — and dependency freshness are app- and operation-specific, so a framework can't guard them automatically. The incidents we keep seeing are less elaborate attacks than the "authenticated but no ownership check" type. So the center of gravity is working the table above top-down — tighten Strong Parameters, make authorization explicit, and monitor gems for CVEs.
Read next
- Hub: security by framework · Laravel security (same MVC, similar authorization shape)
- Practice: the vulnerability-response playbook · monitoring dependency CVEs · keep secrets out of public directories
- Glossary: what IDOR is · SQL injection · CSRF · SSRF
- Tools: security headers checker
FAQ
QWhat should I do first to secure Rails?
The three P0 items: (1) handle secrets safely (keep encrypted credentials separate from the master key, don't commit the master key; secret_key_base underpins signed/encrypted cookies, so rotate on leak); (2) firm up production config (don't expose exception detail; force_ssl); (3) machine-monitor gem (dependency) CVEs and patch fast, judging by the running version. Next, move to Strong Parameters and authorization.
QWhat should I watch for with Strong Parameters?
Explicitly allow (permit) which fields you accept to prevent Mass Assignment (bulk-assigning unintended fields). The danger is permitting broadly for convenience, and using permit! to allow everything. Keep permitted fields to the minimum, and never let a privilege field like is_admin be assigned from user input.
QHow do I implement authorization safely?
On top of login (authentication), implement an ownership check that the target truly belongs to the user. Make the policy explicit with Pundit/CanCanCan, run authorize in each action, and scope resources by current_user. Don't rely on hidden routes or hard-to-guess IDs — verify explicitly on every read/update/delete path.
QHow do I manage gem (dependency) vulnerabilities?
Machine-monitor known CVEs with bundler-audit or osv-scanner and patch fast, judging by the running version (decide on the actual version in Gemfile.lock). Keep Rails and Ruby on supported versions and don't leave EOL versions in place. Dependency freshness is a more realistic factor in incidents than elaborate attacks.
QWhat are the dangerous dynamic methods?
Passing user input to send / public_send / constantize can lead to unintended method calls or class resolution. Likewise, loading via Marshal or untrusted YAML (restoring objects) can, under the right conditions, lead to code execution. Don't pass user-derived data to these; if you must, restrict strictly with an allowlist.