공개된 재전송 공격(Open Redirection Attack) 방어하기 (C#)

등록일시: 2013-01-15 16:55,  수정일시: 2013-09-09 17:18
조회수: 7,882
이 문서는 ASP.NET MVC 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.

본 자습서에서는 여러분의 ASP.NET MVC 응용 프로그램에서 공개된 재전송 공격을 방지할 수 있는 방법을 살펴봅니다. 그리고, ASP.NET MVC 3에서 AccountController가 어떻게 변경되었는지 설명하고, 이 변경 사항들을 기존의 ASP.NET MVC 1.0 및 2 응용 프로그램에 적용할 수 있는 방법을 보여줍니다.

공개된 재전송 공격이란?

쿼리스트링이나 폼 데이터 등을 통해서 지정된 특정 URL로 재전송을 수행하는 모든 웹 응용 프로그램들은, 잠재적으로 사용자들이 외부의 악의적인 URL로 재전송 될 수 있는 변조 가능성을 갖고 있습니다. 바로 이런 변조 행위를 공개된 재전송 공격(Open Redirection Attack)이라고 합니다.

응용 프로그램의 로직이 지정된 URL을 재전송할 때는 언제나, 재전송 URL이 변조되었는지 여부를 반드시 확인해야만 합니다. ASP.NET MVC 1.0 및 ASP.NET MVC 2에서 사용되는 기본 AccountController의 로그인은 공개된 재전송 공격에 취약합니다. 다행스럽게도, ASP.NET MVC 3 프리뷰에 적용된 보완방식을 손쉽게 기존 응용 프로그램에 적용할 수 있습니다.

먼저, 취약점에 대한 이해를 돕기 위해, 기본 ASP.NET MVC 2 웹 응용 프로그램 프로젝트의 로그인 재전송이 동작하는 방식을 살펴보도록 하겠습니다. 이 응용 프로그램에서는 인증받지 않은 사용자가 [Authorize] 어트리뷰트가 적용된 컨트롤러 액션에 접근하려고 시도하는 경우, /Account/LogOn 뷰로 재전송됩니다. 이 /Account/LogOn에 대한 재전송에는 returnUrl라는 이름의 쿼리스트링 매개변수가 포함되어 있으며, 정상적으로 로그인이 된 후에 이 매개변수를 이용해서 사용자들은 최초에 요청했던 URL로 되돌아가게 됩니다.

다음 스크린샷에서는, 로그인하지 않은 상태에서 /Account/ChangePassword 뷰에 접근을 시도한 결과, /Account/LogOn?ReturnUrl=%2fAccount%2fChangePassword%2f로 재전송 된 것을 확인할 수 있습니다.

그림 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

이 때, 재전송될 returnUrl 쿼리스트링 매개변수의 url이 nerddiner.com이라는 점에 주의하시기 바랍니다. 즉, dinner라는 단어에서 "n"이 하나 빠진 전혀 엉뚱한 도메인인 것입니다. 이번 예제에서 이 도메인은 공격자가 관리하는 도메인인 것으로 가정해보겠습니다. 사용자들이 이 링크에 접근하면, 일단은 적절한 NerdDinner.com 로그인 페이지로 이동하게 됩니다.

그림 02: 공개된 재전송을 포함하고 있는 NerdDinner의 로그인 페이지

정상적으로 로그인을 마치고 나면, ASP.NET MVC AccountController의 LogOn 액션은 returnUrl 쿼리스트링 매개변수에 지정되어 있는 URL로 사용자들을 재전송시키게 됩니다. 그런데, 이 예제에서 해당 URL은 공격자가 입력한 http://nerddiner.com/Account/LogOn입니다. 공격자가 공을 들여서 이 위조된 페이지의 외관을 실제 로그인 페이지와 매우 비슷한 모습으로 꾸며 놓는다면, 어지간히 주의 깊은 사용자가 아닌 바에야 눈치채지 못할 가능성이 매우 높습니다. 이 위조된 페이지는 다시 한 번 로그인을 하도록 유도하는 오류 메시지를 출력할 것입니다. 마치 사용자가 실수로 비밀번호를 잘못 입력한 것처럼 말입니다.

그림 03: 위조된 NerdDinner 로그인 화면

그에 따라, 사용자 이름과 비밀번호를 다시 입력하면, 위조된 로그인 페이지가 이 정보를 저장한 다음, 다시 사용자를 실제 NerdDinner.com 사이트로 돌려보내 버리는 것입니다. 이 시점에 해당 사용자는 이미 NerdDinner.com 사이트에 로그인 되어 있는 상태이므로, 위조된 로그인 페이지는 사용자를 특정 페이지로 마음대로 재전송시킬 수 있습니다. 결국 사용자는 본인도 모르는 사이에 공격자에게 사용자 이름과 비밀번호를 제공하게 되는 것입니다.

AccountController LogOn 액션의 취약성 코드 살펴보기

ASP.NET MVC 2 응용 프로그램에서 제공되는 LogOn 액션의 코드는 다음과 같습니다. 정상적인 로그인이 이루어지고 나면, 컨트롤러가 returnUrl로 재전송을 수행한다는 점에 주의하시기 바랍니다. 그리고, 이 코드에서 returnUrl 매개변수에 대한 어떠한 검증도 이루어지지 않고 있는 것을 확인할 수 있습니다.

목록 1 - ASP.NET MVC 2에서 제공되는 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 및 MVC 2 응용 프로그램 보호하기

기존의 ASP.NET MVC 1.0 및 2 응용 프로그램에서도 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; 
    } 
}

