次の方法で共有


SignalR セキュリティ入門 (SignalR 1.x)

作成者: Patrick FletcherTom FitzMacken

警告

このドキュメントは、最新版の SignalR に対するものではありません。 ASP.NET Core SignalR に関する記事を参照してください。

この記事では、SignalR アプリケーションを開発するときに考慮する必要があるセキュリティの問題について説明します。

概要

このドキュメントは、次のトピックに分かれています。

SignalR セキュリティの概念

認証と権限承認

SignalR は、アプリケーションの既存の認証構造に統合されるように設計されています。 ユーザーを認証するための機能は提供されません。 そうではなく、アプリケーションで通常と同様にユーザーを認証してから、SignalR コードで認証の結果を処理します。 たとえば、ASP.NET フォーム認証を使用してユーザーを認証し、次にハブで、メソッドを呼び出す権限を持つユーザーまたはロールを制定することが考えられます。 ハブでは、ユーザー名やユーザーがロールに属しているかどうかなどの認証情報をクライアントに渡すこともできます。

SignalR には、ハブまたはメソッドにアクセスできるユーザーを指定するための Authorize 属性が用意されています。 Authorize 属性は、ハブまたはハブ内の特定のメソッドに適用します。 Authorize 属性がない場合、ハブに接続されているクライアントは、ハブ上のすべてのパブリック メソッドを使用できます。 ハブの詳細については、「SignalR ハブの認証と承認」を参照してください。

Authorize 属性はハブでのみ使用されます。 PersistentConnection を使用するときの承認規則を適用するには、AuthorizeRequest メソッドをオーバーライドする必要があります。 永続的な接続の詳細については、「SignalR 永続的接続の認証と承認」を参照してください。

接続トークン

SignalR は、送信者の ID を検証することで、悪意のあるコマンドを実行するリスクを軽減します。 認証されたユーザーの接続 ID とユーザー名を含む接続トークンが、要求ごとにクライアントとサーバーの間で受け渡されます。 接続 ID は、新しい接続の作成時にサーバーによってランダムに生成され、その接続の期間中保持される一意識別子です。 ユーザー名は、Web アプリケーションの認証メカニズムによって提供されます。 接続トークンは、暗号化とデジタル署名で保護されます。

Diagram Connection Token system, showing the relationship between the Client, Server, Authentication System, and Connection Token.

要求ごとに、サーバーがトークンの内容を検証して、要求が指定されたユーザーから送信されていることを確認します。 ユーザー名は、接続 ID に対応している必要があります。SignalR は、接続 ID とユーザー名の両方を検証することで、悪意のあるユーザーが別のユーザーを簡単に偽装するのを防止します。 サーバーが接続トークンを検証できない場合、要求は失敗します。

Diagram of the Connection Token system, showing the relationship between the Client, Server, and Saved Token.

接続 ID は検証プロセスの一部であるため、あるユーザーの接続 ID を他のユーザーに公開したり、その値をクライアント (Cookie など) に格納したりしないでください。

再接続時のグループへの再参加

既定では、SignalR アプリケーションは、一時的な中断から再接続するとき (接続が切断され、その接続がタイムアウトになる前に再確立されたときなど) に、ユーザーを適切なグループに自動的に再割り当てします。クライアントは再接続時に、接続 ID と割り当てられたグループを含むグループ トークンを渡します。 グループ トークンはデジタル署名され、暗号化されます。 クライアントは再接続後も同じ接続 ID を保持します。そのため、再接続されたクライアントから渡される接続 ID が、クライアントで使用されている前の接続 ID と一致する必要があります。 この検証により、再接続時に悪意のあるユーザーが承認されていないグループに参加する要求を渡すことを防ぎます。

ただし、グループ トークンの有効期限が切れないことに注意することが重要です。 ユーザーが過去にグループに属していたが、そのグループから禁止された場合、そのユーザーは禁止されたグループを含むグループ トークンを模倣できる場合があります。 どのユーザーがどのグループに属するかを安全に管理する必要がある場合は、サーバーのデータベースなどにそのデータを格納する必要があります。 そのうえで、ユーザーがグループに属しているかどうかをサーバー上で検証するロジックをアプリケーションに追加します。 グループ メンバーシップの確認の例については、「グループを使用する」を参照してください。

グループの自動再参加は、一時的な中断後に接続が再接続されたときにのみ適用されます。 ユーザーがアプリケーションから移動して切断した場合やアプリケーションが再起動した場合は、そのユーザーを適切なグループに追加する方法をアプリケーションが処理する必要があります。 詳細については、「グループを使用する」を参照してください。

SignalR がクロスサイト リクエスト フォージェリを防ぐしくみ

