열린 리디렉션 공격 방지(C#)

작성자 : Jon Galloway

이 자습서에서는 ASP.NET MVC 애플리케이션에서 열린 리디렉션 공격을 방지하는 방법을 설명합니다. 이 자습서에서는 ASP.NET MVC 3의 AccountController에서 변경된 내용을 설명하고 기존 ASP.NET MVC 1.0 및 2 애플리케이션에서 이러한 변경 내용을 적용하는 방법을 보여 줍니다.

공개 리디렉션 공격이란?

쿼리 문자열 또는 양식 데이터와 같은 요청을 통해 지정된 URL로 리디렉션되는 모든 웹 애플리케이션은 사용자를 외부 악성 URL로 리디렉션하기 위해 변조될 수 있습니다. 이 변조를 오픈 리디렉션 공격이라고 합니다.

애플리케이션 논리가 지정된 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으로의 이 리디렉션에는 사용자가 성공적으로 로그인한 후 원래 요청된 URL로 반환될 수 있도록 returnUrl querystring 매개 변수가 포함됩니다.

아래 스크린샷에서는 로그인하지 않은 경우 /Account/ChangePassword 보기에 액세스하려고 하면 /Account/LogOn으로 리디렉션되는 것을 볼 수 있습니다. ReturnUrl=%2fAccount%2fChangePassword%2f.

내 M V C 애플리케이션 로그온 페이지를 보여 주는 스크린샷. 제목 표시줄이 강조 표시됩니다.

그림 01: 열린 리디렉션이 있는 로그인 페이지

ReturnUrl querystring 매개 변수의 유효성이 검사되지 않았으므로 공격자는 매개 변수에 URL 주소를 삽입하도록 수정하여 열린 리디렉션 공격을 수행할 수 있습니다. 이를 설명하기 위해 ReturnUrl 매개 변수를 수정하여 https://bing.com결과 로그인 URL이 /Account/LogOn이 되도록 할 수 있습니다. ReturnUrl=https://www.bing.com/. 성공적으로 사이트에 로그인하면, https://bing.com로 리다이렉트됩니다. 이 리디렉션은 유효성이 검사되지 않으므로 사용자를 속이려는 악의적인 사이트를 대신 가리킬 수 있습니다.

보다 복잡한 오픈 리디렉션 공격

공개 리디렉션 공격은 특히 위험합니다. 공격자가 특정 웹 사이트에 로그인하려고 한다는 것을 알고 있기 때문에 피싱 공격에 취약해집니다. 예를 들어 공격자는 자신의 암호를 캡처하기 위해 웹 사이트 사용자에게 악성 이메일을 보낼 수 있습니다. NerdDinner 사이트에서 어떻게 작동하는지 살펴보겠습니다. (공개 리디렉션 공격으로부터 보호하기 위해 라이브 NerdDinner 사이트가 업데이트되었습니다.)

먼저 공격자가 위조된 페이지로의 리디렉션을 포함하는 NerdDinner의 로그인 페이지에 대한 링크를 보냅니다.

http://nerddinner.com/Account/LogOn?returnUrl=http://nerddiner.com/Account/LogOn

반환 URL은 nerddiner.com 가리키며, 저녁 식사라는 단어에서 "n"이 누락되었습니다. 이 예제에서는 공격자가 제어하는 도메인입니다. 위의 링크에 액세스하면 합법적인 NerdDinner.com 로그인 페이지로 이동됩니다.

Nerd Dinner dot com 홈페이지를 보여주는 스크린샷. 제목 표시줄이 강조 표시되고 Nerd Diner dot com을 가리키는 U R L로 채워집니다.

그림 02: 열린 리디렉션이 있는 NerdDinner 로그인 페이지

올바르게 로그인하면 ASP.NET MVC AccountController의 LogOn 작업이 returnUrl 쿼리 문자열 매개 변수에 지정된 URL로 리디렉션됩니다. 이 경우 공격자가 입력한 http://nerddiner.com/Account/LogOnURL입니다. 우리가 매우 조심하지 않는 한, 특히 공격자가 위조된 페이지가 합법적인 로그인 페이지와 정확히 같은지 확인하기 위해 주의를 기울였기 때문에 이를 눈치채지 못할 가능성이 높습니다. 이 로그인 페이지에는 다시 로그인하라는 오류 메시지가 포함되어 있습니다. 우리는 암호를 잘못 입력한 것 같습니다.

위조된 Nerd Dinner 로그온 페이지를 보여 주는 스크린샷으로, 사용자에게 자격 증명을 다시 입력하라는 메시지를 표시합니다. 제목 표시줄에서 위조된 U R L이 강조 표시됩니다.

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

사용자 이름과 암호를 다시 입력하면 위조된 로그인 페이지에서 정보를 저장하고 합법적인 NerdDinner.com 사이트로 다시 보냅니다. 이 시점에서 NerdDinner.com 사이트에서 이미 인증했으므로 위조된 로그인 페이지가 해당 페이지로 직접 리디렉션될 수 있습니다. 최종 결과는 공격자가 사용자 이름과 암호를 가지고 있으며, 우리는 우리가 그들에게 제공한 것을 인식하지 못합니다.

AccountController LogOn 작업에서 취약한 코드 살펴보기

ASP.NET MVC 2 애플리케이션의 LogOn 작업에 대한 코드는 다음과 같습니다. 로그인이 성공하면 컨트롤러는 returnUrl로 리디렉션을 반환합니다. returnUrl 매개 변수에 대해 유효성 검사가 수행되지 않는 것을 확인할 수 있습니다.

목차 1 – ASP.NET MVC 2 LogOn 액션 AccountController.cs

[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.");
        }
    }
 
    // If we got this far, something failed, redisplay form
    return View(model);
}

