第13章 安全な.NETプログラム

高度に相互接続されたWebベースのサービスで、トランスポートにHTTP、コンテンツにXMLを使用した新しい環境に勇敢に乗り出すとき、サービスは潜在的な悪意を持つ多数の人々にさらされます。したがって、セキュリティの重要性はさらに高まります。「第1章 セキュリティの必要性」で言及したように、インターネットなどを経由して相互接続したコンピュータは、真っ先に攻撃の対象になります。スタンドアロンコンピュータに対してセキュリティを侵害する攻撃が仕掛けられる可能性より、はるかに大きいのです。明らかに、WebベースのMicrosoft .NETサービスを利用するクライアントおよびサーバーは高度にネットワーク化されているため、セキュリティ攻撃に対して警戒を要します。

.NET共通言語ランタイム(CLR:Common Language Runtime)によって提供されるマネージコードは、バッファオーバーフローなど、多くの一般的なセキュリティ脆弱性を軽減します。また、ActiveXコントロールなど、完全に信頼されるモバイルコードの問題に対処するために役立ちます。Microsoft Windowsの伝統的なセキュリティチェック機能では、プリンシパルの身元だけが考慮されます。言い換えれば、プログラムはユーザーの身元で動作するので、ユーザーが信頼されていればプログラムは信頼され、ユーザーと同じ権限を持つことになります。Windows 2000およびWindows XPの制限付きトークンに基づく技術は、この問題を緩和してくれます。制限付きトークンの詳細については、「第5章 最小限の権限の原則」を参照してください。しかし、.NETのセキュリティはさらに進化しています。ユーザーの権限だけでなく、プログラムに関する証拠とシステムポリシーに基づいて、さまざまなレベルの信頼をプログラムに提供できます。「証拠」は、送信元のデジタル署名やサイトなど、プログラムのプロパティから構成されます。セキュリティポリシーは証拠を判断材料としてプログラムにアクセス許可を与えます。

Dd297677.attention(ja-jp,MSDN.10).gif 筆者の見解では、最も安全な最善のアプリケーションは、Windowsの最善のセキュリティ機構と、.NETの最善のセキュリティ機構を組み合わせたアプリケーションです。これは、セキュリティの問題に対する取り組みにおいて、両者の視点が異なるためです。どちらの技術も万能薬ではありません。アプリケーションを構築する場合に、どの技術を使用することが最善であるか理解することが大切です。どの技術が最も適切であるか判断する際には、STRIDE脅威モデルを使用できます。

しかし、このことからセキュリティ対策について誤った認識を持たないでください。.NETアーキテクチャとマネージコードは、攻撃の可能性を低下させる手段を提供するものですが、それが万能薬になるわけではありません。この章では、回避可能なセキュリティ面の誤りをいくつか取り上げます。また、.NET共通言語ランタイム、Webサービス、XMLを使用してプログラムを記述する場合に、従うべき最善の手順を示します。まず最初に、バッファオーバーフローへの対策として共通言語ランタイムで提供しなければならない機能を紹介しましょう。

Dd297677.important(ja-jp,MSDN.10).gif 共通言語ランタイムは、ある種のセキュリティバグに対する防御機構を提供します。しかし、それによって散漫なプログラミングが許されるわけではありません。核となるセキュリティ原則に従わなければ、最善のセキュリティ機構でも役に立ちません。

13.1 | バッファオーバーフローと共通言語ランタイム

非常に残念なことですが、筆者がバッファオーバーフローについて調査するたびに、それが主にCおよびC++の問題であることに気付きます。CおよびC++は、システムメモリへの直接アクセスを含めて、速度および柔軟性を配慮した設計がなされています。バッファオーバーフローはその他の言語にも存在しますが、その場合にはユーザープログラム内ではなく、ランタイムエンジンまたはライブラリ内にバグが存在する傾向があります。

ここで朗報です。マネージコード環境では、ユーザープログラム内のバッファオーバーフローの可能性が劇的に低下します。しかし、CとC++が一夜にして消え去るわけではありません。マネージコードからアンマネージコードを呼び出すこともできます。アンマネージコードとは、一般的に記述されているCまたはC++のコードなどです。したがって、それだけでは皆さんがご存知の古典的かつ不愉快なセキュリティ面の脆弱性を持つことになります。

真の吉報は、Microsoft Visual C++ .NETの新機能がある種のバッファオーバーフローの問題を緩和するために役立つということです。もちろん、優れた安全なプログラミング手順に代わるものはありません。ただし、それでは十分でないこともあります。私たちは人間であり、誤りを犯します。プログラムに脆弱な部分を残してしまうこともあるでしょう。

Visual C++ .NETで、新たに追加された/GSオプションは、「バッファセキュリティチェック」オプションと呼ばれています。アプリケーションまたはDLLの起動コード、関数のプロローグコードおよびエピローグコードに特殊なコードを挿入するオプションです。

関数のプロローグコードおよびエピローグコードとは?

