批量赋值
现在,我们将深入探讨批量赋值漏洞的表现形式,并介绍几种规避方法。首先,快速回顾一下:
批量赋值是一种漏洞,其产生原因是 API 端点未限制用户可修改关联对象的哪些属性。
当使用某些库或框架自动将 HTTP 参数绑定到模型,且后续未进行任何验证就直接使用该模型时,就可能出现此漏洞。
虽然将请求自动绑定到对象在某些情况下非常方便,但如果模型中包含不应向用户公开的属性,这也会引发安全问题。
示例
我们以一个允许用户更改姓名、电子邮件地址等信息的网页为例。假设我们定义了如下 User 模型:
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; }
}
前端表单定义如下。请注意,其中并未包含 `IsAdmin` 值:
<form method="POST">
<input name="Id" type="hidden">
<input name="Name" type="text">
<input name="EmailAddress" type="text">
<input type="submit">
</form>
控制器定义的端点如下。通过将 `UserModel` 作为参数,框架会自动将相应的属性映射到该模型中:
[HttpPost]
public bool UpdateUser(UserModel model)
{
// 确保用户只能更新自己的信息
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)
{
// 直接保存用户提供的对象
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("禁止修改其他用户信息");
}
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 "用户已更新";
})
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 "用户已更新";
})
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)
实用安全编码
探索安全编码指南,了解并缓解 OWASP Top 10 等软件漏洞,并通过引导式训练任务在真实的应用程序模拟中进行实践。