【テクニカル・上級編】VB.NETにおけるSecureStringの正しい使い方とメモリ保護:平文パスワードを保持しない安全な認証情報管理 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

VB.NETにおけるSecureStringの正しい使い方とメモリ保護:平文パスワードを保持しない安全な認証情報管理

長年、エンタープライズ領域のレガシーシステムからモダンな.NET環境への移行を見続けてきた。
VB.NET(およびその祖先であるVB6/VBA)を扱う現場では、未だに以下のようなコードが平然と書かれている。

.net
‘ 【アンチパターン】絶対にやってはならない実装
Dim connectionString As String = “Server=myServerAddress;Database=myDataBase;Uid=myUsername;Pwd=myPassword;”

String型はイミュータブル(不変)であり、GC(ガベージコレクタ)によって回収されるまでの間、その平文のパスワード文字列はマネージドヒープ上にそのままの形で残り続ける。メモリダンプの採取、あるいはプロセスの脆弱性を突いたメモリハックによって、機密情報はいとも簡単に露出する。

プロフェッショナルなエンジニアであれば、このリスクを看過してはならない。
本稿では、VB.NETにおいてメモリ上で機密情報を暗号化し、平文を一切保持しないための『SecureString』の真の活用法を、メモリライフサイクルの制御と共にあらゆる妥協を排除して解説する。

1. なぜ String 型では不十分なのか?

.NETの `String` オブジェクトは、文字列プールやガベージコレクションのアルゴリズムの都合上、意図したタイミングでメモリから完全に消去することが極めて困難である。

  • イミュータブル性: 文字列を変更(あるいは破棄しようと)するたびに新しいインスタンスが生成され、古い実体はGCが回収するまでメモリ上に漂い続ける。
  • メモリダンプの脅威: タスクマネージャーやDbgHelp等によるミニダンプ採取が行われた際、String型の領域は平文のまま露わになる。

これ対抗するための仕組みが `System.Security.SecureString` である。
SecureStringは、内部バッファを暗号化(WindowsのDPAPI等をベースにした保護)して保持し、必要最小限の時間だけアンマネージドメモリ上に復元する。

2. SecureString の正しい実装パターン(VB.NET)

SecureStringを扱う上で最も重要な鉄則は、「文字単位で構築し、使用後は即座に破棄(Dispose)する」ことである。最初からString型を経由してSecureStringに変換するような実装は、大元のString型がメモリに残るため無意味である。

以下のコードは、コンソール入力やセキュアなUIから一文字ずつSecureStringを構築し、安全に消費する模範的な実装である。

.net
Imports System.Security
Imports System.Runtime.InteropServices
Imports System.Net

Public Class SecureCredentialManager

‘ SecureStringを安全に構築する例
Public Function CreateSecurePassword(ByVal inputChars As Char()) As SecureString
Dim secStr As New SecureString()
Try
For Each c As Char In inputChars
secStr.AppendChar(c)
Next
‘ 読み取り専用にロック(改ざん防止)
secStr.MakeReadOnly()
Return secStr
Catch
‘ 異常系では確実にリソースを解放
secStr.Dispose()
Throw
Finally
‘ 平文が含まれる可能性のある文字配列をメモリ上でクリア
Array.Clear(inputChars, 0, inputChars.Length)
End Try
End Function

‘ SecureStringを復号せずにNetworkCredential等へ安全に渡す例
Public Sub ConnectToExternalService(ByVal userName As String, ByVal securePassword As SecureString)
‘ .NET Framework / .NET Core共通:NetworkCredentialはSecureStringを直接受け取れる
Dim credential As New NetworkCredential(userName, securePassword)

‘ 以降、credentialオブジェクトを用いた通信処理
‘ 例: WebClientやHttpClientHandlerでの利用

‘ 使い終わったCredentialの即時破棄(IDisposableの実装を強制)
If TypeOf credential Is IDisposable Then
DirectCast(credential, IDisposable).Dispose()
End If
End Sub

End Class

3. アンマネージドメモリとの境界線:Marshalerの活用

