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

スポンサーリンク

【Access VBA】SQLインジェクションを100%根絶する。QueryDefとParameterオブジェクトによる「動的SQL」極限統制規約

開発現場でいまだに散見される、最も悪質で、かつ致命的なアンチパターンをご存知だろうか。
それが 「文字列連結によるSQL文の動的生成」 だ。

‘ 【絶対に行ってはならない最悪のコード例】
Dim sql As String
sql = “SELECT FROM T_Employees WHERE EmployeeName = ‘” & Me.txtInput.Value & “‘;”
Set rs = CurrentDb.OpenRecordset(sql)

この書き方がなぜ「業務システムテロ」に等しいのか。
SQLインジェクションによるデータ破壊や情報漏洩のリスクはもちろんのこと、シングルクォーテーションのミスによる構文エラー、日付や数値の暗黙の型変換バグ、そしてクエリプランのキャッシュが効かないことによるパフォーマンスの劇的な劣化など、百害あって一利なしだ。

プロフェッショナルなAccess開発者であれば、動的SQLの生成には必ず `DAO.QueryDef` と `DAO.Parameter` オブジェクトを使用しなければならない。

今回は、現場のエンジニアたちに迷う余地を与えず、SQLインジェクションを100%防ぎながら圧倒的なパフォーマンスを発揮するための「パラメータクエリ強制適用コーディング規約」と、そのための究極のクラスモジュール実装を伝授する。

—

1. なぜ「パラメータクエリ」なのか?

DAO(Data Access Objects)におけるパラメータクエリとは、SQLの構造とデータを完全に分離する仕組みだ。

文字列連結では、ユーザーが入力した値(例えば `O’Reilly` のようなアポストロフィを含む文字列)がSQLの構文の一部として解釈されてしまいエラーや不正侵入を招く。しかし、パラメータオブジェクトを使用した場合、入力値は「ただのデータ(リテラル)」として安全にバインドされる。

さらに、Access(Jet/ACEエンジン)内部において、パラメータ化されたクエリは実行プランが最適化されやすく、繰り返し実行時のオーバーヘッドが削減されるという大きなメリットもある。

—

2. チーム開発を強制する「安全なクエリ実行クラス」の設計

現場のプログラマに「規約を守ってください」と言葉だけで伝えても、期限に追われれば必ず破られる。だからこそ、「間違った書き方ができない構造(強制)」をコードで担保する。

ここでは、SQL文字列の直接実行を禁止し、必ずパラメータ経由で安全にクエリを実行・取得するラッパークラス `SecureQueryRunner` を実装する。

クラスモジュール名: `SecureQueryRunner`

Option Compare Database
Option Explicit

‘ ==============================================================================
‘ クラス名: SecureQueryRunner
‘ 概要: SQLインジェクションを100%防止し、安全にパラメータクエリを実行するクラス
‘ ==============================================================================

Private m_QueryName As String
Private m_Database As DAO.Database

‘ 初期化時にカレントデータベースをバインド
Private Sub Class_Initialize()
Set m_Database = CurrentDb
End Sub

Private Sub Class_Terminate()
Set m_Database = Nothing
End Sub

‘ ==============================================================================
‘ メソッド名: ExecuteParameterizedQuery
‘ 概要: 一時的なQueryDefを作成し、型安全にパラメータをバインドしてレコードセットを返す
‘ ==============================================================================
Public Function OpenRecordset(ByVal sql As String, ParamArray params() As Variant) As DAO.Recordset
Dim qdf As DAO.QueryDef
Dim i As Long
Dim paramName As String

On Error GoTo ErrorHandler

‘ 1. 一意のテンポラリクエリ名生成(名前衝突を防ぐ)
m_QueryName = “~TMP_SECURE_QDF_” & Format(Timer, “00000.00”)

‘ 2. QueryDefの作成(永続化せず、メモリ上で完結させる)
Set qdf = m_Database.CreateQueryDef(m_QueryName, sql)

‘ 3. パラメータの動的バインド
‘ パラメータは @p0, @p1, @p2 … という名前でSQL側にプレースホルダーがある前提とする
If UBound(params) >= LBound(params) Then
For i = LBound(params) To UBound(params)
paramName = “@p” & (i – LBound(params))

