オープン リダイレクト攻撃の防止 (C#)

作成者: Jon Galloway

このチュートリアルでは、ASP.NET MVC アプリケーションでオープン リダイレクト攻撃を防ぐ方法について説明します。 このチュートリアルでは、ASP.NET MVC 3 の AccountController で行われた変更について説明し、既存の ASP.NET MVC 1.0 および 2 アプリケーションでこれらの変更を適用する方法について説明します。

オープン リダイレクト攻撃とは

クエリ文字列やフォーム データなどの要求を介して指定された URL にリダイレクトする Web アプリケーションは、ユーザーを外部の悪意のある URL にリダイレクトするために改ざんされる可能性があります。 この改ざんは、オープン リダイレクト攻撃と呼ばれています。

アプリケーション ロジックによって、指定された URL へのリダイレクトが行われるたびに、リダイレクト URL が改ざんされていないことを検証する必要があります。 ASP.NET MVC 1.0 と ASP.NET MVC 2 の両方で既定の AccountController で使用されるログインは、オープン リダイレクト攻撃に対して脆弱です。 幸いにも、既存のアプリケーションを更新して、ASP.NET MVC 3 プレビューの修正を使用するのは簡単です。

この脆弱性を理解するために、既定の ASP.NET MVC 2 Web アプリケーション プロジェクトでのログイン リダイレクトのしくみを見てみましょう。 このアプリケーションでは、[Authorize] 属性を持つコントローラー アクションにアクセスしようとすると、承認されていないユーザーが /Account/LogOn ビューにリダイレクトされます。 /Account/LogOn へのこのリダイレクトには returnUrl querystring パラメーターが含まれるため、ユーザーは正常にログインした後、最初に要求された URL に戻すことができます。

次のスクリーンショットでは、ログインしていないときに /Account/ChangePassword ビューにアクセスしようとすると、/Account/LogOn にリダイレクトされることがわかります。ReturnUrl=%2fAccount%2fChangePassword%2f。

[My M V C アプリケーション のログオン] ページを示すスクリーンショット。タイトル バーが強調表示されています。

図 01: 開いているリダイレクトを含むログイン ページ

ReturnUrl querystring パラメーターは検証されないため、攻撃者はパラメーターに URL アドレスを挿入するように変更して、開いているリダイレクト攻撃を実行できます。 これを示すために、ReturnUrl パラメーターを に https://bing.com変更して、結果のログイン URL が /Account/LogOn になるようにすることができます。ReturnUrl=https://www.bing.com/. サイトに正常にログインすると、 に https://bing.comリダイレクトされます。 このリダイレクトは検証されないため、代わりに、ユーザーをだまそうとする悪意のあるサイトを指している可能性があります。

より複雑なオープン リダイレクト攻撃

オープン リダイレクト攻撃は、攻撃者が特定の Web サイトにログインしようとしていることを知っているため、特に危険です。これにより、 フィッシング攻撃に対して脆弱になります。 たとえば、攻撃者がパスワードをキャプチャしようとして、悪意のある電子メールを Web サイト ユーザーに送信する可能性があります。 NerdDinner サイトでこれがどのように機能するかを見てみましょう。 (ライブ NerdDinner サイトは、オープン リダイレクト攻撃から保護するように更新されていることに注意してください)。

まず、攻撃者が NerdDinner のログイン ページへのリンクを送信し、偽造されたページへのリダイレクトを含めます。

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

戻り URL は nerddiner.com を指しており、dinner という単語に "n" が含まれていることに注意してください。 この例では、これは攻撃者が制御するドメインです。 上記のリンクにアクセスすると、正当な NerdDinner.com ログイン ページに移動します。

Nerd Dinner dot com のホーム ページを示すスクリーンショット。タイトル バーが強調表示され、Nerd Diner dot com を指す URL が表示されます。

図 02: 開いているリダイレクトを含む NerdDinner ログイン ページ

正しくログインすると、ASP.NET MVC AccountController の LogOn アクションによって、returnUrl querystring パラメーターで指定された URL にリダイレクトされます。 この場合は、攻撃者が入力した URL () です http://nerddiner.com/Account/LogOn。 私たちが非常に注意深い場合を除き、特に攻撃者が偽造されたページが正当なログイン ページとまったく同じように見えるように注意しているため、このことに気付かない可能性が非常に高いです。 このログイン ページには、再度ログインすることを要求するエラー メッセージが含まれています。 不器用な私たちは、私たちのパスワードを誤って入力している必要があります。

偽造された Nerd Dinner Log On ページを示すスクリーンショット。ユーザーに資格情報の再入力を求めるメッセージが表示されています。タイトル バーの偽造された URL が強調表示されています。

図 03: Forged NerdDinner ログイン画面

ユーザー名とパスワードを再入力すると、偽造ログイン ページによって情報が保存され、正当な NerdDinner.com サイトに返送されます。 この時点で、NerdDinner.com サイトは既に認証されているため、偽造されたログイン ページは、そのページに直接リダイレクトできます。 最終的な結果は、攻撃者がユーザー名とパスワードを持っており、それを提供したことに気付かないということです。

AccountController LogOn アクションで脆弱なコードを確認する

ASP.NET MVC 2 アプリケーションの LogOn アクションのコードを次に示します。 ログインが成功すると、コントローラーは returnUrl へのリダイレクトを返します。 returnUrl パラメーターに対して検証が実行されていないことを確認できます。

リスト 1 – で MVC 2 LogOn アクションを ASP.NET する 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 – で MVC 3 LogOn アクションを ASP.NET する 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 の変更を利用できます。

この検証はアプリケーションでも使用されるため、UrlHelper IsLocalUrl() メソッドは実際には System.Web.WebPages のメソッド ASP.NET Web ページを呼び出すだけです。

リスト 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 アプリケーションでは、IsLocalUrl() メソッドを AccountController に追加しますが、可能であれば別のヘルパー クラスに追加することをお勧めします。 AccountController 内で動作するように、ASP.NET MVC 3 バージョンの IsLocalUrl() に 2 つの小さな変更を加えます。 まず、コントローラー内のパブリック メソッドにコントローラー アクションとしてアクセスできるため、パブリック メソッドからプライベート メソッドに変更します。 次に、アプリケーション ホストに対して 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/ もう一度。

[My M V C アプリケーション のログオン] ページを示すスクリーンショット。タイトル バーが強調表示され、外部の戻り値 U R L が入力されます。

図 04: 更新された LogOn アクションのテスト

正常にログインすると、外部 URL ではなくホーム/インデックス コントローラー アクションにリダイレクトされます。

[My M V C アプリケーション インデックス] ページを示すスクリーンショット。

図 05: Open Redirection 攻撃が敗北しました

まとめ

オープン リダイレクト攻撃は、リダイレクト URL がアプリケーションの URL のパラメーターとして渡されるときに発生する可能性があります。 ASP.NET MVC 3 テンプレートには、オープン リダイレクト攻撃から保護するコードが含まれています。 このコードは、MVC 1.0 および 2 アプリケーション ASP.NET 変更して追加できます。 ASP.NET 1.0 および 2 つのアプリケーションにログインするときのオープン リダイレクト攻撃から保護するには、IsLocalUrl() メソッドを追加し、LogOn アクションで returnUrl パラメーターを検証します。