From grimoire
Defines allowlists per operation to block privilege escalation via mass assignment in web frameworks like FastAPI, Django, Rails, and Spring Boot.
How this skill is triggered — by the user, by Claude, or both
Slash command
/grimoire:prevent-mass-assignmentThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Allowlist only the fields users are permitted to set when binding request data to model objects — never allow users to set internal fields like `is_admin`, `role`, `account_balance`, or `created_at`.
Allowlist only the fields users are permitted to set when binding request data to model objects — never allow users to set internal fields like is_admin, role, account_balance, or created_at.
Adopted by: OWASP API Security Top 10 2023 API3 (Broken Object Property Level Authorization) is entirely caused by mass assignment. Rails, Django, and Laravel have all had critical mass assignment CVEs: GitHub (2012, CVE-2012-2661, allowed pushing to any repo via mass assignment to ActiveRecord), Rails (CVE-2013-2615). Django REST Framework, FastAPI, Spring Boot, and ASP.NET Core all provide allowlist mechanisms. OWASP ranks this in the API Top 10 specifically because it's pervasive in modern auto-binding frameworks.
Impact: The GitHub 2012 mass assignment vulnerability (Egor Homakov) allowed any user to gain admin-level access by submitting user[admin]=1 in a form — exploited publicly to demonstrate the issue. Every framework that auto-binds request parameters to model attributes is vulnerable by default unless explicitly configured. One missing allowlist in a user update endpoint can allow privilege escalation.
Why best: Denylist approaches (listing which fields to block) require knowing all dangerous fields in advance — new fields added to the model become vulnerable automatically. Allowlist (permitting only explicitly safe fields) is safe by default: new fields are blocked until explicitly permitted.
Sources: OWASP Mass Assignment Cheat Sheet; GitHub CVE-2012-2661; OWASP API Security Top 10 2023; CWE-915
Define explicit allowlists per operation — different operations permit different fields:
# FastAPI / Pydantic — separate schemas for create vs update vs response
from pydantic import BaseModel
from typing import Optional
class UserCreate(BaseModel):
username: str
email: str
password: str
# NOT included: id, role, is_admin, created_at, account_balance
class UserUpdate(BaseModel):
email: Optional[str] = None
display_name: Optional[str] = None
# More restrictive than create — can't change username
class UserResponse(BaseModel):
id: int
username: str
email: str
display_name: Optional[str]
# NOT included: password_hash, internal_notes, payment_info
@app.post('/users')
def create_user(data: UserCreate): # only fields in UserCreate can be set
user = User(**data.dict())
db.save(user)
return UserResponse.from_orm(user)
Django — use fields on ModelForm and Serializer:
# ModelForm: explicit fields
class UserUpdateForm(forms.ModelForm):
class Meta:
model = User
fields = ['email', 'first_name', 'last_name']
# is_staff, is_superuser, groups NOT listed → cannot be set
# Django REST Framework: explicit fields
class UserUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['email', 'first_name', 'last_name']
read_only_fields = ['id', 'username', 'date_joined']
Rails — use strong parameters (required since Rails 4):
def user_params
params.require(:user).permit(:email, :first_name, :last_name)
# role, admin, confirmed NOT permitted
end
def update
@user.update(user_params) # only permitted params applied
end
Spring Boot — use DTOs, not entities directly:
// BAD — User entity has role, isAdmin, createdAt
@PutMapping("/users/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) {
return userRepo.save(user); // attacker can set user.setAdmin(true)
}
// GOOD — DTO limits what can be changed
public class UserUpdateDTO {
private String email;
private String displayName;
// role, isAdmin, createdAt NOT present
}
@PutMapping("/users/{id}")
public UserResponseDTO updateUser(@PathVariable Long id,
@RequestBody @Valid UserUpdateDTO dto) {
User user = userRepo.findById(id).orElseThrow();
user.setEmail(dto.getEmail());
user.setDisplayName(dto.getDisplayName());
return toResponseDTO(userRepo.save(user));
}
Never use update_attributes(params) or equivalent with raw request data:
# BAD — Rails, binds everything
@user.update_attributes(params[:user])
# GOOD — filter first
@user.update_attributes(user_params)
# BAD — Django, binds everything
User.objects.filter(pk=user_id).update(**request.data)
# GOOD — use serializer with explicit fields
serializer = UserUpdateSerializer(user, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
Use read-only fields for system-managed properties — even if a field leaks into the allowlist, mark it non-writable:
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'email', 'created_at']
read_only_fields = ['id', 'created_at'] # API can return but not set
user[address][attributes][admin]=true bypasses flat parameter filters.password_hash, internal_token, or ssn in API responses.exclude lists — exclusion lists grow stale as models evolve.exclude instead of fields — adding a new sensitive field to the model automatically exposes it. Always use fields (allowlist).user.address = Address(**request.data['address']) can mass-assign the nested object too.npx claudepluginhub jeffreytse/grimoire --plugin grimoireDetects ORM create/update calls that spread request bodies without explicit field allowlists. Use when auditing code for mass assignment vulnerabilities.
Tests API endpoints for mass assignment vulnerabilities by injecting unauthorized fields like role, isAdmin, price, balance into requests. Useful for OWASP API3:2023 BOLA audits in Rails, Django, Express, Spring apps.
Discovers and exploits mass assignment vulnerabilities in REST APIs for privilege escalation, restricted field updates, and auth bypass during security testing of Rails, Django, Laravel, Spring apps.