이제 ASP.NET MVC 3 LogOn 작업의 변경 내용을 살펴보겠습니다. System.Web.Mvc.Url 도우미 클래스 IsLocalUrl()에서 새 메서드를 호출하여 returnUrl 매개 변수의 유효성을 검사하도록 이 코드가 변경되었습니다.

목록 2 – ASP.NET MVC 3 LogOn 작업 AccountController.cs

[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.");
        }
    }
 
    // If we got this far, something failed, redisplay form
    return View(model);
}

System.Web.Mvc.Url 도우미 클래스 IsLocalUrl()에서 새 메서드를 호출하여 반환 URL 매개 변수의 유효성을 검사하도록 변경되었습니다.

ASP.NET MVC 1.0 및 MVC 2 애플리케이션 보호

IsLocalUrl() 도우미 메서드를 추가하고 LogOn 작업을 업데이트하여 returnUrl 매개 변수의 유효성을 검사하여 기존 ASP.NET MVC 1.0 및 2 애플리케이션의 ASP.NET MVC 3 변경 내용을 활용할 수 있습니다.

이 유효성 검사는 ASP.NET Web Pages 애플리케이션에서도 사용되므로 UrlHelper IsLocalUrl() 메서드는 실제로 System.Web.WebPages의 메서드를 호출하기만 하면 됩니다.

목록 3 - ASP.NET MVC 3 UrlHelper의 IsLocalUrl() 메서드 class

public bool IsLocalUrl(string url) {
    return System.Web.WebPages.RequestExtensions.IsUrlLocalToHost(
        RequestContext.HttpContext.Request, url);
}

IsUrlLocalToHost 메서드는 목록 4에 표시된 대로 실제 유효성 검사 논리를 포함합니다.

목록 4 – System.Web.WebPages RequestExtensions 클래스의 IsUrlLocalToHost() 메서드

public static bool IsUrlLocalToHost(this HttpRequestBase request, string url)
{
   return !url.IsEmpty() &&
          ((url[0] == '/' && (url.Length == 1 ||
           (url[1] != '/' && url[1] != '\\'))) ||   // "/" or "/foo" but not "//" or "/\"
           (url.Length > 1 &&
            url[0] == '~' && url[1] == '/'));   // "~/" or "~/foo"
}

ASP.NET MVC 1.0 또는 2 애플리케이션에서는 AccountController에 IsLocalUrl() 메서드를 추가하지만 가능한 경우 별도의 도우미 클래스에 추가하는 것이 좋습니다. AccountController 내에서 작동하도록 ASP.NET MVC 3 버전의 IsLocalUrl()을 두 가지 약간 변경합니다. 먼저 컨트롤러의 공용 메서드에 컨트롤러 작업으로 액세스할 수 있으므로 공용 메서드에서 프라이빗 메서드로 변경합니다. 둘째, 애플리케이션 호스트에 대해 URL 호스트를 확인하는 호출을 수정합니다. 이 호출은 UrlHelper 클래스에서 로컬 RequestContext 필드를 사용합니다. RequestContext.HttpContext.Request.Url.Host 대신에 우리는 Request.Url.Host를 사용합니다. 다음 코드는 ASP.NET MVC 1.0 및 2 애플리케이션에서 컨트롤러 클래스와 함께 사용하기 위해 수정된 IsLocalUrl() 메서드를 보여 줍니다.

목록 5 – MVC 컨트롤러 클래스에 사용하기 위해 수정된 IsLocalUrl() 메서드

private bool IsLocalUrl(string url)
{
   if (string.IsNullOrEmpty(url))
   {
      return false;
   }
   else
   {
      return ((url[0] == '/' && (url.Length == 1 ||
              (url[1] != '/' && url[1] != '\\'))) ||   // "/" or "/foo" but not "//" or "/\"
              (url.Length > 1 &&
               url[0] == '~' && url[1] == '/'));   // "~/" or "~/foo"
   }
}

IsLocalUrl() 메서드가 구현되었으므로 다음 코드와 같이 LogOn 작업에서 호출하여 returnUrl 매개 변수의 유효성을 검사할 수 있습니다.

목록 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=https://www.bing.com/ 다시.

내 M V C 애플리케이션 로그온 페이지를 보여 주는 스크린샷. 제목 표시줄이 강조 표시되고 외부 반환 U R L로 채워집니다.

그림 04: 업데이트된 LogOn 작업 테스트

성공적으로 로그인하면 외부 URL이 아닌 홈/인덱스 컨트롤러 작업으로 리디렉션됩니다.

내 M V C 애플리케이션 인덱스 페이지를 보여 주는 스크린샷

그림 05: 오픈 리디렉션 공격이 패배했습니다.

요약

리디렉션 URL이 애플리케이션의 URL에 매개 변수로 전달될 때 열린 리디렉션 공격이 발생할 수 있습니다. ASP.NET MVC 3 템플릿에는 열린 리디렉션 공격으로부터 보호하는 코드가 포함되어 있습니다. ASP.NET MVC 1.0 및 2 애플리케이션을 일부 수정하여 이 코드를 추가할 수 있습니다. ASP.NET 1.0 및 2 애플리케이션에 로그인할 때 열린 리디렉션 공격으로부터 보호하려면 IsLocalUrl() 메서드를 추가하고 LogOn 작업에서 returnUrl 매개 변수의 유효성을 검사합니다.