Mass Parameter Assignment

Overview

Many modern software development frameworks bind user input directly into internal objects. This is known as mass parameter assignment or auto-binding. While this feature is helpful to assign parameters to objects, an attacker can use it to write fields that should not be set by a user, leading to privilege escalation, data tampering, bypass of security mechanisms, and more.

For example, consider a Java Spring web application with a User object:

public class User {
    private String username;
    private String password;
    private String email;
    private boolean isAdmin;

    // Getters & Setters
}

The web application implements the following view:

<form action="/createUser" method="POST">
    <input name="username" type="text">
    <input name="password" type="text">
    <input name="email" text="text">
    <input type="submit" value="Create">
</form>

The controller that handles the creation request (Spring provides the automatic bind with the User model):

@RequestMapping(value = "/createUser", method = RequestMethod.POST)
public String createUser(User user) {
    userService.add(user);
    return "successPage";
}

In this case, the browser sends the following request when the form is submitted:

POST /createUser HTTP/1.1
...

username=bob&password=supersecretpassword&email=bob@domain.test

Due to the auto-binding, an attacker can add the isAdmin parameter to the request, which the controller will automatically bind to the model. As a result, the request below will create a user with administrative privileges.

POST /createUser HTTP/1.1
...

username=bob&password=supersecretpassword&email=bob@domain.test&isAdmin=true

This page contains recommendations for the implementation of protection against mass parameter assignment attacks.

General

  • Use Data Transfer Objects instead of binding parameters directly to internal objects.

Clarification

A Data Transfer Object (DTO) is an object which only contains attributes that can be modified by a user. It serves as an intermediary between received parameters and a final object. Using Data Transfer Objects makes mass parameter assignment or auto-binding completely safe because they do not contain additional parameters that should not be passed by a user.

For example, the UserRegistrationFormDTO below does not contain the isAdmin parameter that might be used to elevate privileges as it is described in the Overview section:

public class UserRegistrationFormDTO {
    private String userid;
    private String password;
    private String email;

    // NOTE: isAdmin field is not present

    // Getters & Setters
}
  • If there is no possibility to use DTOs, use an allow list and non-sensitive parameters to restrict auto-binding only to those. Avoid using block lists.

Example

In the example below, the binding function uses a set of allowed fields, restricting any other field to be auto-bound and preventing the mass parameter assignment attack.

@Controller
public class UserController
{
    @InitBinder
    public void initBinder(WebDataBinder binder, WebRequest request) {
        binder.setAllowedFields(["userid", "password", "email"]);
    }
    // ...
}
  • Implement comprehensive input validation for each request parameter, see the Input Validation page.

Auto-binding allow list implementation

Gin Web Framework allows mass parameter assignment with functions such as Bind(), shouldBind() or mustBind(). These functions rely on the form: tag in a struct and only attributes tagged with form: will be considered safe for auto-binding. Use the form tag to mark parameters that are safe to be auto-bound.

For example, the User struct below does not have the form: tag for the IsAdmin parameter. It means that IsAdmin can not be auto-bound and used to elevate privileges as it is described in the Overview section:

package main

import (
    "log"
    "github.com/gin-gonic/gin"
)

type User struct {
    Username    string  `form:"username"`
    Password    string  `form:"password"`
    Email       string  `form:"email"`
    IsAdmin     bool    `default:false`
}

func main() {
    route := gin.Default()
    route.POST("/createUser", createUser)
    route.Run()
}

func createUser(c *gin.Context) {
    var user User
    if c.ShouldBind(&user) == nil {
        log.Println(user.Username)
        log.Println(user.Password)
        log.Println(user.Email)
        log.Println(user.IsAdmin)
    }
    c.String(200, "Success")
}

References

Last updated