Rails earned its "secure by default" reputation honestly — CSRF protection, escaped ERB output, and parameterized queries are all on out of the box. But defaults protect you only where you stay on the paved path, and Rails makes it easy to step off. The framework's history proves it: in 2012, security researcher Egor Homakov exploited Rails' then-default mass assignment to grant himself commit access to the Ruby on Rails project on GitHub, a demonstration so pointed it drove the introduction of Strong Parameters. That pattern — a convenient default becoming a foot-gun the moment a developer overrides it — repeats across ActiveRecord queries, view rendering, and the gem ecosystem. This guide walks the places Rails apps actually get breached and the idiomatic way to stay safe.
How do strong parameters stop mass assignment?
By forcing you to declare which attributes a request is allowed to set. Without a filter, Model.new(params[:user]) would let an attacker submit admin=true (or any column) and have ActiveRecord happily assign it — that is mass assignment, and it is exactly what the 2012 GitHub incident exploited. Strong Parameters requires you to permit fields explicitly:
# app/controllers/users_controller.rb
def user_params
params.require(:user).permit(:name, :email, :bio)
# :admin and :role are NOT permitted — an attacker can't set them
end
def update
@user.update(user_params)
end
The rule: never pass raw params into create, update, new, or assign_attributes. Always route through a permit allowlist, and keep privilege-granting columns (admin, role, account_id) out of it unless the action genuinely needs them.
Where does SQL injection still happen in ActiveRecord?
Whenever you interpolate user input into a query string instead of parameterizing it. ActiveRecord parameterizes automatically when you pass conditions as a hash or use placeholders — but the moment you build a fragment with string interpolation, you own the injection risk:
# VULNERABLE: user input concatenated into SQL
User.where("email = '#{params[:email]}'")
# SAFE: hash conditions — ActiveRecord parameterizes
User.where(email: params[:email])
# SAFE: positional placeholders when you need a raw fragment
User.where("email = ?", params[:email])
The same applies to order, group, having, pluck, and find_by_sql — any place you hand ActiveRecord a raw string. Never interpolate params, and be especially careful with order(params[:sort]), a classic injection vector.
Is Rails' CSRF protection enough on its own?
It is strong but only if you leave it on and don't punch holes in it. protect_from_forgery (enabled by default via ActionController::Base) validates an authenticity token on non-GET requests. Two common mistakes undo it: disabling it broadly with skip_before_action :verify_authenticity_token on controllers that handle state changes, and switching the strategy to :null_session for API controllers without adding an alternative (like token or same-site cookie enforcement). For JSON APIs consumed by first-party SPAs, prefer SameSite cookies plus origin checks over silently dropping CSRF entirely.
What about view rendering and output escaping?
ERB escapes interpolated output by default, so <%= @comment.body %> is safe. The danger is raw, html_safe, and <%== %>, which mark a string as trusted HTML and skip escaping — the Rails equivalent of dangerouslySetInnerHTML. Calling .html_safe on user-controlled content is a direct XSS hole. If you must render user HTML, sanitize it with Rails' built-in sanitize helper and a restrictive tags allowlist rather than trusting it wholesale.
How dangerous is deserialization and dynamic rendering?
Very, when untrusted data reaches it. Loading user-supplied YAML with unsafe deserialization, or passing user input into render / render inline: / constantize / send, can escalate to remote code execution. Historically, Rails file-disclosure issues (such as the Action View path-traversal advisory CVE-2019-5418, which let a crafted Accept header read arbitrary files) show how rendering internals become exploitable when input steers them. Never let user input choose a template path, a class name, or a method name; use safe_load for YAML.
Rails security checklist
| Area | Do | Avoid |
|---|---|---|
| Mass assignment | params.permit(:allowed) | Raw params into update |
| SQL | Hash conditions / ? placeholders | String interpolation of params |
| CSRF | Keep protect_from_forgery on | Blanket skip_before_action |
| Views | Default ERB escaping + sanitize | raw / html_safe on user input |
| Deserialization | YAML.safe_load | send/constantize on user input |
| Secrets | Encrypted credentials / ENV | Secrets in source or logs |
| Gems | Continuous SCA + bundler-audit | Unvetted version bumps |
The gem supply chain
Every Rails app depends on dozens of gems and their transitive dependencies, and RubyGems is a supply-chain target like any registry. bundler-audit checks your Gemfile.lock against known advisories, but version-matching alone floods you with findings that may never be reachable from your code. Layer software composition analysis over the full dependency graph so you patch what actually matters, and pin exact versions in Gemfile.lock with bundle install --frozen in CI so a compromised registry can't swap a tarball under a version string.
How Safeguard Helps
Safeguard resolves your complete gem dependency tree and runs reachability analysis, so the CVEs you triage are the ones your Rails code actually calls, not every advisory bundler-audit can name. Griffin, our AI analysis engine, reviews new and updated gem versions for behavioral anomalies before they land in a build, and when a safe upgrade exists, auto-fix opens a tested pull request with the minimal bump. A CLI scan wired into CI turns these standards into an enforced gate instead of a wiki page. Strong Parameters, parameterized queries, and output escaping stay your responsibility — Safeguard secures the gems beneath them.
Harden your Rails supply chain today — get started free, read the documentation, or see how Safeguard compares to Snyk.