대량 할당
지금부터 대량 할당 취약점이 무엇인지, 어떤 형태로 나타나는지, 그리고 이를 방지하는 몇 가지 방법에 대해 알아보겠습니다. 먼저 간단히 요약하자면 다음과 같습니다.
대량 할당은 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과 같은 소프트웨어 취약점을 이해하고 완화하기 위한 보안 코딩 가이드라인을 살펴보고, 실제 앱 시뮬레이션을 통해 실습할 수 있는 가이드형 교육 미션을 시작해 보세요.