その名前が示すように、プロローグ(除幕)コードおよびエピローグ(終幕)コードは、関数の呼び出しの最初と最後に置かれるコードです。/GSオプションでコンパイルしたC/C++ソースファイルのアセンブラ出力を見ると、関数の先頭でスタックに無作為のcookieデータを設定する命令が追加されています。これが関数プロローグコードです。また、関数の最後に__security_check_cookieへの呼び出しが含まれます。これが関数エピローグコードです。

この新しいコンパイル時オプションは、cookieを関数のローカルデータとリターンアドレスの間のスタックに追加します。cookieの値は無作為であり、プロセスまたはDLLの起動コードで指定されます。そして関数が戻るときにcookieがチェックされ、cookieが変更されていた場合、特殊なエラーハンドラ関数が呼び出されます。これは既定でプロセスを停止させます。攻撃者がプロセス内にプログラムを挿入して実行するリスクを考えれば、アプリケーションを中止した方が得策です。図13-1は、cookieの挿入されたスタックレイアウトを簡単に表したものです。

図13-1 スタックレイアウトの概略とスタックへのcookieの挿入
▲図13-1 スタックレイアウトの概略とスタックへのcookieの挿入

さらに嬉しいのは、新しいC++プロジェクトでは/GSオプションが既定で有効になることです。このオプションを変更する必要がある場合には、Visual C++ .NETプロジェクト内で次の手順に従う必要があります。

  1. [表示]をクリックし、[ソリューションエクスプローラ]を選択する。
  2. ソリューションエクスプローラ内でプロジェクト名を右クリックする。
  3. [プロパティ]を選択して[プロパティページ]ダイアログボックスを開く。
  4. 必要に応じて[構成プロパティ]ノードを展開した後、[C/C++]ノードを展開する。
  5. [コード生成]をクリックする。
  6. [プロパティページ]ダイアログボックスの左上の[構成]ボックスで[Release]が選択されていることを確認する。
  7. [バッファセキュリティチェック]オプションで[はい(/GS)]を選択する。

図13-2は、プロジェクトの[プロパティページ]ダイアログボックスで[バッファセキュリティチェック]オプションを有効にした様子です。

図13-2 [バッファセキュリティチェック]オプションを有効にした
▲図13-2 [バッファセキュリティチェック]オプションを有効にした

デバッグビルドの場合、/GSの上位セットである/RTC1オプションが既定で有効になるので、/GSオプションを選択する必要はありません。このオプションはまた、実行時に初期化されていない変数を検出し、スタックポインタの破損をチェックします。

独自のGSハンドラを作成しなければならない場合、例外をスローしないでください。この例外にはDebugBreakの呼び出しが含まれ、そこでもまた例外がスローされます。これにより、スタック上の例外処理のアドレスが別のコードにより上書きされる原因になります。

Dd297677.attention(ja-jp,MSDN.10).gif /GSオプションは、CおよびC++のアンマネージコードにのみ適用されます。マネージコードにはこのオプションは適用されません。

/GSオプションを有効にした場合のパフォーマンスに対する影響が心配な方もいるかもしれません。しかし、その影響は最小限です。すべての関数が影響を受けるわけではなく、スタックベースのバッファを備えた関数だけが対象になるためです。このオプションを使用したさまざまなグループの話を聞くと、最悪のケースでも、非常に負荷の高いサーバーベースのアプリケーションで1.3%程度のパフォーマンスの劣化が見られた程度のようです。クライアントアプリケーションの場合には、統計誤差を超える影響はありませんでした。

13.1.1|独自のセキュリティエラーハンドラの追加

スタックベースのバッファオーバーフローが検出された場合に、プロセスの実行を停止することを望まない開発者もいるかもしれません。独自のバージョンのセキュリティエラーハンドラ関数を記述し、そのハンドラ関数へのポインタとともに**_set_security_error_handlerを呼び出すことによって、既定のセキュリティハンドラに優先させることができます。詳細については、Microsoft Visual Studio .NETオンラインドキュメントで_set_security_error_handler**を検索してください。

13.1.2|実際の効果

WindowsベースのC/C++アプリケーションのセキュリティにとって/GSオプションの存在は大きな進歩です。しかし、このオプションを設定するだけでは、バッファオーバーフローを解消できません。まず、/GSオプションによって検出されるのは、関数のリターンアドレスを上書きするスタックベースのバッファオーバーフローに限られます。ヒープベースのバッファオーバーフローが検出されることはありません。また、リターンアドレスを上書きしないバッファオーバーフローも検出されません。次のような例を考えてみてください。関数がスタック上にバッファと関数ポインタを持ちます。そして、このバッファでは、バッファおよび関数ポインタが上書きされます。次の人為的なプログラムを見てください(ここで人為的と述べたのは、Microsoftの製品を含めて、大半の優れたC++コンパイラがこのようなバグを最適化して解消するためです)。

                  
void foo() {
    // 何らかの処理を行う。
}