본문에서는 IsLocalUrl() 메서드를 ASP.NET MVC 1.0 또는 2 응용 프로그램의 AccountController에 추가하고 있지만, 가능하다면 여러분은 이 메서드를 별도의 도우미 클래스에 추가하는 것이 바람직합니다. 여기에서는 ASP.NET MVC 3 버전의 IsLocalUrl() 메서드에서 두 가지 부분을 변경해서 AccountController 내부에서 동작하도록 만들어 볼 것입니다. 첫 번째로 메서드를 public 메서드에서 private 메서드로 변경해야 하는데, 그 이유는 컨트롤러에 존재하는 public 메서드는 컨트롤러 액션으로 접근이 가능하기 때문입니다. 두 번째로 URL의 호스트가 응용 프로그램의 호스트와 일치하는지 검사하기 위해 메서드를 호출하는 부분을 변경됩니다. 메서드를 호출할 때, UrlHelper 클래스의 지역 RequestContext 필드를 이용합니다. 즉, this.RequestContext.HttpContext.Request.Url.Host 대신 this.Request.Url.Host를 이용한다는 뜻입니다. 다음 코드는 ASP.NET MVC 1.0 및 2 응용 프로그램의 컨트롤러 클래스에서 사용될 수정된 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/을 입력해봅니다.

그림 04: 변경된 LogOn 액션 테스트

정상적으로 로그인이 되고 나면, 지정한 외부 URL이 아닌 Home/Index 컨트롤러 액션으로 재전송됩니다.

그림 05: 방지된 공개된 재전송 공격

요약

공개된 재전송 공격은 재전송될 URL이 URL을 통해서 응용 프로그램에 매개변수로 전달되는 경우 발생하기 쉽습니다. ASP.NET MVC 3 템플릿에는 공개된 재전송 공격을 방지하기 위한 코드가 포함되어 있습니다. 이 코드를 약간 수정해서 ASP.NET MVC 1.0 및 2 응용 프로그램에 적용할 수 있습니다. ASP.NET MVC 1.0 및 2 응용 프로그램에서 공개된 재전송 공격을 방지하기 위해서는, IsLocalUrl() 메서드를 추가하고 LogOn 액션에서 returnUrl 매개변수를 검사하면 됩니다.