クロスサイト リクエスト フォージェリ (CSRF) は、ユーザーが現在ログインしている脆弱なサイトに、悪意のあるサイトが要求を送信する攻撃です。 SignalR は、SignalR アプリケーションに対して悪意のあるサイトが有効な要求を作成する可能性を非常に低くすることで、CSRF を防止します。

CSRF 攻撃の説明

CSRF 攻撃の例を次に示します。

  1. ユーザーが、フォーム認証を使用して www.example.com にログインします。

  2. サーバーがユーザーを認証します。 サーバーからの応答には、認証 Cookie が含まれています。

  3. ログアウトしないまま、ユーザーが悪意のある Web サイトにアクセスします。 この悪意のあるサイトには、次の HTML フォームが含まれています。

    <h1>You Are a Winner!</h1>
    <form action="http://example.com/api/account" method="post">
        <input type="hidden" name="Transaction" value="withdraw" />
        <input type="hidden" name="Amount" value="1000000" />
        <input type="submit" value="Click Me"/>
    </form>
    

    悪意のあるサイトではなく、脆弱なサイトにフォームのアクションが投稿されることがわかります。 これが CSRF の "クロスサイト" 部分です。

  4. ユーザーが送信ボタンをクリックします。 ブラウザーが、要求に認証 Cookie を含めます。

  5. 要求はユーザーの認証コンテキストを使用して example.com サーバー上で実行されるため、認証されたユーザーが実行を許可される任意のアクションを実行できます。

この例ではユーザーがフォーム ボタンをクリックする必要がありますが、悪意のあるページは SignalR アプリケーションに AJAX 要求を送信するスクリプトを簡単に実行できます。 さらに、SSL を使用しても CSRF 攻撃は防止されません。悪意のあるサイトは "https://" 要求を送信できるためです。

CSRF 攻撃は、認証に Cookie を使用する Web サイトに対して普通に実行可能です。関連するすべての Cookie を、ブラウザーが宛先 Web サイトに送信するためです。 ただし、CSRF 攻撃は Cookie の悪用に限定されません。 たとえば、基本認証やダイジェスト認証も脆弱です。 ユーザーが基本認証またはダイジェスト認証を使用してログインした後は、セッションが終了するまで、ブラウザーが資格情報を自動的に送信します。

SignalR が実行する CSRF の軽減策

SignalR は、悪意のあるサイトが SignalR アプリケーションに対して有効な要求を作成するのを防ぐために、次の手順を実行します。 これらの手順は既定で実行され、コードで何も行う必要はありません。

  • クロス ドメイン要求を無効にする
    ユーザーが外部ドメインから SignalR エンドポイントを呼び出すことを防止するため、SignalR アプリケーションではクロス ドメイン要求が既定で無効になっています。 外部ドメインからの要求は自動的に無効とみなされ、ブロックされます。 この既定の動作を維持することをお勧めします。そうしないと、悪意のあるサイトがユーザーをだましてサイトにコマンドを送信する可能性があります。 クロス ドメイン要求を使用する必要がある場合は、「クロス ドメイン接続を確立する方法」を参照してください。
  • Cookie ではなくクエリ文字列で接続トークンを渡す
    SignalR は、Cookie ではなくクエリ文字列値として接続トークンを渡します。 接続トークンを Cookie として格納しないことで、悪意のあるコードが検出されたときに接続トークンが誤ってブラウザーから転送されることがなくなります。 接続トークンは現在の接続を超えて保持されることもありません。 そのため、悪意のあるユーザーが、別のユーザーの認証資格情報で要求を行うことはできません。
  • 接続トークンを確認する
    接続トークン」セクションで説明されているように、サーバーは、認証された各ユーザーに関連付けられている接続 ID を認識しています。 サーバーは、ユーザー名と一致しない接続 ID からの要求を処理しません。 悪意のあるユーザーが有効な要求を推測できる可能性はほとんどありません。それには、悪意のあるユーザーが、ユーザー名と現在のランダム生成された接続 ID を知る必要があるためです。その接続 ID は、接続が終了するとすぐに無効になります。 匿名ユーザーは機密情報にアクセスできないことになります。

SignalR のセキュリティに関する推奨事項

Secure Sockets Layer (SSL) プロトコル

SSL プロトコルは、暗号化を使用して、クライアントとサーバー間のデータ転送をセキュリティで保護します。 SignalR アプリケーションがクライアントとサーバーの間で機密情報を送信する場合は、転送のために SSL を使用します。 SSL の設定の詳細については、「IIS 7 で SSL を設定する方法」を参照してください。

セキュリティ メカニズムとしてグループを使用しない

