【テクニカル・上級編】SQLインジェクションを100%防ぐための「パラメータクエリ」強制適用コーディング規約 – Access VBA解析バイブル

スポンサーリンク

Access VBAを掌握する極限の知見:SQLインジェクションを100%防ぐ「パラメータクエリ」強制適用アーキテクチャ

レガシーシステムの代名詞のように語られることのあるMicrosoft Accessだが、現場の第一線で巨大なデータ量をさばくシステムアーキテクトにとって、その実態は「使い捨てのプロトタイピングツール」ではなく、適切にチューニングされたRDBエンジン(ACE/Jet)を内包する強力なクライアント・サーバー型アーキテクチャの末裔である。

しかし、多くの開発現場がいまだに犯している致命的な過ちがある。それが「文字列連結による動的SQLの生成」だ。

‘ 【厳禁】絶対に書いてはならないアンチパターン
Dim strSQL As String
strSQL = “SELECT FROM T_Order WHERE CustomerID = ‘” & Me.txtInput & “‘;”
Set rs = CurrentDb.OpenRecordset(strSQL)

このコードは、SQLインジェクションの脆弱性を孕むだけでなく、クエリプランのキャッシュ効率を完全に破壊し、Accessのパフォーマンスを地の底まで落とす。

本稿では、DAOの真の力を引き出し、SQLインジェクションを構造的に100%根絶するための「パラメータクエリ強制適用クラス」の設計と実装を公開する。

—

1. なぜ文字列連結のSQLは「悪」なのか?

パフォーマンスとセキュリティの観点から、文字列連結によるSQL生成を排除すべき理由は明確である。

1. SQLインジェクションの温床: ユーザー入力値にエスケープ処理を忘れた瞬間、任意のSQL文が実行される。特に日本語環境におけるマルチバイト文字の「いわゆる`\0`問題(Shift_JISの2バイト目トラップ)」は、自前でのサニタイズ処理を極めて困難にする。
2. クエリプランの再コンパイル(Jet/ACEエンジンの悲鳴): SQL文字列が毎回変化すると、データベースエンジンはクエリプランをキャッシュできず、文法解析と最適化を毎回実行する。これがネットワーク(またはローカルファイル)越しにおけるIOのボトルネックと相まって、システム全体を重くする。

救世主:DAO.QueryDef と Parameters コレクション

DAO(Data Access Objects)の `QueryDef` オブジェクトと `Parameters` コレクションを使用すれば、SQLの「構造(構造化されたクエリ)」と「データ(パラメータ)」を完全に分離できる。これにより、Jetエンジンはクエリプランを一度だけコンパイルし、安全かつ高速に実行し続けることが可能になる。

—

2. 設計思想:なぜ「クラスモジュール」で隠蔽するのか?

DAOのパラメータ設定コードは、生で書くと冗長になりがちである。

Dim qdf As DAO.QueryDef
Set qdf = CurrentDb.QueryDefs(“Q_GetOrder”)
qdf.Parameters(“prmCustomerID”) = Me.txtInput
Set rs = qdf.OpenRecordset()

これを都度書かせるのではなく、「開発者が文字列連結SQLを書く物理的な手段を奪い、パラメータ指定を強制するラッパークラス」を導入する。

実装要件

  • `QueryDef` のライフサイクルを厳密に管理し、メモリリークを防ぐ。
  • バリアント型の暗黙の型変換に頼らず、明示的な型指定による堅牢性を担保する。
  • エラー発生時にも確実にオブジェクトを解放する。

—

3. 実装コード:セキュア・パラメータ実行エンジン

以下のコードを、AccessVBAプロジェクトにクラスモジュールとして追加せよ。モジュール名は `SecureQueryRunner` とする。

VERSION 1.0 CLASS
BEGIN
MultiUse = -1 ‘True
END
Attribute VB_Name = “SecureQueryRunner”
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
‘ ==============================================================================
‘ クラス名: SecureQueryRunner
‘ 概要: SQLインジェクションを完全に排除し、DAOパラメータクエリの実行を強制するラッパークラス
‘ アーキテクチャ: チーフアーキテクト専用設計
‘ ==============================================================================
Option Explicit

