CreateFileでファイル操作を行う際のセキュリティ属性
Windows APIのCreateFile関数は、ファイルを開くだけでなく、新規作成、デバイスへのアクセス、名前付きパイプの接続などにも使われます。そのため、単にファイル名とアクセス権を指定するだけでは、意図しない共有や権限継承が起きる可能性があります。
CreateFileの第7引数には、セキュリティ属性を表すSECURITY_ATTRIBUTES構造体へのポインターを渡します。この構造体は、作成したオブジェクトのセキュリティ記述子と、返されるハンドルを子プロセスへ継承できるかどうかを指定するために利用されます。
既存ファイルを開く場合と、新しいファイルを作成する場合では、セキュリティ属性の扱いが異なる点にも注意が必要です。既存オブジェクトのアクセス制御リストが優先される場面があるため、SECURITY_ATTRIBUTESを渡せば常にDACLが置き換わるとは限りません。
また、セキュリティ属性はCreateFileだけの話ではありません。プロセス、イベント、ミューテックス、パイプなどのカーネルオブジェクトにも共通する考え方があり、ハンドル継承やアクセス権の設計を理解することが安全なWindowsプログラミングにつながります。
SECURITY_ATTRIBUTESの役割
SECURITY_ATTRIBUTESは、次の3つのメンバーで構成されます。
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SECURITY_ATTRIBUTES;
nLengthには構造体のサイズを設定し、通常はsizeof(SECURITY_ATTRIBUTES)を指定します。lpSecurityDescriptorにはSECURITY_DESCRIPTORへのポインターを渡し、bInheritHandleをTRUEにすると、条件を満たす子プロセスへハンドルを継承できる状態になります。
もっとも基本的な指定は、セキュリティ記述子を既定値に任せ、ハンドル継承だけを制御する方法です。
SECURITY_ATTRIBUTES sa{};
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = nullptr;
sa.bInheritHandle = FALSE;
HANDLE hFile = CreateFileW(
L"C:\\Data\\sample.dat",
GENERIC_READ | GENERIC_WRITE,
0,
&sa,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
nullptr
);
lpSecurityAttributesにNULLを渡した場合も、システム既定のセキュリティ記述子が使われます。アプリケーション内部だけで利用する一時ファイルなどでは十分な場合がありますが、複数ユーザーが利用する共有フォルダーやサービス環境では、明示的な権限制御を検討します。
DACLとアクセス権の設計
セキュリティ記述子には、所有者、グループ、DACL、SACLなどの情報が含まれます。CreateFileで重要になるのは、誰に読み取り、書き込み、削除、属性変更などを許可するかを定めるDACLです。DACLに許可エントリーがなければ、要求したアクセスマスクが拒否されることがあります。
アクセス権は、GENERIC_READやGENERIC_WRITEのような汎用権限だけでなく、FILE_READ_DATA、FILE_WRITE_DATA、DELETE、READ_CONTROLなどの具体的な権限に分解できます。必要以上にGENERIC_ALLを要求すると、DACLが許可していても攻撃や誤操作の影響範囲が広がります。
新しいファイルを作成する際、親ディレクトリからセキュリティ記述子が継承されることがあります。さらに、明示的なSECURITY_DESCRIPTORを指定した場合でも、継承フラグや保護設定によって結果が変わります。作成後に実際のDACLを確認するには、GetNamedSecurityInfoなどのAPIを使います。
文字列形式のSDDLを利用して権限を組み立てる方法もあります。ただし、文字列の意味を誤ると、一般ユーザーへの書き込み許可や全ユーザーへのフルアクセスを与える危険があります。SIDを固定文字列で決め打ちせず、対象環境に応じたアカウントやサービスSIDを確認することが重要です。
ハンドル継承を制御する方法
bInheritHandleがTRUEのハンドルは、子プロセスの生成時に継承対象となる可能性があります。標準入出力をリダイレクトする処理では便利ですが、秘密情報を含むファイルや排他的に管理すべきログファイルまで渡ると、別プロセスから意図せず利用されるおそれがあります。
継承を使う場合は、CreateProcessのbInheritHandlesだけに依存せず、SetHandleInformationで対象ハンドルのフラグを調整する設計が安全です。より厳密に制御するなら、STARTUPINFOEXとPROC_THREAD_ATTRIBUTE_HANDLE_LISTを用いて、子プロセスへ渡すハンドルを明示します。
SetHandleInformation(
hFile,
HANDLE_FLAG_INHERIT,
0
);
反対に、継承が必要なハンドルだけを用意し、他のハンドルは作成時から非継承にします。ウィンドウフックやプロセス間連携のように別プロセスへ処理を渡す技術を調べる際も、関連する Windowsフックの注意点 と同様、境界を越えるデータと権限を明確にすることが欠かせません。
Windowsのバージョンや実行方式によって、ハンドル継承の挙動を誤認しないことも大切です。継承されたハンドルは、子プロセスが終了するまで開いたままになることがあり、ファイル削除やアンインストールが失敗する原因にもなります。
NULL指定と明示的な記述子
CreateFileのlpSecurityAttributesにNULLを指定すると、システムは既定のセキュリティ属性を適用します。これは簡潔ですが、サービスがSYSTEM権限で作成したファイルを一般ユーザーが読む必要があるケースや、逆に一般ユーザーから完全に隔離したいケースでは、要求を満たさないことがあります。
セキュリティ記述子を作成するには、InitializeSecurityDescriptor、SetSecurityDescriptorDacl、ConvertStringSecurityDescriptorToSecurityDescriptorなどのAPIが利用できます。後者はSDDLを直接変換できるため便利ですが、返されたメモリをLocalFreeで解放する必要があります。
PSECURITY_DESCRIPTOR psd = nullptr;
ConvertStringSecurityDescriptorToSecurityDescriptorW(
L"D:P(A;;GA;;;SY)(A;;GR;;;BA)",
SDDL_REVISION_1,
&psd,
nullptr
);
SECURITY_ATTRIBUTES sa{};
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = psd;
sa.bInheritHandle = FALSE;
この例では、SYSTEMにフルアクセスを与え、Administratorsには読み取りを許可する構成を示しています。実際にはアプリケーションの実行アカウント、更新処理、バックアップ、障害解析の要件を確認して、必要最小限のACEだけを設定します。
実装時に確認すべき事項
ファイルを開くコードでは、セキュリティ属性だけでなく、アクセスマスク、共有モード、作成方法、属性フラグを一体として確認します。dwShareModeを0にすれば他プロセスからの共有を拒否できますが、ウイルス対策ソフトやバックアップ処理まで妨げる場合があります。
エラー処理では、CreateFileがINVALID_HANDLE_VALUEを返したときにGetLastErrorを保存します。アクセス拒否を単純な再試行で解決しようとすると、権限昇格を促す設計になりかねません。ユーザーが指定したパスを使う場合は、シンボリックリンク、再解析ポイント、親ディレクトリの権限も考慮します。
| 確認項目 | 主な指定・API | セキュリティ上の要点 |
|---|---|---|
| セキュリティ属性 | SECURITY_ATTRIBUTES |
構造体サイズと記述子の有効期間を確認する |
| DACL | SetSecurityDescriptorDacl |
必要なユーザーやグループだけに許可する |
| ハンドル継承 | bInheritHandle、SetHandleInformation |
子プロセスへ渡す対象を限定する |
| ファイル共有 | dwShareMode |
他プロセスとの競合と可用性を両立する |
| アクセス権 | dwDesiredAccess |
読み取りだけなら書き込み権限を要求しない |
| 解放処理 | CloseHandle、LocalFree |
ハンドルや記述子を確実に解放する |
Windows APIの仕様確認や関連する実装情報を探すときは、Windows API技術情報 のような資料も参照しながら、対象APIの引数だけでなく、呼び出し元と呼び出し先の境界まで確認すると整理しやすくなります。
実務では、まずlpSecurityAttributesをNULLにした場合の既定動作を把握し、共有要件がある箇所だけ明示的なDACLを追加します。そのうえで、必要最小限のアクセス権を要求し、継承しないハンドルを明示し、作成後のACLとハンドル状態を検証する流れが安全な基本形です。