void CopyData(char *bBuff, int cbBuff) {
    char bName[128];
    void (*func)() = foo;

    memcpy(bName, bBuff, cbBuff);
    (func)();
}

bNameよりbBuffが大きい場合(ただし変数func内に保持されたfooへのポインタは上書きされるが、cookieが上書きされるほど大きくない場合)、func内に保持されたアドレスをバッファbNameの先頭を参照するように変更できます。攻撃者はこのバッファに悪意のあるプログラムを含めることができます。

それでもこの新しいコンパイル時オプションは、C/C++のアンマネージプロジェクトに素晴らしい防御機構を追加してくれます。最低限でも、このオプションを有効にした後、パフォーマンス分析を行い、十分な効率性が得られているかどうか確かめるべきです。この機能にきっと満足するでしょう。

13.2 | .NETにおける機密データの保存

現在、.NET共通言語ランタイムおよび.NET Frameworkでは、機密データを安全に保存するためのサービスが提供されていません。「第7章 機密データの保存」にあるように、ソフトウェアで機密データを安全に保存することは、矛盾する2つのものの両立を求めることと同じです。しかし、攻撃者に対する防御をより強化することはできます。ただし、XMLファイル内のプレーンテキストにパスワードを保存してしまっては、攻撃者に対する防御は効果的なものになりません。

機密データの保存をサポートしない理由の1つに、Xcopyによるアプリケーションの配置を可能にするという.NETの方針があります。言い換えれば、どのようなアプリケーションでも、単純なファイルコピーツールを使用して配置できることを意味します。そのためには、DLLやコントロール、あるいはどのような設定値であっても、レジストリ内に登録する必要がないことが要求されます。ファイルをコピーすれば、アプリケーションを実行できます。そのことを考慮すれば、機密データを保存することでこの方針が台無しになることに気付くかもしれません。機密データを保存するには複雑なアルゴリズムと鍵を使用する暗号化が要求されるため、支援ツールがないと不可能です。しかし、アプリケーション開発者が機密データ構成ツールを使用してからアプリケーションを配置することを禁止する理由はありません。また、アプリケーションに機密データを保存せず、使用するだけに留めることもできます。つまり、アプリケーションで機密データを使用してそれを一時的に保管しますが、そのデータを永続化しません。その場合は、Xcopyによる配置が依然として有効です。

次のような「暗号化プログラム」を見つけたら、それをバグとして記録し、できるだけ早く修正しなければなりません。

                  
class MyCoolCrypt {
    public static char[] EncryptAndDecrypt(string data) {
        // 注意。秘密情報である
        string key = "yeKterceS";
        char[] text = data.ToCharArray();
        for (int i = 0; i < text.Length; i++)
            text[i] ^= key[i % key.Length];

        return text;
    }
}

Windows 2000以降では、データを保護する手段としてDPAPI(Data Protection API)が提供されています。DPAPIの詳細については、「第7章 機密データの保存」を参照してください。次のサンプルプログラムは、C#を使用してDPAPIとのインターフェイスになるクラスを作成する方法を示しています。また、このC#プログラムはNativeMethods.csとともに使用します。NativeMethods.csには、DPAPIを呼び出すのに必要なプラットフォーム呼び出し定義、データ構造、定数が含まれています。すべてのファイルは、付属CD-ROMの[\Jsecureco\Chapter13\DataProtection]フォルダに収録されています。

System.Runtime.InteropServices名前空間は、.NETベースのアプリケーションからCOMオブジェクトおよびネイティブAPIにアクセスするのに役立つクラスのコレクションを提供します。

                  
// DataProtection.cs
namespace Microsoft.Samples.DPAPI {

    using System;
    using System.Runtime.InteropServices;
    using System.Text;

    public class DataProtection {
        // 文字列を保護し、Base64エンコードデータを返す
        public static string ProtectData(string data,
                                         string name,
                                         int flags) {
            byte[] dataIn = Encoding.Unicode.GetBytes(data);
            byte[] dataOut = ProtectData(dataIn, name, flags);

            return (null != dataOut)
                ? Convert.ToBase64String(dataOut)
                : null;
    }

    // Base64エンコードデータの保護を解除し、文字列を返す
    public static string UnprotectData(string data) {
        byte[] dataIn = Convert.FromBase64String(data);
        byte[] dataOut = UnprotectData(dataIn,
            NativeMethods.UIForbidden |
            NativeMethods.VerifyProtection);

        return (null != dataOut)
            ? Encoding.Unicode.GetString(dataOut)
            : null;
    }

    ////////////////////////
    //      内部関数      //
    ////////////////////////

