准则

大规模派送

现在,我们将讨论 "批量分配 "漏洞及其表现形式,以及避免这些漏洞的几种方法。首先,简单回顾一下:

"批量分配 "是一种漏洞,在这种漏洞中,API 端点没有限制用户可以修改相关对象的哪些属性。 

当使用允许将 HTTP 参数自动绑定到模型上的库/框架时,可能会出现此漏洞。 

使用从请求到对象的自动绑定有时会非常有用,但如果模型具有用户无法访问的属性,也会导致安全问题。

示例

我们将以用户可以更改姓名、电子邮件地址等详细信息的网页为例进行说明。我们的用户模型定义如下

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;    
}

从这里我们可以假定,"UserService.UpdateUser "方法不会进行任何进一步的授权验证,而只是简单地保存所提供的用户对象。 

(如果没有为某个属性提供值,则会保留现有值) 

这就意味着,用户可以提交 "IsAdmin "请求,这样就可以覆盖当前值,使用户成为管理员:

<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>  

缓解战略

以下是一些避免大规模分配漏洞的缓解策略。

避免为请求模型重复使用数据模型

重要的是,要将数据模型(可能持久化在数据库中)与与客户端通信时使用的模型分开。处理向控制器提交的表单与在数据库中持久化数据以及如何在数据库中表示数据是完全不同的事情。这使得前端和持久层之间的耦合度大大提高。 

明确映射

自动绑定(或映射)的问题在于,由于缺乏显式映射,很容易暴露出模型上本不可访问的属性。通过在请求模型和后端其他部分之间建立显式映射,可以从一开始就防止这些类型的暴露。

这可以通过使用不同的请求模型和数据模型来实现。这并不妨碍在请求模型和数据模型之间使用自动映射器,因为请求模型不应暴露特定请求不允许使用的属性。

更多实例

下面,我们用不同的语言举例说明。 

C# - 不安全

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# - 安全

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# - 替代 - 排除参数

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 - 不安全

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 - 安全

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 - 不安全

app.get('/user/update',  (req, res) => {
    var user = req.user;

    Object.assign(user, req.body);

    UserService.Update(user); 

    return "User has been updated";
})

Javascript - 安全

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 - 不安全

@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 - 安全

@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)