SignalR セキュリティ入門
作成者: Patrick Fletcher、Tom FitzMacken
警告
このドキュメントは、最新版の SignalR に対するものではありません。 ASP.NET Core SignalR に関する記事を参照してください。
この記事では、SignalR アプリケーションを開発するときに考慮する必要があるセキュリティの問題について説明します。
このトピックで使用されるソフトウェアのバージョン
- Visual Studio 2013
- .NET 4.5
- SignalR バージョン 2
このトピックの以前のバージョン
SignalR の以前のバージョンの詳細については、「SignalR の以前のバージョン」を参照してください。
質問とコメント
このチュートリアルの感想、改善に関するフィードバックをページの下部にあるコメント欄にお寄せください。 チュートリアルに直接関連しない質問がある場合は、ASP.NET SignalR フォーラムまたは StackOverflow.com に投稿できます。
概要
このドキュメントは、次のトピックに分かれています。
SignalR セキュリティの概念
認証と権限承認
SignalR では、ユーザーを認証するための機能は提供されません。 代わりに、SignalR 機能をアプリケーションの既存の認証構造に統合します。 アプリケーションで通常と同様にユーザーを認証し、SignalR コードで認証の結果を処理します。 たとえば、ASP.NET フォーム認証を使用してユーザーを認証し、次にハブで、メソッドを呼び出す権限を持つユーザーまたはロールを制定することが考えられます。 ハブでは、ユーザー名やユーザーがロールに属しているかどうかなどの認証情報をクライアントに渡すこともできます。
SignalR には、ハブまたはメソッドにアクセスできるユーザーを指定するための Authorize 属性が用意されています。 Authorize 属性は、ハブまたはハブ内の特定のメソッドに適用します。 Authorize 属性がない場合、ハブに接続されているクライアントは、ハブ上のすべてのパブリック メソッドを使用できます。 ハブの詳細については、「SignalR ハブの認証と承認」を参照してください。
Authorize
属性をハブに適用しますが、永続的な接続には適用しません。 PersistentConnection
を使用するときの承認規則を適用するには、AuthorizeRequest
メソッドをオーバーライドする必要があります。 永続的な接続の詳細については、「SignalR 永続的接続の認証と承認」を参照してください。
接続トークン
SignalR は、送信者の ID を検証することで、悪意のあるコマンドを実行するリスクを軽減します。 要求ごとに、クライアントとサーバーで、認証されたユーザーの接続 ID とユーザー名を含む接続トークンを受け渡します。 接続 ID は、接続されている各クライアントを一意に識別します。 新しい接続の作成時にサーバーは接続 ID をランダムに生成し、その ID を接続の期間中保持します。 ユーザー名は、Web アプリケーションの認証メカニズムによって提供されます。 SignalR は暗号化とデジタル署名を使用して、接続トークンを保護します。
要求ごとに、サーバーがトークンの内容を検証して、要求が指定されたユーザーから送信されていることを確認します。 ユーザー名は、接続 ID に対応している必要があります。SignalR は、接続 ID とユーザー名の両方を検証することで、悪意のあるユーザーが別のユーザーを簡単に偽装するのを防止します。 サーバーが接続トークンを検証できない場合、要求は失敗します。
接続 ID は検証プロセスの一部であるため、あるユーザーの接続 ID を他のユーザーに公開したり、その値をクライアント (Cookie など) に格納したりしないでください。
接続トークンと他のトークンの種類
接続トークンは、セッション トークンまたは認証トークンのように見えるため、セキュリティ ツールによってフラグが設定されることがあり、公開された場合にリスクを引き起こします。
SignalR の接続トークンは認証トークンではありません。 これは、この要求を行っているユーザーが、接続を作成したユーザーと同じであることを確認するために使用されます。 ASP.NET SignalR では接続をサーバー間で移動できるため、接続トークンが必要です。 トークンは接続を特定のユーザーに関連付けますが、要求を行っているユーザーの ID をアサートしません。 SignalR 要求が適切に認証されるには、Cookie やベアラー トークンなど、ユーザーの ID をアサートする他のトークンが必要です。 ただし、接続トークン自体は、要求がそのユーザーによって行われたことを示すことはありません。トークン内に含まれる接続 ID がそのユーザーに関連付けられているだけです。
接続トークンは独自の認証クレームを提供しないため、"セッション" または "認証" トークンとは見なされません。 特定のユーザーの接続トークンを取得して、別のユーザー (または認証されていない要求) として認証された要求で再生すると、要求のユーザー ID とトークンに保存されている ID が一致しないため、失敗します。
再接続時のグループへの再参加
既定では、SignalR アプリケーションは、一時的な中断から再接続するとき (接続が切断され、その接続がタイムアウトになる前に再確立されたときなど) に、ユーザーを適切なグループに自動的に再割り当てします。クライアントは再接続時に、接続 ID と割り当てられたグループを含むグループ トークンを渡します。 グループ トークンはデジタル署名され、暗号化されます。 クライアントは再接続後も同じ接続 ID を保持します。そのため、再接続されたクライアントから渡される接続 ID が、クライアントで使用されている前の接続 ID と一致する必要があります。 この検証により、再接続時に悪意のあるユーザーが承認されていないグループに参加する要求を渡すことを防ぎます。
ただし、グループ トークンの有効期限が切れないことに注意することが重要です。 ユーザーが過去にグループに属していたが、そのグループから禁止された場合、そのユーザーは禁止されたグループを含むグループ トークンを模倣できる場合があります。 どのユーザーがどのグループに属するかを安全に管理する必要がある場合は、サーバーのデータベースなどにそのデータを格納する必要があります。 そのうえで、ユーザーがグループに属しているかどうかをサーバー上で検証するロジックをアプリケーションに追加します。 グループ メンバーシップの確認の例については、「グループを使用する」を参照してください。
グループの自動再参加は、一時的な中断後に接続が再接続されたときにのみ適用されます。 ユーザーがアプリケーションから移動して切断した場合やアプリケーションが再起動した場合は、そのユーザーを適切なグループに追加する方法をアプリケーションが処理する必要があります。 詳細については、「グループを使用する」を参照してください。
SignalR がクロスサイト リクエスト フォージェリを防ぐしくみ
クロスサイト リクエスト フォージェリ (CSRF) は、ユーザーが現在ログインしている脆弱なサイトに、悪意のあるサイトが要求を送信する攻撃です。 SignalR は、SignalR アプリケーションに対して悪意のあるサイトが有効な要求を作成する可能性を非常に低くすることで、CSRF を防止します。
CSRF 攻撃の説明
CSRF 攻撃の例を次に示します。
ユーザーが、フォーム認証を使用して www.example.com にログインします。
サーバーがユーザーを認証します。 サーバーからの応答には、認証 Cookie が含まれています。
ログアウトしないまま、ユーザーが悪意のある 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 の "クロスサイト" 部分です。
ユーザーが送信ボタンをクリックします。 ブラウザーが、要求に認証 Cookie を含めます。
要求はユーザーの認証コンテキストを使用して example.com サーバー上で実行されるため、認証されたユーザーが実行を許可される任意のアクションを実行できます。
この例ではユーザーがフォーム ボタンをクリックする必要がありますが、悪意のあるページは SignalR アプリケーションに AJAX 要求を送信するスクリプトを簡単に実行できます。 さらに、SSL を使用しても CSRF 攻撃は防止されません。悪意のあるサイトは "https://" 要求を送信できるためです。
CSRF 攻撃は、認証に Cookie を使用する Web サイトに対して普通に実行可能です。関連するすべての Cookie を、ブラウザーが宛先 Web サイトに送信するためです。 ただし、CSRF 攻撃は Cookie の悪用に限定されません。 たとえば、基本認証やダイジェスト認証も脆弱です。 ユーザーが基本認証またはダイジェスト認証を使用してログインした後は、セッションが終了するまで、ブラウザーが資格情報を自動的に送信します。
SignalR が実行する CSRF の軽減策
SignalR は、悪意のあるサイトがアプリケーションに対して有効な要求を作成するのを防ぐために、次の手順を実行します。 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 プロキシ ファイルにハブとメソッドのすべては含めたくない場合、ファイルの自動生成を無効にできます。 複数のハブとメソッドがあるが、すべてのユーザーにすべてのメソッドを認識させたくない場合は、このオプションを選択することが考えられます。 EnableJavaScriptProxies を false に設定することで、自動生成を無効します。
var hubConfiguration = new HubConfiguration();
hubConfiguration.EnableJavaScriptProxies = false;
app.MapSignalR(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.");
}
}