ADO.NETの `SqlConnection` など、古いアーキテクチャを引き継ぐコンポーネントの中には、未だに `Password=String` の形式を要求するものがある。
どうしても平文に戻さざるを得ない瞬間があるならば、その露出時間は「マイクロ秒単位」に限定し、使用後は直ちにゼロクリアしなければならない。

Marshalクラスを用いた極限のメモリ操作のコードを見てほしい。

.net
Public Class DatabaseConnector

‘ SecureStringから一時的に平文を取り出し、即座に消去するパターン
Public Sub ExecuteWithSecureConnection(ByVal connectionStringTemplate As String, ByVal userName As String, ByVal securePwd As SecureString)
Dim bstrPtr As IntPtr = IntPtr.Zero
Dim plainPassword As String = String.Empty

Try
‘ 1. BSTR(アンマネージドメモリ)として一時的に復号化して取得
bstrPtr = Marshal.SecureStringToBSTR(securePwd)

‘ 2. String型へ一時変換(この瞬間のみメモリ上に平文が存在する)
plainPassword = Marshal.PtrToStringBSTR(bstrPtr)

‘ 3. 接続文字列の構築(実際にはSqlConnectionStringBuilder等を推奨)
Dim finalConnString As String = String.Format(“{0}User ID={1};Password={2};”, connectionStringTemplate, userName, plainPassword)

‘ — ここでデータベース接続とクエリ実行 —
Using connection As New System.Data.SqlClient.SqlConnection(finalConnString)
connection.Open()
‘ 処理…
End Using

Finally
‘ 4. 【最重要】取得したBSTRポインタのメモリ領域を確実にゼロクリアして破棄
If bstrPtr <> IntPtr.Zero Then
Marshal.ZeroFreeBSTR(bstrPtr)
End If

‘ 5. ローカル変数の参照を切る(GCへのヒント)
plainPassword = Nothing
End Try
End Sub

End Class

ここで `Marshal.ZeroFreeBSTR` を使っている点に注目してほしい。通常の `Marshal.FreeBSTR` ではメモリを解放するだけで、そこに機密情報(平文パスワード)が残存してしまう。`ZeroFreeBSTR` は、メモリ領域を解放する直前にすべてのバイトを `0` で上書き(ゼロクリア)する。これこそが、プロフェッショナルのメモリ管理である。

4. レガシー環境・システム間連携における現実解

現実のシステム開発では、外部の古いWeb APIや、SecureStringをサポートしていないサードパーティ製COMコンポーネントと連携しなければならない場面に直面する。

ここで安易に「SecureStringは面倒だから全工程でStringを使おう」と妥協してはならない。境界線の設計を以下のように厳格に分離するべきである。

1. 境界の外側(入力・通信): UIや設定ファイルからの読み込み時のみ一時的に平文を扱い、直ちに SecureString にカプセル化する。
2. コアロジック内部: すべて SecureString または `ReadOnlyMemory(Of Char)` 等で保持し、平文を露出させない。
3. 境界の内側(出力・レガシーAPI呼び出し): 直前の瞬間だけ `Marshal` を用いて復号し、使い終わり次第 `ZeroFree` で即座に粉砕する。

5. チーフアーキテクトからの総括

セキュリティとは「破られないこと」ではなく、「リスクの総量を極小化し、万が一の漏洩時にも被害を局限化すること」である。

VB.NETは、その歴史の長さゆえにレガシーな記述が許容されてしまう側面を持つ。しかし、言語仕様の奥底にある .NET のランタイム構造を正しく理解し、アンマネージドメモリのライフサイクルまで制御下に置くことで、C#や他のモダン言語に劣らない堅牢なセキュリティインフラストラクチャを構築できる。

明日、いや、今すぐ既存のコードベースを見直してほしい。
あなたのアプリケーションのメモリ上に、無防備に横たわるパスワードの文字列はないだろうか? その脆弱性を断ち切るコードに書き換えることこそが、真のエンジニアリングである。

タイトルとURLをコピーしました