Introduction
If you've ever built a Rails application, you've likely used mass assignment — the convenient ability to set multiple model attributes at once from a hash of parameters. It's one of Rails' most developer-friendly features. But convenience without guardrails can be dangerous.
A mass assignment vulnerability occurs when a web application blindly accepts user-supplied parameters and passes them directly to a model without filtering which attributes are allowed to be set. In Rails, this means an attacker could craft an HTTP request that sets any attribute on your model — including ones you never intended to expose.
This post breaks down a real-world mass assignment vulnerability found in a Ruby on Rails codebase, explains how it could be exploited, and walks through the fix that closes the security gap.
The Vulnerability Explained
What Is Mass Assignment?
In Rails, mass assignment looks innocent enough:
# A typical controller action
def create
@user = User.new(params[:user])
@user.save
end
This takes all parameters under the user key and assigns them to the model at once. Convenient — but what if params[:user] contains fields like admin: true or role: "superuser"?
The Problem: No Attribute Whitelisting
When a model does not use attr_accessible (for older Rails versions) or strong parameters (for Rails 4+), every attribute on the model is fair game for mass assignment. The vulnerability identified in this codebase falls squarely into this category:
# VULNERABLE: No attr_accessible defined
class User < ActiveRecord::Base
# All attributes are freely assignable — including sensitive ones!
validates :email, presence: true
validates :username, presence: true
end
Without attr_accessible, there is no limiting of which variables can be manipulated through mass assignment. This is precisely what the security scanner flagged.
How Could It Be Exploited?
An attacker who understands your database schema (or can infer it) can send crafted HTTP requests to manipulate protected fields. Here's a concrete attack scenario:
Scenario: Privilege Escalation
Imagine your users table has an is_admin boolean column. Your registration form only shows fields for username, email, and password. But an attacker can bypass your form entirely:
# Attacker sends a crafted POST request
curl -X POST https://yourapp.com/users \
-d "user[username]=hacker" \
-d "user[email]=hacker@evil.com" \
-d "user[password]=secret123" \
-d "user[is_admin]=true" # <-- This shouldn't be settable!
If the model has no attribute protection, Rails happily accepts is_admin=true and the attacker just gave themselves administrator access.
Other Real-World Attack Vectors:
- 🔑 Account takeover — Setting another user's
emailorpassword_digestdirectly - 💰 Financial fraud — Manipulating
balance,credits, ordiscount_ratefields - 🔓 Authorization bypass — Setting
role,permissions, orconfirmedflags - 📧 Data poisoning — Overwriting
created_at,updated_at, or audit trail fields
This vulnerability type is so well-known that it has its own OWASP entry and contributed to one of the most famous Rails security incidents — the GitHub mass assignment hack of 2012, where a researcher added his SSH key to any repository by exploiting this exact flaw.
Severity Assessment
| Factor | Detail |
|---|---|
| Severity | Medium |
| OWASP Category | A04:2021 – Insecure Design |
| CWE | CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes |
| Exploitability | Easy — requires only crafted HTTP parameters |
| Impact | Privilege escalation, data tampering, authentication bypass |
The Fix
The fix depends on which version of Rails your application uses. Both approaches enforce explicit allowlisting — you declare exactly which attributes users are permitted to set.
Fix for Older Rails (Rails 3 and below): attr_accessible
# BEFORE (vulnerable)
class User < ActiveRecord::Base
validates :email, presence: true
validates :username, presence: true
end
# AFTER (protected)
class User < ActiveRecord::Base
# Only these attributes can be set via mass assignment
attr_accessible :username, :email, :password, :password_confirmation
validates :email, presence: true
validates :username, presence: true
end
With attr_accessible, any attempt to mass-assign an unlisted attribute (like is_admin) is silently ignored. The sensitive field simply won't be set.
Fix for Modern Rails (Rails 4+): Strong Parameters
For newer Rails applications, the protection moves to the controller layer using strong parameters:
# BEFORE (vulnerable controller)
class UsersController < ApplicationController
def create
@user = User.new(params[:user]) # Accepts everything!
if @user.save
redirect_to @user
else
render :new
end
end
end
# AFTER (protected with strong parameters)
class UsersController < ApplicationController
def create
@user = User.new(user_params) # Only permitted params pass through
if @user.save
redirect_to @user
else
render :new
end
end
private
def user_params
# Explicitly declare which attributes are permitted
params.require(:user).permit(:username, :email, :password, :password_confirmation)
# Notice: :is_admin, :role, :balance are NOT listed here
end
end
Why Strong Parameters Is Preferred
Strong parameters offer several advantages over attr_accessible:
- Context-aware — Different controller actions can permit different attributes (e.g., admins can set
role, regular users cannot) - Explicit errors — Attempting to use unpermitted parameters raises an
ActionController::UnpermittedParametersexception in development - Separation of concerns — Security logic lives in the controller, not the model
- Nested attributes — Handles complex nested parameter structures more gracefully
# Strong parameters can handle role-based attribute access elegantly
def user_params
permitted = [:username, :email, :password]
permitted << :is_admin if current_user.admin? # Only admins can set this
params.require(:user).permit(*permitted)
end
Conclusion
Mass assignment vulnerabilities are a classic example of how developer convenience can become a security liability. The absence of attr_accessible or strong parameters in a Rails model is not just a code smell — it's an open door that attackers can walk through to manipulate sensitive data, escalate privileges, or bypass authentication entirely.
The fix is straightforward: always explicitly declare which attributes users are permitted to set. Whether you're using attr_accessible for legacy Rails or strong parameters for modern applications, the principle is the same — trust nothing from user input until you've explicitly said it's okay.
Key takeaways from this vulnerability:
- ✅ Allowlist, don't blocklist — Specify what's permitted, not what's forbidden
- ✅ Use strong parameters in Rails 4+ applications
- ✅ Scan regularly with tools like Brakeman to catch unprotected models
- ✅ Write security tests to verify your protections work as expected
- ✅ Apply defense-in-depth — combine application-level and database-level constraints
Security isn't a feature you add at the end — it's a practice you build into every model, every controller, and every line of code. When in doubt, be explicit about what you allow.
Found a mass assignment vulnerability in your own codebase? Automated security scanning can catch these issues before they reach production. Stay secure, stay explicit.