‘ クエリ側に該当パラメータが存在するかチェックして安全に値・型を渡す
If qdf.Parameters(paramName).Type Then
qdf.Parameters(paramName).Value = IIf(IsNull(params(i)), Null, params(i))
End If
Next i
End If

‘ 4. 安全なレコードセットの取得
Set OpenRecordset = qdf.OpenRecordset(dbOpenSnapshot, dbForwardOnly)

‘ 5. クリーンアップ(QueryDefの破棄)
m_Database.QueryDefs.Delete m_QueryName
Exit Function

ErrorHandler:
‘ エラー発生時は確実に一時クエリを削除する
On Error Resume Next
If m_QueryName <> “” Then
m_Database.QueryDefs.Delete m_QueryName
End If
Err.Raise Err.Number, “SecureQueryRunner.OpenRecordset”, “クエリ実行に失敗しました: ” & Err.Description
End Function

—

3. 実務における実装例:どう呼び出すべきか

上記のクラスモジュールを作成したら、実際の業務フォームやモジュールからは以下のように呼び出す。

SQL側では、変数を直接埋め込むのではなく、`@p0`, `@p1` といったパラメータ名(プレースホルダー)を記述する。

‘ ==============================================================================
‘ 実装例: 検索フォームからの安全なデータ取得
‘ ==============================================================================
Public Sub SearchEmployees(ByVal departmentId As Long, ByVal keyword As String)
Dim runner As SecureQueryRunner
Dim rs As DAO.Recordset
Dim sql As String

‘ SQLは完全な静的文字列として定義(連結は一切しない)
sql = “SELECT EmployeeID, EmployeeName, DepartmentID, HireDate ” & _
“FROM T_Employees ” & _
“WHERE DepartmentID = @p0 ” & _
” AND (EmployeeName LIKE ‘%’ + @p1 + ‘%’ OR @p1 IS NULL);”

Set runner = New SecureQueryRunner

On Error GoTo CleanUp

‘ パラメータを引数として順番に渡す(型は自動推論・安全にバインド)
‘ ユーザーがどんな悪意ある文字列(SQLインジェクションの断片など)を入力しても
‘ @p1 は「ただの文字列リテラル」として安全に処理される。
Set rs = runner.OpenRecordset(sql, departmentId, keyword)

‘ 結果の処理
Do Until rs.EOF
Debug.Print rs!EmployeeID & “: ” & rs!EmployeeName
rs.MoveNext
Loop

CleanUp:
If Not rs Is Nothing Then
rs.Close
Set rs = Nothing
End If
Set runner = Nothing
End Sub

—

4. チーフアーキテクトからの実践的アドバイス

1. 「SQLの文字列連結を発見したら即リファクタリング」をチームの共通認識に
コードレビューにおいて、SQL文の途中に `&` を使って変数を組み込んでいる行を見つけたら、それだけでマージを拒否するべきだ。セキュリティホールだけでなく、デバッグのしやすさ(SQL文をそのままSQLビューにコピペしてテストできなくなる)の観点からも絶対に排除すべき悪習である。

2. 前方一致・部分一致検索のプレースホルダーの扱い
LIKE演算子を使う際、`”LIKE ‘%” & keyword & “%'”` のように文字列を連結しがちだが、これもNG。上記の実装例のように、プレースホルダー側には純粋な検索キーワードを渡し、SQL側で `’+ @p1 +’` のように結合するか、条件判定を工夫することで安全性を保つことができる。

3. トランザクションと一意性
今回紹介したクラスでは、一時クエリ名に `Timer` 関数を用いて衝突を避けているが、超高負荷なマルチユーザー環境や同時実行性が求められるバックエンド処理の場合は、`GUID` (Typelib / Scriptlet.TypeLib等を利用) を用いてクエリ名を完全に一意化すると、さらに堅牢性が増す。

—

結論

Access VBAだからといって、セキュリティやコードの品質を妥協する言い訳にはならない。
「文字列連結SQLの全廃」と「Parameterオブジェクトによる完全な型バインド」を徹底することで、あなたの作るAccessアプリは、極めて堅牢で、保守性が高く、バグの起きない「プロフェッショナル・システム」へと生まれ変わる。

明日からのコードレビューで、ぜひこの設計思想を取り入れてほしい。

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