オープン リダイレクト攻撃の防止 (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 へのリダイレクトには、ログインが成功した後に、ユーザーを当初要求した URL に返せるような、returnUrl クエリ文字列パラメーターが含まれます。
次のスクリーンショットでは、ログインせずに /Account/ChangePassword ビューにアクセスしようとすると、/Account/LogOn?ReturnUrl=%2fAccount%2fChangePassword%2f にリダイレクトされることが確認できます。
図 01: オープン リダイレクトを含むログイン ページ
ReturnUrl クエリ文字列パラメーターが検証されないため、攻撃者はそれを変更して、任意の 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" が 1 つ欠けていることに注意してください。 この例では、これが攻撃者がコントロールしているドメインです。 上記のリンクにアクセスすると、正当な 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 – AccountController.cs
の ASP.NET MVC 2 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.");
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
次に、ASP.NET MVC 3 LogOn アクションでの変更を見てみましょう。 このコードは、IsLocalUrl()
という名前の System.Web.Mvc.Url ヘルパー クラスの新しいメソッドを呼び出すことで、returnUrl パラメーターを検証するように変更されました。
リスト 2 – AccountController.cs
の ASP.NET MVC 3 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.");
}
}
// 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() ヘルパー メソッドを追加し、returnUrl パラメーターを検証するように LogOn アクションを更新することで、既存の ASP.NET MVC 1.0 および 2 のアプリケーションで ASP.NET MVC 3 の変更を活用できます。
この検証は ASP.NET Web ページ アプリケーションでも使用されるため、UrlHelper IsLocalUrl() メソッドは実際には System.Web.WebPages のメソッドを呼び出すだけです。
リスト 3 – ASP.NET MVC 3 UrlHelper class
の IsLocalUrl() メソッド
public bool IsLocalUrl(string url) {
return System.Web.WebPages.RequestExtensions.IsUrlLocalToHost(
RequestContext.HttpContext.Request, url);
}
リスト 4 に示すように、IsUrlLocalToHost メソッドには、実際の検証ロジックが含まれています。
リスト 4 – System.Web.WebPages RequestExtensions class の 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() に 2 つの小さな変更を加えます。 1 つ目は、パブリック メソッドからプライベート メソッドへの変更です。コントローラーのパブリック メソッドにはコントローラー アクションとしてアクセスできるためです。 2 つ目は、アプリケーション ホストに対して URL ホストをチェックする呼び出しを変更します。 この呼び出しでは、UrlHelper クラスのローカル RequestContext フィールドを使用します。 this.RequestContext.HttpContext.Request.Url.Host を使用する代わりに、this.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 パラメーターを検証します。