Asignación de masas
En este momento, vamos a repasar las vulnerabilidades de Asignación Masiva y cómo se ven, junto con algunas formas de evitarlas. En primer lugar, un breve resumen:
La asignación masiva es una vulnerabilidad en la que los puntos finales de la API no restringen qué propiedades de su objeto asociado pueden ser modificadas por un usuario.
Esta vulnerabilidad puede ocurrir cuando se hace uso de una librería/framework que permite la vinculación automática de parámetros HTTP a un modelo que luego pasa a ser utilizado sin ningún tipo de validación.
El uso de la vinculación automática de una solicitud a un objeto puede ser extremadamente útil a veces, pero también puede conducir a problemas de seguridad si el modelo tiene propiedades que no están destinadas a ser accesibles para el usuario.
Ejemplo
Vamos a utilizar el ejemplo de una página web donde un usuario puede cambiar detalles, como su nombre, dirección de correo electrónico y otras cosas similares. Tenemos un modelo de Usuario definido como:
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; }
}
The frontend part defines a form as following. Note the absence of the `IsAdmin` value:
<form method="POST">
<input name="Id" type="hidden">
<input name="Name" type="text">
<input name="EmailAddress" type="text">
<input type="submit">
</form>
The controller defines an endpoint as following. By having the `UserModel` as a parameter, our framework will automatically map the respective properties onto this model for us:
[HttpPost]
public bool UpdateUser(UserModel model)
{
// Ensure the user only updates themselves
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 hace ninguna validación adicional en términos de autorización, y simplemente guarda el objeto usuario proporcionado.
(Si no se proporciona ningún valor para una propiedad, se mantiene el valor existente)
Esto significa que un usuario podría enviar una solicitud con el valor 'IsAdmin', que anularía el valor actual y convertiría al usuario en administrador de esta forma:
<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 cuando se trata de evitar vulnerabilidades de Asignación Masiva.
Evitar la reutilización de modelos de datos para modelos de solicitud
Es importante mantener los modelos de datos (que pueden persistir en una base de datos) separados de los modelos que se utilizan al comunicarse con un cliente. Manejar el envío de un formulario a un controlador es una preocupación muy diferente de la persistencia de datos en una base de datos y cómo se representa en la base de datos. Esto crea un nivel mucho más alto de acoplamiento entre el frontend y la capa de persistencia de lo que es bueno.
Sea explícito en sus asignaciones
El problema con la vinculación automática (o mapeo) es que la falta de mapeos explícitos hace que sea fácil exponer propiedades que no están destinadas a ser accesibles en el modelo. Al ser explícito en los mapeos entre los modelos de solicitud, y el resto de su backend, puede evitar este tipo de exposiciones desde el principio.
Esto podría hacerse utilizando modelos diferentes para las peticiones y los datos. Esto no le impide utilizar un mapeador automático entre el modelo de solicitud y el de datos, ya que su modelo de solicitud no debe exponer propiedades que no estén permitidas para la solicitud específica.
Otros ejemplos
A continuación le ofrecemos algunos ejemplos adicionales en diferentes idiomas.
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)
{
// Just saves the user as provided
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("Requesting changing of another user");
}
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 "User has been updated";
})
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 "User has been updated";
})
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