SQLインジェクションを「過去の遺物」にする:プリペアドステートメントの本質的理解
現場でコードレビューをしていると、未だに「入力値をエスケープすれば大丈夫だろ?」という甘い考えで書かれたクエリに出くわすことがある。はっきり言おう。エスケープ関数への依存は、セキュリティの「自転車操業」だ。 一箇所でもエスケープ漏れがあれば、そこからシステムは崩壊する。
今日は、SQLインジェクション(SQLi)という、Webセキュリティにおける「最も原始的でありながら、最も致命的な脆弱性」を、根こそぎ断ち切るための技術――プリペアドステートメントについて、現場の視点から深掘りする。
—
1. なぜ「エスケープ」では不十分なのか?
攻撃者は「SQLの構文とデータが混ざり合う境界線」を狙う。例えば、ユーザー入力をそのまま文字列結合するコードは、こんな惨事を招く。
/ 脆弱なクエリの裏側 /
SELECT FROM users WHERE username = ‘$user_input’;
攻撃者が admin' -- と入力したらどうなるか。クエリは SELECT FROM users WHERE username = 'admin' -- '; となり、以降のパスワードチェックはすべて無効化され、認証がバイパスされる。
エスケープ処理(mysqli_real_escape_string 等)は、この入力文字の一部を無力化しようとする試みだが、文字コードの誤認や、開発者の「うっかり(エスケープ忘れ)」を防ぎきれない。人間がエラーをゼロにすることは不可能だ。 だからこそ、人間が介入する余地のない「構造的な防御」が必要になる。
—
2. プリペアドステートメントの「境界線」理論
プリペアドステートメント(準備されたステートメント)が強力なのは、「クエリの骨組み」と「データ」を完全に分離してデータベースに送るからだ。
1. コンパイル(準備): SQLのテンプレート(SELECT FROM users WHERE id = ?)を先にデータベースへ送信し、実行計画を立てる。
2. バインド(紐付け): 後から送られてきたデータ(105 OR 1=1)を、あくまで「値」としてのみ扱う。
データベースは、後から送られてきたデータがどんなに凶悪なSQLコマンドを含んでいても、それを「命令」として解釈することはない。ただの「文字列」として検索対象にするだけだ。これが、脆弱性が物理的に発生し得ない構造的な理由である。
—
3. 実践:セキュアな実装コード例
現場で即戦力となる、各言語の推奨実装を記す。これ以外の「自作エスケープ」は今すぐ廃止してほしい。
PHP (PDOを使用)
PHPで mysqli_ 系の関数を直接使うのはもうやめよう。PDO一択だ。
PDO::ERRMODE_EXCEPTION, // エラーは例外で投げる
PDO::ATTR_EMULATE_PREPARES => false, // 本物のプリペアドステートメントを強制
]);
$user_id = $_GET[‘id’];
// 1. プレースホルダ(?)を使ったクエリを用意
$stmt = $pdo->prepare(“SELECT username, email FROM users WHERE id = ?”);
// 2. データをバインドして実行
$stmt->execute([$user_id]);
// 3. 結果の取得
$user = $stmt->fetch();
?>
Python (sqlite3 / psycopg2)
Pythonも同様に、ライブラリが提供するパラメータバインディング機能を使う。
import sqlite3
接続
conn = sqlite3.connect(‘app.db’)
cursor = conn.cursor()
user_input_id = “105”
プレースホルダに ? を使用
query = “SELECT username FROM users WHERE id = ?”
実行時にタプルとして渡す(ここが重要)
cursor.execute(query, (user_input_id,))
print(cursor.fetchone())
—
4. 現場のチーフとして伝えたい「守りの要点」
コードを直すだけでは不十分な場合もある。インシデントを未然に防ぐための最後の砦を共有しておく。
PDO::ATTR_EMULATE_PREPARES => false:
PHPのPDOでは、デフォルトで「エミュレーション」が有効になっている場合がある。これだと、擬似的にプリペアドステートメントを再現しているだけで、DBレベルでの分離が甘い。必ず false に設定し、DBエンジン側に直接クエリを解析させること。
- 最小権限の原則:
アプリケーションが接続するDBユーザーには、DROP TABLE や GRANT などの権限を与えてはならない。SELECT, INSERT, UPDATE のみに絞る。もしSQLiが突破されたとしても、被害は最小限に抑えられる。
- WAFの多層防御:
アプリケーションコードの修正が完了するまでの「応急処置」として、AWS WAFなどのシグネチャベースの防御は有効だ。しかし、WAFはあくまで「すり抜け」があり得る。WAFは「傘」であり、アプリの堅牢性は「家そのもの」。 傘があるからといって、家の屋根に穴を開けたままにするエンジニアはいないはずだ。
—
最後に:エンジニアとしての矜持
「動けばいい」コードは、誰にでも書ける。しかし、あなたの書いたコードが数年後に脆弱性の温床となり、顧客のデータ流出を招くとしたら?
セキュリティは「機能」ではなく「品質」だ。プリペアドステートメントの使用は、プロフェッショナルであれば議論の余地すらない前提条件である。今日から、プロジェクト内の全SQLクエリをチェックしてくれ。クリーンなコードで、強固なシステムを構築しよう。それが、我々エンジニアの誇りだ。
コメント