가이드라인

질량 할당

지금부터 대량 할당 취약점과 취약점이 어떻게 생겼는지, 그리고 이를 방지할 수 있는 몇 가지 방법을 살펴보겠습니다. 먼저, 간단히 요약하면

대량 할당은 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";
}

↪f_200D↩
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";
})

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)