Mass Assignment Vulnerability: Why Your Rails Models Need attr_accessible
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
Prevention & Best Practices
1. Always Explicitly Allowlist Attributes
Never use permit! (which allows all parameters) except in rare, well-justified circumstances:
# DANGEROUS — never do this in production
params.require(:user).permit!
# SAFE — explicit allowlist
params.require(:user).permit(:name, :email)
2. Enable Strong Parameter Enforcement
In your Rails configuration, ensure unpermitted parameters raise exceptions in development:
# config/environments/development.rb
config.action_controller.action_on_unpermitted_parameters = :raise
# config/environments/test.rb
config.action_controller.action_on_unpermitted_parameters = :raise
3. Audit Your Models Regularly
Use security scanning tools to catch unprotected models:
- Brakeman — The gold standard for Rails static analysis
bash gem install brakeman brakeman -o output.html - bundler-audit — Checks for vulnerable gem versions
- RuboCop with security extensions — Catches common security anti-patterns
4. Apply the Principle of Least Privilege
Only permit the minimum set of attributes needed for each operation:
# Separate parameter methods for different contexts
def user_registration_params
params.require(:user).permit(:username, :email, :password, :password_confirmation)
end
def user_profile_update_params
params.require(:user).permit(:display_name, :bio, :avatar)
end
def admin_user_params
params.require(:user).permit(:username, :email, :role, :is_active)
end
5. Use Database-Level Constraints as a Defense-in-Depth
Don't rely solely on application-level protection. Add database constraints for sensitive boolean fields:
# db/migrate/xxx_add_defaults_to_users.rb
def change
change_column_default :users, :is_admin, false
change_column_null :users, :is_admin, false
end
6. Write Tests for Security Boundaries
Verify that sensitive attributes cannot be mass-assigned:
# spec/models/user_spec.rb
RSpec.describe User, type: :model do
describe 'mass assignment protection' do
it 'does not allow is_admin to be set via mass assignment' do
user = User.new(username: 'test', email: 'test@example.com', is_admin: true)
expect(user.is_admin).to be_falsey
end
end
end
Relevant Security Standards
| Standard | Reference |
|---|---|
| OWASP Top 10 | A04:2021 – Insecure Design |
| CWE | CWE-915: Improperly Controlled Modification of Dynamically-Determined Object Attributes |
| Rails Security Guide | Strong Parameters |
| OWASP Rails Cheatsheet | Mass Assignment |
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.