    internal static byte[] ProtectData(byte[] data,
                                       string name,
                                       int dwFlags) {
        byte[] cipherText = null;

        // アンマネージメモリにデータをコピーする
        NativeMethods.DATA_BLOB din =
            new NativeMethods.DATA_BLOB();
        din.cbData = data.Length;
        din.pbData = Marshal.AllocHGlobal(din.cbData);
        Marshal.Copy(data, 0, din.pbData, din.cbData);

        NativeMethods.DATA_BLOB dout =
            new NativeMethods.DATA_BLOB();

        NativeMethods.CRYPTPROTECT_PROMPTSTRUCT ps =
            new NativeMethods.CRYPTPROTECT_PROMPTSTRUCT();

        // DPAPIプロンプト構造に格納する
        InitPromptstruct(ref ps);

            try {
                bool ret =
                    NativeMethods.CryptProtectData(
                        ref din,
                        name,
                        NativeMethods.NullPtr,
                        NativeMethods.NullPtr,
                        ref ps,
                        dwFlags, ref dout);

                if (ret) {
                    cipherText = new byte[dout.cbData];
                    Marshal.Copy(dout.pbData,
                                 cipherText, 0, dout.cbData);
                    NativeMethods.LocalFree(dout.pbData);
                } else {
                    #if (DEBUG)
                    Console.WriteLine("Encryption failed: " +
                        Marshal.GetLastWin32Error().ToString());
                    #endif
                }
            }
            finally {
                if ( din.pbData != IntPtr.Zero )
                    Marshal.FreeHGlobal(din.pbData);
            }

            return cipherText;
        }

        internal static byte[] UnprotectData(byte[] data,
                                             int dwFlags) {
            byte[] clearText = null;

            // アンマネージメモリにデータをコピーする
            NativeMethods.DATA_BLOB din =
                new NativeMethods.DATA_BLOB();
            din.cbData = data.Length;
            din.pbData = Marshal.AllocHGlobal(din.cbData);
            Marshal.Copy(data, 0, din.pbData, din.cbData);

            NativeMethods.CRYPTPROTECT_PROMPTSTRUCT ps =
                new NativeMethods.CRYPTPROTECT_PROMPTSTRUCT();

            InitPromptstruct(ref ps);

            NativeMethods.DATA_BLOB dout =
                new NativeMethods.DATA_BLOB();

            try {
                bool ret =
                    NativeMethods.CryptUnprotectData(
                        ref din,
                        null,
                        NativeMethods.NullPtr,
                        NativeMethods.NullPtr,
                        ref ps,
                        dwFlags,
                        ref dout);

                if (ret) {
                    clearText = new byte[dout.cbData];
                    Marshal.Copy(dout.pbData,
                                 clearText, 0, dout.cbData);
                    NativeMethods.LocalFree(dout.pbData);
                } else {
                    #if (DEBUG)
                    Console.WriteLine("Decryption failed: " +
                        Marshal.GetLastWin32Error().ToString());
                    #endif
                }
            }

            finally {
                if ( din.pbData != IntPtr.Zero )
                    Marshal.FreeHGlobal(din.pbData);
            }

            return clearText;
        }

        static internal void InitPromptstruct(
            ref NativeMethods.CRYPTPROTECT_PROMPTSTRUCT ps) {
            ps.cbSize = Marshal.SizeOf(
                typeof(NativeMethods.CRYPTPROTECT_PROMPTSTRUCT));
            ps.dwPromptFlags = 0;
            ps.hwndApp = NativeMethods.NullPtr;
            ps.szPrompt = null;
        }
    }
}

次のC#ドライバプログラムは、DataProtectionクラスを使用する方法を示しています。

                  
using Microsoft.Samples.DPAPI;
using System;
using System.Text;

class TestStub {
    public static void Main(string[] args)
    {
        string data = "Look out for the Balrog in Moria.";
        string name="MySecret";
        Console.WriteLine("String is: " + data);
        string s = DataProtection.ProtectData(data,
            name,
            NativeMethods.UIForbidden);
        if (null == s) {
            Console.WriteLine("Failure to encrypt");
            return;
        }
        Console.WriteLine("Encrypted Data: " + s);
        s = DataProtection.UnprotectData(s);
        Console.WriteLine("Cleartext: " + s);
    }
}

ASPページの場合と同様に、機密データをASP.NETページに保存しないでください。機密データを保存する必要がある場合には、前述のプログラム、または同様のプログラムを代わりに使用します。

また、COM+コンストラクション文字列を使用することもできます。COM+オブジェクトコンストラクションを使用することで、COM+メタデータに格納する初期化文字列を指定できます。したがって、クラス内で構成情報をハードコードする必要がありません。このアプローチについては、「第12章 Webベースのサービスのセキュリティ」で説明しました。System.EnterpriseServices.ServicedComponent名前空間内の関数を使用してコンストラクション文字列にアクセスできます。

13.3 | 適切なアクセス許可を要求する

アクセス許可を要求するということは、プログラムで処理を実行するために何が必要とされるか、その要件を.NET共通言語ランタイムに通知することを意味します。アクセス許可の要求はオプションであり、プログラムをコンパイルするために必須ではありません。それでも、プログラム内で適切なアクセス許可を要求すべき重要な理由が存在します。

