【実務・中級編】【動的コード実行の罠】ExecuteGlobal を使用した共通設定読み込み時の変数スコープ汚染とセキュリティ対策 – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク
スポンサーリンク

容易な「共通化」が招く破滅:ExecuteGlobalの罠と、堅牢な設計への転換

業務自動化の現場において、VBScriptは今なお強力な武器だ。特に、設定値や共通関数を外部ファイルに切り出し、それをメインスクリプトで読み込む手法は、コードの重複を避けるための「定石」として広く知られている。

その際、多くの開発者が手にする禁断の果実が `ExecuteGlobal` だ。

「外部の `.vbs` ファイルを読み込んで、そのまま変数や関数として使える。なんて便利なんだ」

そう感じたなら、君はまだプロフェッショナルなアーキテクトではない。その利便性の裏側には、「変数スコープの汚染」と「意図しないコード注入」という、保守性とセキュリティを根底から破壊する時限爆弾が仕掛けられている。

本稿では、なぜ `ExecuteGlobal` が危険なのかを論理的に解明し、実務において「バグが起きない」「誰が読んでも壊れない」堅牢な設計パターンを伝授する。

1. ExecuteGlobal が引き起こす「スコープ汚染」の正体

なぜ `ExecuteGlobal` は危険なのか。その最大の理由は、「実行コンテキストの境界を破壊する」点にある。

通常、プログラミングにおいて変数の生存範囲(スコープ)は、設計者が制御すべき重要な要素だ。しかし、`ExecuteGlobal` は外部コードを、呼び出し元のグローバルスコープへ直接「注入」する。

発生する問題:名前空間の衝突(Namespace Collision)

例えば、以下のような状況を想像してほしい。

1. `config.vbs` という共通設定ファイルがある。中身は `Dim Timeout : Timeout = 30` だ。
2. メインスクリプト `main.vbs` で `ExecuteGlobal` を使ってこれを読み込む。
3. しかし、`main.vbs` の別のモジュール、あるいは将来追加される別のライブラリでも、たまたま `Timeout` という変数名を使っていたとしたらどうなるか?

`ExecuteGlobal` は、既存の `Timeout` を無慈悲に上書きする。あるいは、既存の変数を予期せぬ値に変えてしまう。これは「サイレントな破壊」だ。エラーは出ない。ただ、プログラムの挙動が「なぜかおかしい」という、デバッグが極めて困難な状態に陥る。

「共通化」の名の下に、グローバルな変数空間をカオスなゴミ溜めに変えてしまう。これがスコープ汚染の本質だ。

2. セキュリティの脆弱性:コード注入の脅威

次に、セキュリティの観点だ。`ExecuteGlobal` は、読み込むファイルの内容を「命令」として実行する。

もし、その設定ファイルが、ユーザーが編集可能な共有フォルダに置かれていたり、あるいはデータベースから取得した文字列を対象にしているとしたらどうなるか?

悪意のあるユーザー、あるいは誤った操作によって、設定ファイル内に以下のようなコードが紛れ込んだとする。

' config.vbs に紛れ込んだ悪意あるコード
Set shell = CreateObject("WScript.Shell")
shell.Run "cmd /c del /q C:\CriticalData\*"

`ExecuteGlobal` は、これを疑うことなく実行する。設定値を読み込んでいるつもりで、システムを破壊するコマンドを実行させてしまうのだ。「データ」と「コード」を混同する設計は、セキュリティにおける致命的な欠陥である。

3. アーキテクトが提示する解決策:データとロジックの完全分離

では、どう設計すべきか。答えはシンプルだ。
「設定は『データ』として扱い、ロジックは『オブジェクト』として扱う」。これに尽きる。

`ExecuteGlobal` を捨て、以下の2つのアプローチを使い分けろ。

アプローチA:設定値の読み込みには「Dictionaryオブジェクト」を使う

設定ファイルにはコード(命令)を書かず、純粋なデータのみを記述する。そして、それを `Scripting.Dictionary` オブジェクトに格納して返す関数を設計する。これにより、変数の衝突は物理的に不可能になる。

アプローチB:共通関数には「Classオブジェクト」を使う

関数をグローバルにばら撒くのではなく、一つのクラス(またはオブジェクト)にカプセル化する。これにより、名前空間が保護される。