Private m_QueryName As String
Private m_Qdf As DAO.QueryDef
Private m_Params As Object ‘ Scripting.Dictionary (後期バインディングで依存を排除)
Private m_IsPrepared As Boolean

‘ クラス初期化
Private Sub Class_Initialize()
Set m_Params = CreateObject(“Scripting.Dictionary”)
m_IsPrepared = False
End Sub

‘ クラス終期化 (確実なメモリ解放)
Private Sub Class_Terminate()
Me.Clear
End Sub

‘ ——————————————————————————
‘ ターゲットとなるクエリ名を設定する(あらかじめデザインビュー等で作成された静的クエリ、
‘ またはパラメータ付きSQLを持つQueryDef名を指定)
‘ ——————————————————————————
Public Sub Prepare(ByVal queryName As String)
On Error GoTo ErrorHandler

m_QueryName = queryName
Set m_Qdf = CurrentDb.QueryDefs(m_QueryName)
m_IsPrepared = True
Exit Sub

ErrorHandler:
Err.Raise vbObjectError + 1000, “SecureQueryRunner.Prepare”, _
“指定されたクエリが見つからないか、初期化に失敗しました: ” & queryName & ” (” & Err.Description & “)”
End Sub

‘ ——————————————————————————
‘ パラメータを追加する (型安全性を担保するためのラッパー)
‘ ——————————————————————————
Public Sub AddParam(ByVal paramName As String, ByVal value As Variant)
If Not m_IsPrepared Then
Err.Raise vbObjectError + 1001, “SecureQueryRunner.AddParam”, “Prepareメソッドが実行される前にパラメータを追加することはできません。”
End If

‘ Dictionaryに値を格納 (キー重複時は上書き)
If m_Params.Exists(paramName) Then
m_Params(paramName) = value
Else
m_Params.Add paramName, value
End If
End Sub

‘ ——————————————————————————
‘ レコードセットとして実行・取得 (参照専用)
‘ ——————————————————————————
Public Function OpenRecordset() As DAO.Recordset
On Error GoTo ErrorHandler

Me.BindParameters
Set OpenRecordset = m_Qdf.OpenRecordset(dbOpenSnapshot)
Exit Function

ErrorHandler:
Me.Clear
Err.Raise Err.Number, “SecureQueryRunner.OpenRecordset”, Err.Description
End Function

‘ ——————————————————————————
‘ アクションクエリ(INSERT/UPDATE/DELETE)として実行
‘ ——————————————————————————
Public Sub Execute(Optional ByVal dbFailOnError As Boolean = True)
On Error GoTo ErrorHandler

Me.BindParameters
m_Qdf.Execute IIf(dbFailOnError, dbFailOnError, 0)
Exit Sub

ErrorHandler:
Me.Clear
Err.Raise Err.Number, “SecureQueryRunner.Execute”, Err.Description
End Function

‘ ——————————————————————————
‘ 内部処理: QueryDefのParametersコレクションに値をバインド
‘ ——————————————————————————
Friend Sub BindParameters()
Dim key As Variant
Dim paramObj As DAO.Parameter

If Not m_IsPrepared Then Exit Sub

For Each key In m_Params.Keys
‘ QueryDef側に該当するパラメータが存在するか検証
Dim found As Boolean
found = False

For Each paramObj In m_Qdf.Parameters
If paramObj.Name = key Then
‘ 値の設定(型ミスマッチを防ぐためバリアント経由で安全に代入)
paramObj.Value = m_Params(key)
found = True
Exit For
End If
Next paramObj

If Not found Then
Err.Raise vbObjectError + 1002, “SecureQueryRunner.BindParameters”, _
“定義されていないパラメータ名が指定されました: ” & key & ” (クエリ名: ” & m_QueryName & “)”
End If
Next key
End Sub