プログラムでDemandメソッドを使用してアクセス許可を要求した場合、そのプログラムを呼び出す各プログラムに適切なアクセス許可が存在するかどうかが、共通言語ランタイムによって確認されます。適切なアクセス許可が存在しない場合、要求は失敗します。アクセス許可の確認では、スタックウォークを実行し、コールスタックの各呼び出し元に与えられているアクセス許可と要求されているアクセス許可を比較します。

アクセス許可を要求することで、プログラムの実行が適切に行われる可能性が高まります。プログラムを実行するために必要なアクセス許可の最小セットを指定しない場合、アクセス許可が与えられていない状況に適切に対処するためのエラー処理プログラムがより多く必要になります。アクセス許可を要求することは、プログラムに必要なアクセス許可だけを与えるのに役立ちます。プログラムが必要とするアクセス許可のみを要求し、必要以上の要求を行うべきではありません。

プログラムで保護リソースにアクセスしないか、あるいは機密性を有する処理を実行しない場合、アクセス許可を要求する必要はありません。

13.4 | Assertの過度な使用

.NET共通言語ランタイムでは、Assertと呼ばれるメソッドが提供されています。このメソッドは、プログラムには許可されているが、その呼び出し元には許可されていないアクションを、そのプログラム(およびその下位の呼び出し元)で実行できるようにします。基本的に「assert(アサート)」という語の持つ意味は、「私は何を行っているか知っている。信用してくれ」と主張することです。その後、通常なら呼び出し元がアクセス許可を持つ必要がある、何らかの処理が実行されます。

Dd297677.important(ja-jp,MSDN.10).gif .NET共通言語ランタイムセキュリティ機能の一部であるAssertメソッドと、古典的なCおよびC++のassert関数を混同しないでください。後者の関数は、式を評価し、それが偽であれば診断メッセージを表示します。

アプリケーションで構成ファイルやルックアップファイルを読み取る場合を考えてみてください。このとき、呼び出し元はファイルI/Oを実行するアクセス許可を持たないかもしれません。プログラムでこのファイルを使用する行為に悪意がないことがわかっていれば、このファイルの使用が安全であるとアサートできます。

アサートすることが安全である場合もあれば、そうでない場合もあります。次のMicrosoft Visual Basic .NETプログラムは、アプリケーション自身が専用で使用する構成ファイルを読み取ります。この場合は安全です。したがって、FileIOPermissionアクセス許可でファイルを読み取ることは安全であるとアサートしています。

                  
Imports System
Imports System.IO
Imports System.Security.Permissions

Public Class MyConfigFile
    Public Function Open() As String
        Try
            Dim f As String = "c:\config\config.xml"
            Dim fp As New _
                FileIOPermission(FileIOPermissionAccess.Read, f)
            fp.Assert()
            Dim sr As New StreamReader(f)
            Dim data As String = sr.ReadToEnd()
            sr.Close()

            Open = data
            Catch e As Exception
            Console.WriteLine(e.ToString())
        End Try

    End Function
End Class

しかし、ユーザーなどの信頼されないソースからファイル名を取得し、そのファイルを削除するようなプログラムは危険です。ユーザーがプログラムに../../boot.iniなどといった要求を送信したらどうなるでしょうか。プログラムによってboot.iniファイルが削除されかねません。特に、このファイルのアクセス制御リスト(ACL)が弱い場合、あるいはファイルがFATパーティション上に存在する場合は危険です。

プログラムレビューを実行する場合には、すべてのセキュリティにかかわるAssertに特に注意し、それが本当に悪用されないかどうか確かめてください。

Dd297677.attention(ja-jp,MSDN.10).gif あるアクセス許可についてアサートするには、まず第一にプログラム自身がそのアクセス許可を持つことが必要です。
Dd297677.important(ja-jp,MSDN.10).gif SecurityPermissionFlag.UnmanagedCodeをアサートしてアンマネージコードを呼び出す場合、特に注意が必要です。プログラムに問題がある場合、信頼されないプログラムが不注意に呼び出される恐れがあります。

13.5 | DemandおよびAssertに対する注意

DemandメソッドおよびAssertメソッドを必要とするアプリケーションを構築する場合、次に示すような単純なガイドラインに従うべきです。まず、プログラムで特権を持って安全な処理を実行する場合で、しかもそのアクセス許可を持つことを呼び出し元に要求しない場合は、アクセス許可のアサートを行うべきです。アサート対象のアクセス許可と、アサートを行う権利であるSecurityPermissionFlag.Assertionをプログラムが持つ必要があることに注意してください。

例を挙げましょう。FileIOPermissionをアサートする場合、プログラムがFileIO

Permissionを持つことが必要です。しかし、呼び出し元のプログラムがこのアクセス許可を持つ必要はありません。FileIOPermissionをアサートしたとき、プログラムにそのアクセス許可が与えられていなければ、例外が発生します。

プログラムで呼び出し元にアクセス許可を持つことを要求する場合には、Demandメソッドを使用してそのアクセス許可を要求すべきです。次のような例を考えてみてください。アプリケーションで電子メールを使用して他のアプリケーションに通知を送信する場合に、プログラムでEmailAlertPermissionというカスタムアクセス許可を定義しました。プログラムが呼び出されたときに、すべての呼び出し元に対してこのアクセス許可を要求(demand)できます。呼び出し元がEmailAlertPermissionを持たなければ、要求は失敗します。

