Guidelines

Mass Assignment

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)