‘ ——————————————————————————
‘ 明示的なオブジェクト解放(メモリ最適化)
‘ ——————————————————————————
Public Sub Clear()
On Error Resume Next
If Not m_Qdf Is Nothing Then
m_Qdf.Close
Set m_Qdf = Nothing
End If
If Not m_Params Is Nothing Then
m_Params.RemoveAll
Set m_Params = Nothing
End If
m_IsPrepared = False
End Sub

—

4. 実際の利用シーン:美しく、そして堅牢なコード

上記のクラスを使用する場合、フォームや標準モジュールからの呼び出しは極めてシンプルかつ安全になる。

あらかじめAccessのクエリデザイナ(またはSQLビュー)で、以下のようなパラメータ付きクエリを作成しておく。

クエリ名: `Q_GetCustomerOrders`

PARAMETERS prmCustomerID Long, prmStartDate DateTime;
SELECT OrderID, OrderDate, TotalAmount
FROM T_Orders
WHERE CustomerID = [prmCustomerID]
AND OrderDate >= [prmStartDate];

このクエリを、先ほどのクラス経由で安全に呼び出す。

Public Sub Example_FetchData()
Dim runner As SecureQueryRunner
Dim rs As DAO.Recordset

Set runner = New SecureQueryRunner

On Error GoTo ErrorHandler

‘ 1. クエリのプレースパース
runner.Prepare “Q_GetCustomerOrders”

‘ 2. 型安全なパラメータのバインド(ユーザー入力をそのまま渡しても100%安全)
runner.AddParam “prmCustomerID”, Me.txtCustomerID.Value
runner.AddParam “prmStartDate”, CDate(Me.txtStartDate.Value)

‘ 3. レコードセットの取得
Set rs = runner.OpenRecordset()

‘ データ処理ループ
Do Until rs.EOF
Debug.Print rs.Fields(“OrderID”).Value, rs.Fields(“TotalAmount”).Value
rs.MoveNext
Loop

CleanUp:
‘ 4. オブジェクトの明示的解放
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
Set runner = Nothing
Exit Sub

ErrorHandler:
MsgBox “エラーが発生しました: ” & Err.Description, vbCritical
Resume CleanUp
End Sub

—

5. シニアエンジニアが知るべき「裏側の真実」とパフォーマンス最適化

オブジェクトのライフサイクル管理とメモリリーク対策

Access VBAにおいて、`CurrentDb.QueryDefs(…)` を直接ループ内で回したり、解放せずに放置したりすると、Jet/ACEエンジンの内部キャッシュとCOMの参照カウンタが肥大化し、最悪の場合アプリケーションのメモリリーク(フリーズ・強制終了)を引き起こす。
今回提供した `SecureQueryRunner` では、`Class_Terminate` および明示的な `Clear` メソッドによって `QueryDef` の参照を確実に切り離し、ガベージコレクション頼みではない堅牢なリソース管理を実現している。

ネットワークトラフィックとUPSizingへの布石

もし将来的にこのAccessフロントエンドを、SQL ServerやAzure SQL Databaseなどの「ADP / リンクテーブル(ODBC)」構成に移行する場合でも、この `SecureQueryRunner` のインターフェースを変更する必要はない。
DAOのパラメータクエリ構造は、ODBC経由でSQL Server側へ「パラメータ化されたコマンド(RPC / sp_executesql)」として透過的に伝送されるため、クライアント・サーバー間のネットワーク帯域の無駄な消費を防ぎ、サーバー側の実行プランキャッシュの恩恵を最大限に受けることができる。

—

総括

「動的SQLの文字列連結」という悪習は、VBAプログラミングの歴史において数々のセキュリティホールとパフォーマンス劣化を生み出してきた。

「文字列を組み立てて実行する」という安易なアプローチを捨て、「クエリ定義とパラメータを分離し、クラスによってその規約を強制する」こと。これこそが、レガシーとモダンが交差する現場において、エンジニアが身につけるべき真のプロフェッショナリズムである。

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