Dd297677.important(ja-jp,MSDN.10).gif あるアクセス許可を要求するプログラムを呼び出す場合に、同じアクセス許可を要求しないでください。これによって、まったく無駄なスタックウォークが発生します。たとえば、Environment.GetEnvironmentVariableを呼び出す場合に、EnvironmentPermissionを要求する必要はありません。.NET Frameworkがこの要求を行ってくれます。

アサートと要求(demand)の両方を行うプログラムを記述する場合もあり得ます。たとえば、次のような例です。前述した電子メールのシナリオで、電子メールサブシステムと直接対話するプログラムは、すべての呼び出し元にカスタムアクセス許可であるEmailAlertPermissionを要求します。さらに、電子メールメッセージをSMTPポートに書き込むときにはSocketPermissionをアサートします。このシナリオでは、呼び出し元はプログラムを使用して電子メールを送信できます。しかし、任意のポートにデータを送信する権限(SocketPermission)を必要としません。

アサートしたアクセス許可を必要とする処理が完了したら、CodeAccessPermission.RevertAssertを呼び出してアサートを無効にすべきです。これは、最小限の権限の原則の一例です。必要な期間に限ってアクセス許可を使用します。

次のサンプルC#プログラムは、電子メールの警告を送信するためにAssertおよびDemandを組み合わせて使用しています。


using System;
using System.Net;
using System.Security;
using System.Security.Permissions;

// プログラムの断片である。クラスや名前空間は含まれていない

static void SendAlert(string alert) {
    // 呼び出し元から電子メールを送信できるように要求する
    new EMailAlertPermission(
        EmailAlertPermission.Send).Demand();

    // プログラムは、ある特定のSMTPサーバー上の特定のポートを開く
    NetworkAccess na = NetworkAccess.Connect;
    TransportType t = TransportType.Tcp;
    string host = "mail.northwindtraders.com";
    int port = 25;
    new SocketPermission(na, t, host, port).Assert();

    try {
        SendAlertTo(host, port, alert);
    } finally {
        CodeAccessPermission.RevertAssert();
    }
}

13.6 | アクセス許可を拒否することに躊躇しない

セキュリティの観点から見て、プログラムの範囲が限定されていて、システムリソースにアクセスする目的でプログラムが悪用される懸念があるときは、特定のアクセス許可が与えられないように要求できます。次の例は、環境アクセスとネイティブプログラムアクセスを拒否するようにアプリケーションを構成しています。これは、アンマネージコードです。


using System;
using System.IO;
using System.Security;
using System.Security.Permissions;

[assembly:EnvironmentPermission(
    SecurityAction.RequestRefuse,
    Unrestricted = true)]

[assembly:SecurityPermission(
    SecurityAction.RequestRefuse,
    UnmanagedCode = true)]

    namespace Perms {
        class ReadConfig {
            .
            .
            .
        }
        .
        .
        .
}

13.7 | 信頼されないソースからのデータの検証

信頼されないソースからのデータの検証については、「第12章 Webベースのサービスのセキュリティ」で非常に詳しく説明しました。しかし、この問題は非常に重要なので、.NETの新しい検証コントロールを使用してスクリプト注入などの入力ベースの攻撃のリスクを軽減する方法を理解しておくべきです。

ASP.NETには、Webサーバーコントロールと呼ばれる新しい機能が用意されています。これは、事前に作成されたコントロールをWebページに配置し、それをサーバーおよびクライアント上で実行することを可能にします。この種のコントロールの1つにRegularExpressionValidatorがあります。Web開発者が任意のユーザー入力の構文を簡単にチェックできるコントロールです。このコントロールが便利なのは、自己完結している点です。Webアプリケーション全体で、追加スクリプトを記述する必要がありません。次に、サーバー上でフォームの項目をテストしてそれが有効な電子メールアドレスであることを確認する簡単な例を示します。


<form id="Form1" method="post" runat="server">
    <asp:TextBox
        id="txtEmail"
        runat="server">
    </asp:TextBox>
    <asp:RegularExpressionValidator
        id="regexEmail"
        runat="server"
        ErrorMessage="Try again!"
        ControlToValidate="txtEmail"
        ValidationExpression=
            "\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*"
        ToolTip="Enter a valid email name."
        Display="Dynamic">
    </asp:RegularExpressionValidator>
</form>

フォーム項目ごとにRegularExpressionValidatorオブジェクトを作成し、サーバー上でデータの形式が正しいかどうかチェックしてください。むやみに入力を受け入れるべきではありません。

Dd297677.attention(ja-jp,MSDN.10).gif ASP.NETは、サーバー上だけでなく、クライアント上での検証もサポートします。クライアント上で入力をチェックしてもセキュリティの保護につながるわけではありませんが、ユーザーが単純な誤りを犯した場合にサーバーとの間で発生するやり取りを低減するために役立ちます。