4. プロダクション・レディな実装例

以下に、実務でそのまま使える、堅牢な設定管理システムのテンプレートを示す。ここでは、設定ファイルを「データ」として読み込み、安全に管理する手法を実装している。

' ==========================================================================
' Script Name: RobustConfigManager.vbs
' Description: ExecuteGlobalを使わずに、安全に設定値をロードする設計例
' ==========================================================================
Option Explicit

' --------------------------------------------------------------------------
' Main Execution Logic
' --------------------------------------------------------------------------
Sub Main()
    Dim config, configFile
    configFile = "config.txt" ' 外部設定ファイル

    ' 1. 設定のロード(データとして取得)
    Set config = LoadConfiguration(configFile)
    
    If config Is Nothing Then
        WScript.Echo "Error: 設定ファイルの読み込みに失敗しました。"
        WScript.Quit 1
    End If

    ' 2. 安全な値の参照
    ' 直接変数にアクセスするのではなく、Dictionary経由でアクセスする
    WScript.Echo "--- システム設定ロード完了 ---"
    WScript.Echo "Target Server: " & config("ServerName")
    WScript.Echo "Timeout Value: " & config("Timeout")
    WScript.Echo "----------------------------"

    ' 3. 存在しないキーへのアクセスに対する防御
    If config.Exists("RetryCount") Then
        WScript.Echo "Retry Count: " & config("RetryCount")
    Else
        WScript.Echo "Retry Count: Default(3) を使用します"
    End If
End Sub

' --------------------------------------------------------------------------
' Function: LoadConfiguration
' Purpose: 設定ファイルを解析し、Dictionaryオブジェクトを返す
' Parameter: strFilePath (設定ファイルのパス)
' Returns: Scripting.Dictionary オブジェクト
' --------------------------------------------------------------------------
Function LoadConfiguration(strFilePath)
    Dim fso, file, line, parts, dict
    Set dict = CreateObject("Scripting.Dictionary")
    Set fso = CreateObject("Scripting.FileSystemObject")

    ' ファイルの存在確認
    If Not fso.FileExists(strFilePath) Then
        Set LoadConfiguration = Nothing
        Exit Function
    End If

    On Error Resume Next ' ファイル読み込み中のエラーを捕捉
    Set file = fso.OpenTextFile(strFilePath, 1) ' 1 = ForReading

    Do Until file.AtEndOfStream
        line = Trim(file.ReadLine)
        
        ' 空行およびコメント行(; で始まる)をスキップ
        If line <> "" And Left(line, 1) <> ";" Then
            ' 「キー=値」の形式を想定
            If InStr(line, "=") > 0 Then
                parts = Split(line, "=")
                ' キーと値をトリミングして格納
                dict.Add Trim(parts(0)), Trim(parts(1))
            End If
        End If
    Loop

    file.Close
    
    If Err.Number <> 0 Then
        WScript.Echo "Error reading config: " & Err.Description
        Set LoadConfiguration = Nothing
    Else
        Set LoadConfiguration = dict
    End If
    On Error GoTo 0
End Function

' 実行
Main

【この設計の優位性】

1. スコープの完全保護: `config` 変数は `Main` スコープ内に閉じている。外部から勝手に変数を書き換えられる心配はない。
2. コード注入の防止: 設定ファイルは `Split` 関数によって「文字列」として処理される。ファイル内に `WScript.Run` と書かれていても、それは単なる「文字列」として扱われ、実行されることはない。
3. 保守性の向上: 設定項目が増えても、`config(“NewKey”)` と呼び出すだけで済む。名前の衝突を恐れて変数名を工夫する必要はもうない。

結論:エンジニアとしての規律

`ExecuteGlobal` は、確かに短期的には開発スピードを上げる。しかし、その代償として、将来の自分(あるいは同僚)がデバッグに費やす膨大な時間を奪うことになる。

優れたアーキテクトは、「動的な便利さ」よりも「静的な安全性」を優先する。

「このコードは、意図しない入力によって暴走しないか?」「この変数の寿命は、誰にコントロールされているか?」

常にこの問いを自分に投げかけろ。それが、プロフェッショナルな自動化エンジニアへの第一歩だ。