본문은
ASP.net의 공식 MVC 관련 자습서인
Preventing Open Redirection Attacks (C#)을 편역한 글입니다.
마이크로소프트의 공식 번역 문서가 아니며 태오 사이트 MS 컬럼 번역팀에서 번역한 내용입니다.
그렇기에, 일부 오역이나 오타가 존재할 수 있는 점 미리 양해를 드립니다.
원문에 대한 모든 저작권은 마이크로소프트에 있으며, 컬럼 내용과 관련한 질의 문답 역시 원문 사이트에 문의하시는 것을 추천드립니다.
공개된 재전송 공격(Open Redirection Attack) 방어하기 (C#)
본문에서는 ASP.NET MVC 응용 프로그램에 대한 공개된 재전송 공격을 방어하는 방법에 관해서 알아봅니다.
더불어, ASP.NET MVC 3에서 개선된 AccountController를 살펴보고, 이 개선된 기능을 수작업으로 기존의 ASP.NET MVC 1.0과 2.0 응용 프로그램에 적용할 수 있는 방법을 알아보도록 하겠습니다.
공개된 재전송 공격이란?
쿼리스트링이나 폼 데이터 등을 통해서 지정된 특정 URL로 재전송을 수행하는 모든 웹 응용 프로그램은, 외부의 악의적인 URL로 사용자들이 재전송 될 수 있는 변조 가능성을 잠재적으로 갖고 있습니다.
그리고, 바로 이런 형태의 변조 행위를 공개된 재전송 공격(Open Redirection Attack)이라고 합니다.
기본적으로 응용 프로그램 로직이 지정된 URL로 재전송을 수행할 때는 반드시 재전송 URL이 변조되었는지 여부를 확인해야만 합니다.
ASP.NET MVC 1.0과 ASP.NET MVC 2.0에서 사용되는 기본 AccountController의 로그인은 공개된 재전송 공격에 취약합니다.
그러나, 다행스럽게도 ASP.NET MVC 3 프리뷰(원문 작성 당시의 최신 버전)에 적용된 개선된 기능을 기존 응용 프로그램에도 손쉽게 적용할 수 있습니다.
먼저, 해당 취약점에 대한 이해를 돕기 위해서 ASP.NET MVC 2.0 웹 응용 프로그램 프로젝트의 기본 로그인 재전송이 동작하는 방식을 살펴보도록 하겠습니다.
이 버전에서는 인증받지 않은 사용자가 [Authorize] 어트리뷰트가 적용된 컨트롤러 액션에 접근을 시도하면 /Account/LogOn 뷰로 재전송됩니다.
이 /Account/LogOn에 대한 재전송에는 returnUrl라는 이름의 쿼리스트링 매개변수가 포함되어 있는데, 사용자는 정상적으로 로그인 한 뒤에 이 매개변수를 이용해서 최초에 요청했던 URL로 되돌아가게 됩니다.
다음 스크린샷에서는 사용자가 로그인하지 않은 상태에서 /Account/ChangePassword 뷰에 접근을 시도한 결과, /Account/LogOn?ReturnUrl=%2fAccount%2fChangePassword%2f로 재전송 된 모습을 확인할 수 있습니다.
출처 : http://i2.asp.net/asp.net/images/whitepapers/security/Login%20Screen.png
그림 01: 공개된 재전송이 포함된 로그인 페이지
그런데, 이 때 ReturnUrl 쿼리스트링 매개변수에 대한 검증이 전혀 이루어지지 않으므로, 악의적인 공격자가 공개된 재전송 공격을 시도하는 URL 주소를 이 매개변수에 마음대로 주입할 수 있습니다.
가령, ReturnUrl 매개변수의 값을 http://bing.com/으로 변경해서, 로그인 URL을 /Account/LogOn?ReturnUrl=http://www.bing.com/으로 조정할 수도 있는 것입니다.
그 결과, 사이트에 정상적으로 로그인하고 나면 http://bing.com/으로 재전송됩니다.
이 때 재전송은 전혀 검증되지 않을 뿐더러 사용자를 속이려고 시도하는 악의적인 사이트를 가르킬 수도 있습니다.
보다 복잡한 공개된 재전송 공격
이와 같은 공개된 재전송 공격이 특히 위험한 이유는 사용자가 특정 웹사이트에 로그인하려고 한다는 점을 공격자가 쉽게 짐작할 수 있어서 피싱 공격에 취약하기 때문입니다.
가령, 공격자가 특정 웹사이트의 사용자들에게 악의적인 전자우편을 전송해서 비밀번호를 가로채려고 시도할 수도 있습니다.
이런 공격이 실제로 시도되는 모습을 NerdDinner 사이트를 통해서 살펴보도록 하겠습니다.
(물론, 실제 NerdDinner 사이트는 이미 공개된 재전송 공격에 대응할 수 있도록 보완되어 있습니다.)
먼저, 공격자가 위조된 페이지로 재전송되도록 조작된 NerdDinner 사이트의 로그인 페이지 링크를 전자우편으로 보냅니다:
http://nerddinner.com/Account/LogOn?returnUrl=http://nerddiner.com/Account/LogOn
이 URL에서 재전송 될 returnUrl 쿼리스트링 매개변수의 URL이 nerddiner.com이라는 점에 주의하시기 바랍니다.
즉, dinner라는 단어에서 "n"이 하나 빠진 전혀 엉뚱한 도메인인 것입니다.
바로 이 도메인이 공격자가 관리하는 도메인이라고 가정해보겠습니다.
사용자들이 이 링크에 접근하면 일단은 적절한 NerdDinner.com 로그인 페이지로 이동하게 됩니다.
출처 : http://i2.asp.net/asp.net/images/whitepapers/security/NerdDinner%20Login%20Screen.png
그림 02: 공개된 재전송이 포함된 NerdDinner 로그인 페이지
이 페이지에서 정상적으로 로그인을 하고 나면, ASP.NET MVC AccountController의 LogOn 액션이 returnUrl 쿼리스트링 매개변수에 지정되어 있는 URL로 사용자를 재전송시키게 됩니다.
그런데, 이 예제에서 해당 URL은 공격자가 입력한 http://nerddiner.com/Account/LogOn입니다.
공격자가 공을 들여서 이 위조된 페이지의 외관을 실제 로그인 페이지와 매우 유사한 모습으로 꾸며 놓는다면, 어지간히 주의 깊은 사용자가 아닌 바에야 눈치채지 못할 가능성이 매우 높습니다.
이 위조된 페이지는 다음과 같이 다시 한 번 로그인을 하도록 유도하는 오류 메시지를 출력할 것입니다.
마치 사용자가 실수로 비밀번호를 잘못 입력한 것처럼 말입니다.
출처 : http://i2.asp.net/asp.net/images/whitepapers/security/NerdDinner%20-%20Forged%20Login%20Screen.png
그림 03: 위조된 NerdDinner 로그인 화면
그 사실을 눈치채지 못한 사용자가 사용자 이름과 비밀번호를 다시 입력하면, 그 정보를 위조된 로그인 페이지에서 저장한 다음, 다시 사용자를 실제 NerdDinner.com 사이트로 돌려보내는 것입니다.
이 시점에 해당 사용자는 이미 NerdDinner.com 사이트에 로그인 되어 있는 상태므로, 위조된 로그인 페이지는 사용자를 특정 페이지로 마음대로 재전송시킬 수 있습니다.
결과적으로 사용자는 본인도 모르는 사이에 공격자에게 사용자 이름 및 비밀번호를 제공하게 되는 것입니다.
AccountController LogOn 액션 코드 취약성 검토
ASP.NET MVC 2.0 응용 프로그램에서 제공되는 LogOn 액션의 코드는 다음 목록과 같습니다.
이 컨트롤러 코드에서 정상적인 로그인이 이루어지고 난 뒤, returnUrl로 재전송을 수행하는 부분을 주의해서 살펴보시기 바랍니다.
이 때, returnUrl 매개변수에 대한 어떠한 검증도 이루어지지 않고 있다는 사실을 확인할 수 있습니다.
목록 1 - ASP.NET MVC 2.0에서 제공되는 AccountController.cs의 LogOn 액션
[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
if (ModelState.IsValid)
{
if (MembershipService.ValidateUser(model.UserName, model.Password))
{
FormsService.SignIn(model.UserName, model.RememberMe);
if (!String.IsNullOrEmpty(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
}
// 코드가 여기까지 도달하면 문제가 발생한 것이므로 폼을 다시 출력합니다.
return View(model);
}
그러면 이번에는 ASP.NET MVC 3의 LogOn 액션에서 개선된 부분을 살펴보도록 하겠습니다.
목록 2 - ASP.NET MVC 3에서 제공되는 AccountController.cs의 LogOn 액션
[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
if (ModelState.IsValid)
{
if (MembershipService.ValidateUser(model.UserName, model.Password))
{
FormsService.SignIn(model.UserName, model.RememberMe);
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
}
// 코드가 여기까지 도달하면 문제가 발생한 것이므로 폼을 다시 출력합니다.
return View(model);
}
이 코드를 살펴보면 URL 매개변수를 검증하기 위해서 System.Web.Mvc.Url 도우미 클래스에서 제공되는 새로운 IsLocalUrl()
메서드를 호출하고 있는 것을 확인할 수 있습니다.
ASP.NET MVC 1.0 및 ASP.NET MVC 2.0 응용 프로그램 보호하기
기존의 ASP.NET MVC 1.0 및 2.0 응용 프로그램에서도 직접 IsLocalUrl() 도우미 메서드를 추가하고 returnUrl 매개변수를 검증하도록 LogOn 액션을 수정하면, ASP.NET MVC 3의 개선 사항을 적용할 수 있습니다.
알고보면 UrlHelper 클래스의 IsLocalUrl() 메서드는 ASP.NET 웹 페이지 응용 프로그램에서 유효성 검사에 사용되는 System.Web.WebPages.RequestExtensions의 IsUrlLocalToHost 메서드를 호출할 뿐입니다.
목록 3 - ASP.NET MVC 3 UrlHelper 클래스의 IsLocalUrl() 메서드
public bool IsLocalUrl(string url) {
return System.Web.WebPages.RequestExtensions.IsUrlLocalToHost(
RequestContext.HttpContext.Request, url);
}
목록 4에서 볼 수 있는 것처럼 실제적인 유효성 검사 로직은 IsUrlLocalToHost 메서드에 존재합니다.
목록 4 - System.Web.WebPages.RequestExtensions 클래스의 IsUrlLocalToHost() 메서드
public static bool IsUrlLocalToHost(this HttpRequestBase request, string url) {
if (url.IsEmpty())
{
return false;
}
Uri absoluteUri;
if (Uri.TryCreate(url, UriKind.Absolute, out absoluteUri)) {
return String.Equals(request.Url.Host, absoluteUri.Host, StringComparison.OrdinalIgnoreCase);
}
else {
bool isLocal = !url.StartsWith("http:", StringComparison.OrdinalIgnoreCase)
&& !url.StartsWith("https:", StringComparison.OrdinalIgnoreCase)
&& Uri.IsWellFormedUriString(url, UriKind.Relative);
return isLocal;
}
}
본문에서는 ASP.NET MVC 1.0 또는 2.0 응용 프로그램의 AccountController 내부에 직접 IsLocalUrl() 메서드를 작성해서 추가해 볼 것입니다.
그러나, 가능하다면 여러분은 별도의 도우미 클래스에 메서드를 추가하시기 바랍니다.
그 방식이 더 바람직하기 때문입니다.
이번 예제에서는 ASP.NET MVC 3 버전의 IsLocalUrl() 메서드에서 두 가지 부분을 변경해서 AccountController 내부에서 동작하도록 만들어야 합니다.
먼저, 첫 번째로 메서드를 public 메서드가 아닌 private 메서드로 변경해야 하는데, 그 이유는 컨트롤러에 존재하는 public 메서드는 컨트롤러 액션으로서 접근이 가능하므로 이를 방지해야 하기 때문입니다.
두 번째로 URL의 호스트와 응용 프로그램의 호스트가 일치하는지를 검사하기 위해서 IsUrlLocalToHost() 메서드를 호출하는 부분을 변경해야 합니다.
즉, UrlHelper 클래스 내부에서 IsUrlLocalToHost() 메서드를 호출할 때, this.RequestContext.HttpContext.Request.Url.Host 대신 지역 RequestContext 필드인 this.Request.Url.Host를 사용합니다.
다음 코드는 ASP.NET MVC 1.0 및 2.0 응용 프로그램의 컨트롤러 클래스에서 사용될 수정된 IsLocalUrl() 메서드를 보여줍니다.
목록 5 - MVC 컨트롤러 클래스 내부에서 사용하기 위해 수정된 IsLocalUrl() 메서드
//노트: 이 메서드는 System.Web.WebPages.RequestExtensions 클래스로부터 복사되었습니다.
private bool IsLocalUrl(string url) {
if (string.IsNullOrEmpty(url))
{
return false;
}
Uri absoluteUri;
if (Uri.TryCreate(url, UriKind.Absolute, out absoluteUri))
{
return String.Equals(this.Request.Url.Host, absoluteUri.Host, StringComparison.OrdinalIgnoreCase);
}
else
{
bool isLocal = !url.StartsWith("http:", StringComparison.OrdinalIgnoreCase)
&& !url.StartsWith("https:", StringComparison.OrdinalIgnoreCase)
&& Uri.IsWellFormedUriString(url, UriKind.Relative);
return isLocal;
}
}
이제 IsLocalUrl() 메서드가 준비되었으므로, 다음 코드에서 볼 수 있는 것처럼, returnUrl 매개변수를 검사하기 위해 LogOn 액션에서 호출할 수 있습니다.
목록 6 - returnUrl 매개변수를 검사하기 위해 변경된 LogOn 메서드
[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
if (ModelState.IsValid)
{
if (MembershipService.ValidateUser(model.UserName, model.Password))
{
FormsService.SignIn(model.UserName, model.RememberMe);
if (IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
}
}
모든 작업이 끝났습니다.
이제 외부 URL을 지정한 로그인을 시도해서 공개된 재전송 공격을 테스트해 볼 수 있습니다.
다시 /Account/LogOn?ReturnUrl=http://www.bing.com/을 입력해봅니다.
출처 : http://i2.asp.net/asp.net/images/whitepapers/security/Corrected%20Login%20Screen.png
그림 04: 개선된 LogOn 액션 테스트
정상적으로 로그인이 되고 나면, 지정한 외부 URL이 아닌 Home/Index 컨트롤러 액션으로 재전송됩니다.
출처 : http://i2.asp.net/asp.net/images/whitepapers/security/Validated%20Login.png
그림 05: 방어된 공개된 재전송 공격
요약
공개된 재전송 공격은 URL을 통해서 재전송될 URL이 응용 프로그램에 매개변수로 전달되는 경우 발생하기 쉽습니다.
ASP.NET MVC 3 템플릿에는 공개된 재전송 공격을 방지하기 위한 코드가 포함되어 있습니다.
이 코드를 약간 수정해서 ASP.NET MVC 1.0 및 2.0 응용 프로그램에 적용할 수 있습니다.
ASP.NET MVC 1.0 및 2.0 응용 프로그램에서 공개된 재전송 공격을 방지하기 위해서는, IsLocalUrl() 메서드를 추가하고 LogOn 액션에서 returnUrl 매개변수를 검사하면 됩니다.
宋