오픈 리디렉션 공격 방지(C#)
이 자습서에서는 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.
그림 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 로그인 페이지로 이동합니다.
그림 02: 열린 리디렉션이 있는 NerdDinner 로그인 페이지
올바르게 로그인하면 ASP.NET MVC AccountController의 LogOn 작업이 returnUrl querystring 매개 변수에 지정된 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 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/ 다시.
그림 04: 업데이트된 LogOn 작업 테스트
성공적으로 로그인하면 외부 URL이 아닌 홈/인덱스 컨트롤러 작업으로 리디렉션됩니다.
그림 05: 오픈 리디렉션 공격이 패배했습니다.
요약
리디렉션 URL이 애플리케이션의 URL에 매개 변수로 전달될 때 열린 리디렉션 공격이 발생할 수 있습니다. ASP.NET MVC 3 템플릿에는 열린 리디렉션 공격으로부터 보호하는 코드가 포함되어 있습니다. ASP.NET MVC 1.0 및 2 애플리케이션을 일부 수정하여 이 코드를 추가할 수 있습니다. ASP.NET 1.0 및 2 애플리케이션에 로그인할 때 열린 리디렉션 공격으로부터 보호하려면 IsLocalUrl() 메서드를 추가하고 LogOn 작업에서 returnUrl 매개 변수의 유효성을 검사합니다.