Right now, we’re going to go through Mass Assignment vulnerabilities and what they look like, along with a few ways to avoid them. First, a quick recap:
Mass Assignment is a vulnerability where API endpoints don’t restrict which properties of their associated object can be modified by a user.
This vulnerability can occur when making use of a library/framework that allows for the automatic binding of HTTP parameters onto a model that then goes on to be used without any validation.
The use of the automatic binding from a request onto an object can be extremely helpful at times, but it can also lead to security issues if the model has properties that aren’t meant to be accessible to the user.
Example
We’re going to use the example of a web page where a user can change details, like their name, email address, and other similar stuff. We have a User model defined as:
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;
}
From here, we can assume that the ‘UserService.UpdateUser’ method doesn’t do any further validation in terms of authorization, and simply just saves the provided user object.
(If no value is provided for a property, it just keeps the existing value)
This means that a user could submit a request with the ‘IsAdmin’, which would override the current value and make the user an admin like so:
<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>
Mitigation strategies
Below are a few mitigation strategies to consider when it comes to avoiding Mass Assignment vulnerabilities.
Avoid re-using data models for request models
It's important to keep your data models (which may be persisted in a database) separate from the models that get used when communicating with a client. Handling a form submission to a controller is a very different concern from persisting data in a database and how it's represented in the database. This creates a far higher level of coupling between the frontend and the persistence layer than is good.
Be explicit in your mappings
The problem with automatic binding (or mapping) is that the lack of explicit mappings makes it easy to expose properties that aren’t meant to be accessible on the model. By being explicit in mappings between request models, and the rest of your backend, you can prevent these types of exposures from the start.
This could be done by using different models for requests and data. This doesn't prevent you from using an automatic mapper between the request and data model, as your request model should not expose properties that are not allowed for the specific request.
Further Examples
Below, we’ve got some additional examples in different languages of what this can look like.
C# - insecure
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# - secure
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# - alternative - excludes parameters
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 - insecure
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 - secure
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 - insecure
app.get('/user/update', (req, res) => {
var user = req.user;
Object.assign(user, req.body);
UserService.Update(user);
return "User has been updated";
})
Javascript - secure
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 - Insecure
@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 - Secure
@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)