다음을 통해 공유


오픈 리디렉션 공격 방지(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으로의 이 리디렉션에는 returnUrl querystring 매개 변수가 포함되므로 사용자가 성공적으로 로그인한 후 원래 요청된 URL로 반환될 수 있습니다.

아래 스크린샷에서는 로그인하지 않은 경우 /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은 저녁 식사라는 단어에서 "n"이 누락된 nerddiner.com 가리킵니다. 이 예제에서는 공격자가 제어하는 도메인입니다. 위의 링크에 액세스하면 합법적인 NerdDinner.com 로그인 페이지로 이동합니다.

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

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

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

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

그림 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 작업의 변경 내용을 살펴보겠습니다. 이 코드는 라는 IsLocalUrl()System.Web.Mvc.Url 도우미 클래스에서 새 메서드를 호출하여 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 웹 페이지 애플리케이션에서도 사용되므로 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 매개 변수의 유효성을 검사합니다.