13.8 | ASP.NETでのスレッドに関する注意

スレッドは、マネージコードに固有の問題ではありません。どういう場合であれ、スレッドについて理解しておくべきです。ASP.NETでは、MTA(MultiThreaded Apartment)スレッドプールを使用します(すべてのマネージコードコンポーネントがMTAを使用します)。呼び出し先のCOMオブジェクトでSTA(Single-Threaded Apartment)を使用するようにマークされている場合、Webアプリケーションはスレッドを切り替えるので、偽装コンテキストを喪失します。この問題を回避するには、AspCompatMode=trueを使用してページをSTAスレッド上で動作させます。これは、ASP.NETからSTAコンポーネントを使用するための唯一の方法です。また、必要であればコンポーネント内でCoImpersonateClientを呼び出すこともできます。

それでも、とにかくSTAコンポーネントの使用は避けるべきです。ASP.NETでは大きなパフォーマンスの損失につながる恐れがあります。

Dd297677.attention(ja-jp,MSDN.10).gif Visual Basic 6 COMコンポーネントはSTAを使用します。

13.9 | ASP.NETアプリケーションを配置する前にトレース/デバッグ機能を無効にする

ASP.NETアプリケーションを配置する前にトレース/デバッグ機能を無効にするのは当然のように思えますが、それをしない人があまりにも多いことに驚かされます。これは、2つの理由から問題があります。まず、攻撃者に多くの情報を与えてしまう可能性があります。2つ目に、パフォーマンスに対する悪影響が懸念されます。

トレース/デバッグ機能を無効にするには、3つの方法があります。まず、IIS(Internet Information Services)からDEBUG動詞を削除します。図13-3は、IIS管理ツールでこのオプションを利用できる場所を示しています。

図13-3 デバッグしたくない各拡張子に対して、DEBUG動詞を削除できる(この図ではSOAPファイル)
▲図13-3 デバッグしたくない各拡張子に対して、DEBUG動詞を削除できる(この図ではSOAPファイル)

また、ASP.NETアプリケーション自身で、次のようなPage命令を適切なページに追加することで、トレース/デバッグ機能を無効にすることもできます。

<%@ Page Language="VB" Trace="False" Debug="False" %>

さらに、アプリケーション構成ファイル内でもデバッグ/トレース機能を無効にできます。


<trace enabled = 'false' />
<compilation debug = 'false'/>

13.10 | .NET Frameworkを使用して良い乱数を生成する

暗号処理で安全な乱数を作成する場合、次のようなプログラムを使用すべきではありません。このプログラムでは、Cランタイムのrand関数のような線型合同関数を使用しています。


// 新しい暗号鍵を生成する
byte[] key = new byte[16];
new Random().NextBytes(key);

代わりに、次のC#サンプルプログラムのようなプログラムを使用すべきです。このプログラムは、32バイトのバッファに暗号的な乱数列データを格納します(乱数生成の詳細については、「第6章 暗号化技術の弱点」を参照してください)。


using System.Security.Cryptography;
try {
    byte[] b = new byte[32];
    new RNGCryptoServiceProvider().GetBytes(b);

    for (int i = 0; i < b.Length; i++)
        Console.Write("{0} ", b[i].ToString("x"));

} catch(CryptographicException e) {
    Console.WriteLine(e.ToString());
}

RNGCryptoServiceProviderクラスは、CryptoAPIとCryptGenRandomを呼び出し、ランダムなデータを生成します。Visual Basic .NETでは、同じプログラムは次のようになります。


Dim b(32) As Byte
Dim i As Short

Try
    Dim r As New RNGCryptoServiceProvider()
    r.GetBytes(b)
    For i = 0 To b.Length - 1
        Console.Write("{0}", b(i).ToString("x"))
    Next
Catch e As CryptographicException
    Console.WriteLine(e.ToString)
End Try

13.11 | 信頼されないソースからのデータの逆シリアル化

信頼されないソースからのデータのシリアル化を解除しないでください。これは、本書で前述した「すべての入力は、それが証明されるまで有害である」という規則の.NET版です。.NET共通言語ランタイムは、シリアル化と呼ばれるプロセスを使用してオブジェクトをパッケージ化およびアンパッケージ化するクラスをSystem.Runtime.Serialization名前空間内に提供しています(このプロセスをフリーズドライ処理と呼ぶ人もいます)。しかし、アプリケーションで信頼されないソースから得たデータを逆シリアル化すべきではありません。再構成されたオブジェクトは、完全に信頼されるプログラムとしてローカルコンピュータ上で動作します。

また、このような攻撃には、データを受信するプログラムがSerializationFormatterアクセス許可を持つことが必要です。このアクセス許可は、完全に信頼されたプログラムにのみ適用すべき高度な特権を有するアクセス許可です。