グループは、関連するユーザーをまとめる便利な方法ですが、機密情報へのアクセスを制限するための安全なメカニズムではありません。 これは、ユーザーが再接続中に自動的にグループに再参加できるときに特にあてはまります。 代わりに、特権ユーザーをロールに追加し、ハブ メソッドへのアクセスをそのロールのメンバーのみに制限することを検討してください。 ロールに基づいてアクセスを制限する例については、「SignalR ハブの認証と承認」を参照してください。 再接続時にグループへのユーザー アクセスを検査する例については、「グループを使用する」を参照してください。

クライアントからの入力を安全に処理する

悪意のあるユーザーが他のユーザーにスクリプトを送信しないように、クライアントからの、他のクライアントへのブロードキャストを目的とした入力はすべてエンコードする必要があります。 サーバーではなく受信側のクライアントでメッセージをエンコードすることをお勧めします。SignalR アプリケーションには多くの異なる種類のクライアントが存在する可能性があります。 そのため、HTML エンコードは Web クライアントでは機能しますが、他の種類のクライアントでは機能しません。 たとえば、チャット メッセージを表示する Web クライアント メソッドは、html() 関数を呼び出すことでユーザー名とメッセージを安全に処理します。

chat.client.addMessageToPage = function (name, message) {
    // Html encode display name and message. 
    var encodedName = $('<div />').text(name).html();
    var encodedMsg = $('<div />').text(message).html();
    // Add the message to the page. 
    $('#discussion').append('<li><strong>' + encodedName
        + '</strong>:  ' + encodedMsg + '</li>');
};

アクティブな接続でのユーザーの状態の変更を調整する

アクティブな接続が存在する間にユーザーの認証状態が変わると、ユーザーは "The user identity cannot change during an active SignalR connection (アクティブな SignalR 接続中はユーザー ID を変更できません)" というエラーを受け取ります。その場合、アプリケーションがサーバーに再接続して、接続 ID とユーザー名が調整されていることを確認する必要があります。 たとえば、アクティブな接続が存在する間にユーザーがログアウトすることをアプリケーションが許可している場合、その接続のユーザー名は、次の要求に渡される名前と一致しなくなります。 ユーザーがログアウトする前に接続を停止してから、再起動することを思いつきます。

ただし、ほとんどのアプリケーションでは、接続を手動で停止して開始する必要がないと認識することが重要です。 ログアウト後にアプリケーションがユーザーを別のページにリダイレクトしたり (Web Forms アプリケーションや MVC アプリケーションの既定の動作など)、ログアウト後に現在のページを更新したりする場合、アクティブな接続は自動的に切断され、追加のアクションは必要ありません。

次の例は、ユーザーの状態が変更されたときに接続を停止して開始する方法を示しています。

<script type="text/javascript">
    $(function () {
        var chat = $.connection.sampleHub;
        $.connection.hub.start().done(function () {
            $('#logoutbutton').click(function () {
                chat.connection.stop();
                $.ajax({
                    url: "Services/SampleWebService.svc/LogOut",
                    type: "POST"
                }).done(function () {
                    chat.connection.start();
                });
            });
        });
    });
</script>

それ以外に、サイトがフォーム認証でスライド式有効期限を使用し、認証 Cookie を有効な状態に保つアクティビティがない場合にも、ユーザーの認証状態が変わる可能性があります。 その場合、ユーザーはログアウトされ、ユーザー名が接続トークン内のユーザー名と一致しなくなります。 認証 Cookie を有効に保つために Web サーバー上のリソースを定期的に要求するいくつかのスクリプトを追加することで、この問題を解決できます。 30 分ごとにリソースを要求する方法を次の例に示します。

$(function () {
    setInterval(function() {
        $.ajax({
            url: "Ping.aspx",
            cache: false
        });
    }, 1800000);
});

自動的に生成された JavaScript プロキシ ファイル

各ユーザーの JavaScript プロキシ ファイルにハブとメソッドのすべては含めたくない場合、ファイルの自動生成を無効にできます。 複数のハブとメソッドがあるが、すべてのユーザーにすべてのメソッドを認識させたくない場合は、このオプションを選択することが考えられます。 EnableJavaScriptProxiesfalse に設定することで、自動生成を無効します。

var hubConfiguration = new HubConfiguration();
hubConfiguration.EnableJavaScriptProxies = false;
RouteTable.Routes.MapHubs("/signalr", hubConfiguration);

JavaScript プロキシ ファイルの詳細については、「生成されたプロキシとその機能」を参照してください。

例外

例外オブジェクトをクライアントに渡すことは避ける必要があります。オブジェクトは機密情報をクライアントに公開する可能性があるためです。 代わりに、関連するエラー メッセージを表示するメソッドをクライアントで呼び出します。

public Task SampleMethod()
{
    try
    { 
        // code that can throw an exception
    }
    catch(Exception e)
    {
        // add code to log exception and take remedial steps

        return Clients.Caller.DisplayError("Sorry, the request could not be processed.");
    }
}