Back to Blog
medium SEVERITY7 min read

Mass Assignment Vulnerability: Why Your Rails Models Need attr_accessible

A medium-severity mass assignment vulnerability was identified in a Ruby on Rails model that lacked proper attribute whitelisting via `attr_accessible` or strong parameters. Without this protection, attackers can manipulate any model attribute through crafted HTTP requests, potentially escalating privileges or corrupting data. The fix enforces explicit attribute allowlisting, closing the door on unauthorized mass assignment exploitation.

O
By orbisai0security
May 28, 2026

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 email or password_digest directly
  • 💰 Financial fraud — Manipulating balance, credits, or discount_rate fields
  • 🔓 Authorization bypass — Setting role, permissions, or confirmed flags
  • 📧 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:

  1. Context-aware — Different controller actions can permit different attributes (e.g., admins can set role, regular users cannot)
  2. Explicit errors — Attempting to use unpermitted parameters raises an ActionController::UnpermittedParameters exception in development
  3. Separation of concerns — Security logic lives in the controller, not the model
  4. 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.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1678

Related Articles

critical

Critical Buffer Overflow in OJ's fast.c: How an Unsafe strcpy Nearly Opened the Door to RCE

A critical buffer overflow vulnerability was discovered and patched in the popular OJ Ruby JSON library's fast.c parser, where an unbounded strcpy call allowed attacker-controlled JSON input to overwrite adjacent memory. Left unpatched, this classic CWE-120 flaw could enable arbitrary code execution in any application parsing untrusted JSON with the affected library. The fix eliminates the unsafe copy operation, closing a potential remote code execution vector that affected countless Ruby applic

medium

Silent Data Destruction: The Hidden Danger in Upload Price Tier Logic

A medium-severity vulnerability in Fastlane's `deliver` tool revealed how a subtle semantic distinction between `nil` and an empty array could silently remove an app from sale in every App Store territory worldwide — with no warning, no confirmation, and a misleading success message to cover its tracks. This post breaks down how the bug worked, why it matters, and what developers can learn about defensive coding with destructive operations.

medium

Integer Overflow in Shared Memory Bounds Check: How a Missing Cast Opened the Door to Arbitrary Memory Writes

A subtle but dangerous integer overflow vulnerability was discovered in `lib/rpmi_shmem.c`, where bounds checks on shared memory operations could be silently bypassed due to 32-bit arithmetic overflow. By carefully crafting `offset` and `len` values, an OS-level or hypervisor-level caller could direct firmware writes to arbitrary memory addresses — including interrupt vector tables and security-critical configuration structures. The fix was elegantly simple: casting operands to 64-bit before add

medium

Buffer Overflow in Freestanding Runtime: How Unsafe strcpy() Puts Bare-Metal Systems at Risk

A critical buffer overflow vulnerability was discovered in the freestanding runtime's custom string library, where `strcpy()` and `memcpy()` implementations lacked any bounds checking whatsoever. In a bare-metal or kernel-like environment with no OS-level memory protection, this flaw could allow an attacker to overwrite adjacent memory regions — including function pointers and security-critical state — with arbitrary data. The fix introduces a safe `strlcpy()` implementation that enforces destin

medium

Integer Overflow in Packet Reassembly: How One Missing Check Enables Heap Corruption

A critical heap buffer overflow vulnerability was discovered in the network packet reassembly function of `net_channel_ex.c`, where an attacker-controlled `bodylen` field could be used to corrupt heap memory without any bounds validation. The fix introduces a simple yet effective integer overflow check before accumulating packet body lengths, preventing malformed packets from triggering memory corruption. This type of vulnerability is a stark reminder that even low-level arithmetic operations in

medium

Buffer Overflow via Unsafe sprintf() in C Game Menu: How Shared Campaign Files Could Lead to Code Execution

A series of unbounded `sprintf()` calls in `src/mainmenu.c` created a realistic buffer overflow attack chain, allowing an attacker to craft a malicious campaign file that triggers arbitrary code execution when loaded by a victim. The fix replaces each unsafe `sprintf()` with `snprintf()`, enforcing strict buffer size limits and eliminating the overflow conditions. Because campaign files are routinely shared in game communities, this vulnerability required no special access and posed a significan