Mass Assignment

Many web-frameworks automate the process of assigning parameters from an incoming HTTP request to the fields on an in-memory object. You need to ensure that you are using assignment logic in your code in such a way that only permitted fields are written to.

Risks

Prevalence Occasional
Exploitability Easy
Impact Harmful

Mass assignment vulnerabilities allow an attacker update state they should not be permitted to change, which is often an easy way to escalate privileges.

Anatomy of a Mass Assignment Attack

A common design pattern is to take data from an HTTP request - either the HTTP parameters or from JSON in the body of the request - and to update the contents of an object in-memory or in the database. Using modern web-frameworks, it is easy to write code that takes untrusted input and allows an attacker to overwrite unintended properties.

For example, consider the following code samples that allow a user to update their profile details in the database:

# This example shows how to update a Django model object from JSON in 
# an HTTP request. Using the setattr(...) function makes it easy to 
# create a mass assignment vulnerability.
class User(models.Model):
  ROLES = (
    ('G', 'Guest'),
    ('U', 'User'),
    ('A', 'Admin'),
    ('S', 'Superuser')
  )

  id       = models.IntegerField()
  username = models.CharField(max_length=100)
  email    = models.CharField(max_length=100)
  password = models.CharField(max_length=100)
  role     = models.CharField(max_length=1, choices=ROLES)
  updated  = models.DateTimeField()

def profile(request):
  """View or update the user's profile."""  
  user = get_current_user()

  if user is None:
    return HttpResponseRedirect('/login')

  if request.method == 'POST':
    data = json.loads(request.body)

    # Danger: updating arbitrary fields on the user object 
    # from incoming JSON is dangerous!  
    for key, value in data.items():
      setattr(user, key, value)

    user.updated = datetime.now()
    user.save()
    
    return HttpResponse("Profile updated", 202)

  elif request.method == 'GET':
    return render(request, 'profile.html', { 'user': user })

  return HttpResponseNotAllowed(('GET', 'POST'))
app.post('/profile', (request, response) => {

  // Auto-generating the UPDATE statement from the request parameters 
  // is very dangerous.
  const columns = [], values  = []

  Object.keys(request.body).forEach(name => {
    columns.push(name + ' = ?')
    values.push(request.body[name])
  })

  values.push(request.session.user)

  /**
   * An attacker can update their permissions by running a statement like:
   *   UPDATE users SET is_admin WHERE username = 'attacker@email.com'
   */
  db.run(`UPDATE users SET ${columns.join(',')} WHERE username = ?`, values, error => {
    response.redirect('/profile')
  })
})
class PeopleController < ActionController::Base
  
  # Danger: this will create a Person object with any parameters an 
  # attacker wishes to pass.
  def create
    Person.create(params[:person])
  end

  # Danger: this allows an attack update a parameters on any Person 
  # they please.
  def update
    redirect_to current_account.people.find(params[:id]).tap { |person|
      person.update!(params[:person])
    }
  end
end
@Controller
@RequestMapping("/profile")
public class UserController 
{
    /**
     * Using auto-binding in the Spring framework without enumerating 
     * the fields to set is asking for trouble!
     */
    @PostMapping
    public String update(@RequestBody User user)
    {
        saveUser(user);

        return "redirect:/profile";
    }

    private void saveUser(User user) {
        getDatabase().updateUser(user);
    }
}
[HttpPost]
public IActionResult UpdateUsername(UserModel user)
{
    // Allowing *any* attribute on the UserModel to be updated 
    // is dangerous.
    userManager.Update(user);
    
    return View("Profile", user);
}

Mitigation

These examples are vulnerable to a mass assignment attack since the properties are not being specifically enumerated in the server-side code. An attacker can simply modify the names of the form fields (or add extra) and directly manipulate their profile in the database, setting administrative flags as they see fit.

When taking data from an HTTP request, the properties of the data-object being updated must be explicitly stated in server-side code:

  if request.method == 'POST':
    data = json.loads(request.body)

    # Explicitly enumerating the fields to be updated protects us 
    # from mass-assignment. 
    user.email    = data.get('email',    user.email)
    user.username = data.get('username', user.username)

    user.updated = datetime.now()
    user.save()

    return HttpResponse("Profile updated", 202) 
app.post('/profile', (request, response) => {

  // Explicitly enumerating the columns to update in the database  
  // protects us from mass assignment.
  const values = [
    request.body.name,
    request.body.location,
    request.body.employer,
    request.session.user
  ]

  db.run(`UPDATE USERS SET name = ?, location = ?, employer = ? WHERE username = ?`, 
    values, error => {
    response.redirect('/profile')
  })
})

Use Strong Parameters to safely assign values from the HTTP request to an ActiveModel object:

class PeopleController < ActionController::Base
  
  # Using "Person.create(params[:person])" would raise an
  # ActiveModel::ForbiddenAttributesError exception because it'd
  # be using mass assignment without an explicit permit step.
  # This is the recommended form:
  def create
    Person.create(person_params)
  end

  # This will pass with flying colors as long as there's a person key in the
  # parameters, otherwise it'll raise an ActionController::ParameterMissing
  # exception, which will get caught by ActionController::Base and turned
  # into a 400 Bad Request reply.
  def update
    redirect_to current_account.people.find(params[:id]).tap { |person|
      person.update!(person_params)
    }
  end

  private
    # Using a private method to encapsulate the permissible parameters is
    # a good pattern since you'll be able to reuse the same permit
    # list between create and update. Also, you can specialize this method
    # with per-user checking of permissible attributes.
    def person_params
      params.require(:person).permit(:name, :age)
    end
end
@Controller
@RequestMapping("/profile")
public class UserController 
{
    @InitBinder
    public void initBinder(WebDataBinder binder, WebRequest request)
    {
        // Explicitly enumerate which fields can be set during data binding.
        // Note that 'isAdmin' is *not* included.
        binder.setAllowedFields("password", "address", "phone");
    }
    
    @PostMapping
    public String update(@RequestBody User user)
    {
        saveUser(user);

        return "redirect:/profile";
    }

    private void saveUser(User user) {
        getDatabase().updateUser(user);
    }
}
// Only allow the "Name" field to be bound.
public IActionResult UpdateUsername([Bind(nameof(UserModel.Name))] UserModel user)
{
    userManager.Update(user);
    
    return View("Profile", user);
}
// Never bind the "IsAdmin" field.
public class UserModel
{
    public string Name { get; set; }

    [BindNever]
    public bool IsAdmin { get; set; }
}

Further Reading