Asignación masiva
A continuación, analizaremos las vulnerabilidades de asignación masiva, cómo se manifiestan y varias formas de evitarlas. Primero, un breve resumen:
La asignación masiva es una vulnerabilidad que ocurre cuando los puntos de conexión (endpoints) de una API no restringen qué propiedades de sus objetos asociados pueden ser modificadas por un usuario.
Esta vulnerabilidad puede ocurrir al utilizar una biblioteca o marco de trabajo que permite el enlace automático de parámetros HTTP a un modelo que luego se utiliza sin ninguna validación.
El uso del enlace automático de una solicitud a un objeto puede ser extremadamente útil en ocasiones, pero también puede derivar en problemas de seguridad si el modelo contiene propiedades que no deberían ser accesibles para el usuario.
Ejemplo
Utilizaremos el ejemplo de una página web donde un usuario puede cambiar sus datos, como su nombre, dirección de correo electrónico y otra información similar. Tenemos un modelo de usuario (UserModel) definido de la siguiente manera:
public class UserModel {
public long Id { get; set; }
public string Name { get; set; }
public string PasswordHash { get; set; }
public string EmailAddress { get; set; }
public bool IsAdmin { get; set; }
}
La parte frontal (frontend) define un formulario como el siguiente. Observe la ausencia del valor `IsAdmin`:
<form method="POST">
<input name="Id" type="hidden">
<input name="Name" type="text">
<input name="EmailAddress" type="text">
<input type="submit">
</form>
El controlador define un punto de conexión de la siguiente manera. Al tener `UserModel` como parámetro, nuestro marco de trabajo asignará automáticamente las propiedades respectivas a este modelo por nosotros:
[HttpPost]
public bool UpdateUser(UserModel model)
{
// Asegurar que el usuario solo se actualice a sí mismo
model.Id = Request.User.UserId;
var success = UserService.UpdateUser(model);
return success;
}
A partir de aquí, podemos asumir que el método ‘UserService.UpdateUser’ no realiza ninguna validación adicional en cuanto a autorización y simplemente guarda el objeto de usuario proporcionado.
(Si no se proporciona un valor para una propiedad, simplemente mantiene el valor existente)
Esto significa que un usuario podría enviar una solicitud con el campo ‘IsAdmin’, lo que sobrescribiría el valor actual y convertiría al usuario en administrador de la siguiente manera:
<form method="POST">
<input name="Id" type="hidden" value="666">
<input name="Name" type="text" value="Bad guy">
<input name="EmailAddress" type="text" value="hacker@attacker.com">
<input name="IsAdmin" type="hidden" value="true">
<input type="submit">
</form>
Estrategias de mitigación
A continuación, se presentan algunas estrategias de mitigación a considerar para evitar vulnerabilidades de asignación masiva.
Evite reutilizar modelos de datos para modelos de solicitud
Es importante mantener sus modelos de datos (que pueden persistir en una base de datos) separados de los modelos que se utilizan al comunicarse con un cliente. Gestionar el envío de un formulario a un controlador es una tarea muy distinta a la persistencia de datos en una base de datos y a cómo estos se representan en ella. Esto crea un nivel de acoplamiento entre el frontend y la capa de persistencia mucho mayor de lo recomendable.
Sea explícito en sus asignaciones
El problema con el enlace (o asignación) automático es que la falta de asignaciones explícitas facilita la exposición de propiedades que no deberían ser accesibles en el modelo. Al ser explícito en las asignaciones entre los modelos de solicitud y el resto de su backend, puede prevenir este tipo de exposiciones desde el principio.
Esto se puede lograr utilizando modelos diferentes para las solicitudes y para los datos. Esto no le impide utilizar un asignador automático entre el modelo de solicitud y el de datos, ya que su modelo de solicitud no debería exponer propiedades que no estén permitidas para esa solicitud específica.
Ejemplos adicionales
A continuación, presentamos algunos ejemplos adicionales en diferentes lenguajes sobre cómo puede verse esto.
C# - inseguro
public class User {
public long Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string PasswordHash { get; set; }
public string Country { get; set; }
public string Role { get; set; }
}
[HttpPost]
public ViewResult Edit( User user)
{
// Solo guarda el usuario tal como se proporciona
UserService.UpdateUser(user);
return Ok();
}
C# - seguro
public class User {
public long Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string PasswordHash { get; set; }
public string Country { get; set; }
public string Role { get; set; }
}
public class UpdateUserViewModel {
public string FirstName { get; set; }
public string LastName { get; set; }
public string Country { get; set; }
}
[HttpPost]
public ViewResult Edit(UpdateUserViewModel userModel)
{
var user = Request.User;
user.FirstName = userModel.FirstName;
user.LastName = userModel.LastName;
user.Country = userModel.Country;
UserService.UpdateUser(user);
return Ok();
}
C# - alternativa - excluye parámetros
public class User {
public long Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string PasswordHash { get; set; }
public string Country { get; set; }
public string Role { get; set; }
}
[HttpPost]
public ViewResult Edit([Bind(Include = "FirstName,LastName,Country")] User user)
{
if(Request.User.Id != user.Id) {
return Forbidden("Intentando cambiar a otro usuario");
}
var existingUser = Request.User;
user.PasswordHash = existingUser.PasswordHash;
user.Role = existingUser.Role;
UserService.UpdateUser(user);
return Ok();
}
Java - inseguro
public class User {
public int id;
public String firstName;
public String lastName;
public String passwordHash;
public String country;
public String role;
}
@RequestMapping(value = "/updateUser", method = RequestMethod.POST)
public String updateUser(User user) {
userService.update(user);
return "userUpdatedPage";
}
Java - seguro
public class UserViewModel {
public String firstName;
public String lastName;
public String country;
}
public class User {
public int id;
public String firstName;
public String lastName;
public String passwordHash;
public String country;
public String role;
}
@RequestMapping(value = "/updateUser", method = RequestMethod.POST)
public String updateUser(@AuthenticationPrincipal User currentUser, UserViewModel userViewModel) {
currentUser.firstName = userViewModel.firstName;
currentUser.lastName = userViewModel.lastName;
currentUser.country = userViewModel.country;
userService.update(currentUser);
return "userUpdatedPage";
}
Javascript - inseguro
app.get('/user/update', (req, res) => {
var user = req.user;
Object.assign(user, req.body);
UserService.Update(user);
return "Usuario actualizado";
})
Javascript - seguro
app.get('/user/update', (req, res) => {
var user = req.user;
user.firstName = req.body.firstName;
user.lastName = req.body.lastName;
user.country = req.body.country;
UserService.Update(user);
return "Usuario actualizado";
})
Python - Inseguro
@app.route("/user/update", methods=['POST'])
def update_user():
user = request.user
form = request.form.to_dict(flat=True)
for key, value in form.items():
setattr(user, key, value)
UserService.UpdateUser(user)
return redirect("/user", code=201)
Python - Seguro
@app.route("/user/update", methods=['POST'])
def update_user():
user = request.user
form = request.form
user.firstName = form.firstName
user.lastName = form.lastName
UserService.UpdateUser(user)
return redirect("/user", code=201)
Programación segura y práctica
Explore las directrices de programación segura para comprender y mitigar vulnerabilidades de software como el OWASP Top 10, y sumérjase en misiones de formación guiada para practicar en simulaciones de aplicaciones del mundo real.