マスアサインメント
ここでは、マスアサインメントの脆弱性とはどのようなものか、そしてそれを回避するためのいくつかの方法について解説します。まずは、要点の振り返りです。
マスアサインメントとは、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; }
}
フロントエンド側では、以下のようにフォームを定義します。`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("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)
実践的なセキュアコーディング
OWASP Top 10のようなソフトウェアの脆弱性を理解し軽減するためのセキュアコーディングガイドラインを確認しましょう。また、実践的なアプリシミュレーションを通じて学べるトレーニングミッションにもぜひ挑戦してください。