質量の割り当て
今、私たちはMass Assignmentの脆弱性とそれがどのようなものか、そしてそれらを回避するためのいくつかの方法について説明します。
Mass Assignmentは、APIエンドポイントが、関連するオブジェクトのどのプロパティをユーザが変更できるかを制限しない脆弱性である。
この脆弱性は、HTTPパラメータをモデルに自動的にバインドすることを可能にするライブラリ/フレームワークを使用する場合に発生する可能性があります。
リクエストからオブジェクトへの自動バインディングの使用は、時には非常に便利ですが、モデルにユーザーがアクセスできないプロパティがある場合、セキュリティの問題につながることもあります。
例
ここでは、ユーザが名前、メールアドレスなどの詳細を変更できるWebページを例にします。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; }
}
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>
緩和戦略
以下は、Mass Assignmentの脆弱性を回避するために考慮すべきいくつかの緩和策です。
リクエストモデルのデータモデルの再利用を避ける
データモデル(データベースに永続化されるかもしれない)を、クライアントと通信するときに使用されるモデルから分離しておくことは重要です。コントローラへのフォーム送信を処理することは、データベースにデータを永続化することと、それがデータベースでどのように表現されるかということとは、まったく異なる問題です。これは、フロントエンドと永続化レイヤーの間に、良いレベルよりもはるかに高いレベルの結合を生み出します。
マッピングを明確にする
自動バインディング(またはマッピング)の問題点は、明示的なマッピングがないために、モデル上でアクセスできるはずのないプロパティが簡単に公開されてしまうことです。リクエストモデルとバックエンドの間のマッピングを明示的に行うことで、このような種類の公開を最初から防ぐことができます。
これは、リクエストとデータに異なるモデルを使うことで実現できます。リクエストモデルは特定のリクエストで許可されていないプロパティを公開すべきではありません。
その他の例
以下に、これがどのように見えるかについて、さまざまな言語での例を挙げる。
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";
}
ジャバスクリプト - 安全ではない
app.get('/user/update', (req, res) => {
var user = req.user;
Object.assign(user, req.body);
UserService.Update(user);
return "User has been updated";
})
ジャバスクリプト - 安全
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";
})
パイソン - 安全ではない
@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 - セキュア