Dd297677.attention(ja-jp,MSDN.10).gif 信頼されないソースから得たデータを逆シリアル化することで発生するセキュリティ面の問題は、.NET特有のものではないことに注意してください。この問題は、その他の技術にも存在します。たとえばMFCでは、CArchive::Operator>>およびCArchive::Operator<<を使用することによって、オブジェクトのシリアル化および逆シリアル化を行うことができます。ただし、MFC内のプログラムはすべてアンマネージコードであり、その定義から完全に信頼されたプログラムとして動作します。

13.12 | 失敗時に攻撃者に情報を与えすぎない

プログラムが失敗し、例外が発生すると、.NET環境は見事なデバッグ情報を提供します。しかし、攻撃者がこの情報を悪用し、サーバーベースのアプリケーションに関する情報や、攻撃を仕掛けるための情報を探る手掛かりとして使用する恐れがあります。一例として、次のようなプログラムによって提示されるスタックダンプを示します。


try {
    // 何らかの処理を行う
} catch (Exception e) {
    Result.WriteLine(e.ToString());
}

このプログラムを実行すると、次のような出力結果がユーザーに送信されます。


System.Security.SecurityException: Request for the permission of type
    System.Security.Permissions.FileIOPermission...
    at System.Security.SecurityRuntime.FrameDescHelper(...)
    at System.Security.CodeAccessSecurityEngine.Check(...)
    at System.Security.CodeAccessSecurityEngine.Check(...)
    at System.Security.CodeAccessPermission.Demand()
    at System.IO.FileStream..ctor(...)
    at Perms.ReadConfig.ReadData() in
        c:\temp\perms\perms\class1.cs:line 18

デバッグビルドのアプリケーションの場合を除いて、行番号は送信されません。しかし、担当の開発者またはテスト担当者以外のだれにでも非常に多くの情報が提供されてしまいます。例外が発生した場合には、Windowsイベントログのみを書き込み、ユーザーには要求が失敗したことを告げる簡単なメッセージだけを送信します。


try {
    // 何らかの処理を行う
} catch (Exception e) {

#if(DEBUG)
    Result.WriteLine(e.ToString());
#else
    Result.WriteLine("An error occurred.");
    new LogException().Write(e.ToString());
#endif
}
public class LogException {
    public void Write(string e) {
        try {
            new EventLogPermission(
                EventLogPermissionAccess.Instrument,
                "machinename").Assert();
            EventLog log = new EventLog("Application");
            log.Source="MyApp";
            log.WriteEntry(e, EventLogEntryType.Warning);
        } catch(Exception e2) {
        // イベントログが書き込めない
   }
    }
}

アプリケーションによっては、前述のプログラムのようにEventLogPermission(...).Assertを呼び出す必要があるかもしれません。当然ですが、アプリケーションがイベントログを書き込むアクセス許可を持たなければ、別の例外が発生します。

13.13 | SOAPについての注意事項

RPC(リモートプロシージャコール)アプリケーションを記述したことがある方は、RPCサービスをインターネット上に直接配置すべきかどうか考えてみてください。とんでもないことだ? 本質的にSOAPは、RPCとHTTP、またはCOMとHTTPを結び付けるものです。SOAPアプリケーションはWebアプリケーションであり、最も悪意に満ちた環境で動作することに注意してください。SOAPメソッドに送信されるすべての入力をチェックすることは非常に大切です。正規表現を使用してデータが有効かどうか判断してください。ここでの朗報は、SOAPメソッドを呼び出す際の引数が型を持つことです。したがって、SOAPエンジンは多くの不正なデータを阻止してくれます。それでも、とにかくデータを検証すべきです。

また、SOAPサービスで認証を要求する場合には、同じ認証設定が.wsdl(Web Services Description Language:Webサービス記述言語)ファイルおよび.disco(Web Services Discovery)ファイルにも適用されていることを確認します。これらのファイルはともに、ユーザー、そして攻撃者がアプリケーションに関する情報を知るために利用できます。このような情報には、機密性を有するものが含まれる場合もあるでしょう。したがって、サービスにアクセスするユーザーを認証するのであれば、アプリケーションメタデータを読み取る権利についてもユーザーを認証すべきです。

13.14 | まとめ

.NET Frameworkおよび共通言語ランタイムは、さまざまなセキュリティの問題に関するソリューションを提供します。特にマネージ環境は、ユーザーの記述したアプリケーションでバッファオーバーフローの発生を抑止します。そして、信頼されるプログラム、ある程度信頼されるプログラム、信頼されないプログラムの間で抱えるジレンマの解決に役立つプログラムアクセスセキュリティ機能を提供します。しかし、これだけで満足してよいわけではありません。プログラムが攻撃される可能性を考え、防衛的なコーディングを行う必要があります。

本書で提案されている内容の多くは、マネージアプリケーションにも当てはまります。つまり、Webページとプログラムに機密データを保存しない、最小限の権限の原則に従い限定的なアクセス許可のみを要求してアプリケーションを実行する、セキュリティ面の決定を何らかの名前に基づいて行う場合は注意する、などです。