SQLインジェクションを「過去の遺物」にする:動的クエリの誘惑を断ち切る極意
エンジニア諸君。セキュリティの現場で長年戦っていると、最新のAIハッキングツールよりも、結局のところ「15年前と全く同じSQLインジェクション」で沈没するシステムがいかに多いかに驚かされる。
「うちはフレームワークを使っているから大丈夫」「バリデーションを入れているから平気」……その慢心こそが、データ侵害の入り口だ。今日は、SQLインジェクション(SQLi)の根本的なメカニズムと、なぜ「プリペアドステートメント」だけが唯一の正解なのか、現場の泥臭い視点で解説する。
—
1. なぜSQLiは消えないのか?:動的SQL生成という「劇薬」
SQLインジェクションの根本原因は、「プログラムの命令(SQL)」と「外部からの入力(データ)」を同一の文字列として混ぜ合わせることにある。
例えば、こんなコードを書いたことはないか?
// 最悪のパターン:文字列結合によるSQL生成
$sql = “SELECT FROM users WHERE username = ‘” . $_POST[‘username’] . “‘;”;
攻撃者はここに `’ OR ‘1’=’1` を送り込む。すると生成されるクエリは `SELECT FROM users WHERE username = ” OR ‘1’=’1′;` となり、認証は完全にバイパスされる。これがSQLiの基本にして、全攻撃の出発点だ。
攻撃者の視点:なぜ「クエリ分離」が必要なのか
攻撃者は、データベースのメタデータ(`information_schema`)を読み取り、テーブル構造を解析し、最終的には `UNION SELECT` や `LOAD_FILE` を駆使してOSコマンド実行までたどり着こうとする。彼らは「SQL構文」と「データ」の境界線が曖昧な場所を徹底的に突いてくる。だからこそ、境界線を物理的に分離する(=プリペアドステートメント)以外に、根本的な解決策は存在しないのだ。
—
2. 実装:プリペアドステートメントによる完全防御
プリペアドステートメントの真髄は、「SQLの雛形(テンプレート)」を先にデータベースへ送り、あとから「データ」を流し込む点にある。これにより、入力データがどれほど悪意に満ちていても、データベースエンジンはそれを「ただの文字列」としてのみ扱い、決して「実行命令」として解釈することはない。
Python (psycopg2/PostgreSQL) での実装例
多くのエンジニアがやりがちな `f-string` でのSQL構築は直ちにやめること。以下が正しい実装だ。
import psycopg2
接続設定(環境変数から読み込むのが定石)
conn = psycopg2.connect(dsn=”…”)
cur = conn.cursor()
user_input = “hacker_name”
ポイント:SQL内には %s (プレースホルダ) を書くだけ
query = “SELECT id, email FROM users WHERE username = %s;”
実行時にデータを分離して渡す
これにより、%sの中身がどんな文字列であってもSQLとして解釈されない
cur.execute(query, (user_input,))
result = cur.fetchone()
JavaScript (Node.js/pg) での実装例
非同期処理が主体のNode.jsでも考え方は同じだ。
const { Client } = require(‘pg’);
const client = new Client();
async function getUser(username) {
// $1, $2 はプレースホルダ。ここには直接変数を埋め込まない
const query = ‘SELECT FROM users WHERE username = $1’;
const values = [username];
try {
const res = await client.query(query, values);
return res.rows[0];
} catch (err) {
console.error(err.stack);
}
}
—
3. インフラ層での多層防御(念押し)
アプリケーションコードの修正が基本だが、万が一の脆弱性混入に備え、防御層(Defense in Depth)を構築しておくのがプロの仕事だ。
WAF (AWS WAF) によるSQLiシグネチャ検知
AWS WAFの「SQL インジェクション攻撃グループ」を有効化するだけで、標準的な攻撃パターンの多くをブロックできる。
// AWS WAF のルールの考え方
{
“Name”: “SQLi-Block-Rule”,
“Statement”: {
“SqliMatchStatement”: {
“FieldToMatch”: { “AllQueryArguments”: {} },
“TextTransformations”: [
{ “Priority”: 0, “Type”: “URL_DECODE” },
{ “Priority”: 1, “Type”: “HTML_ENTITY_DECODE” }
]
}
},
“Action”: { “Block”: {} }
}
データベース側の最小権限原則
これが最も重要だ。アプリケーションが接続するDBユーザーには、必要最小限の権限(GRANT)しか与えてはならない。`DROP TABLE` や `GRANT ALL` ができる権限でWebアプリを動かすのは、鍵のかかっていない金庫を街中に置くのと同じことだ。
- `SELECT`, `INSERT`, `UPDATE` のみに制限する
- `information_schema` へのアクセスを制限する
- OSコマンド実行機能(PostgreSQLの `COPY FROM PROGRAM` など)を無効化する
—
最後に:なぜ「コピペ」だけでは不十分なのか
ここまでコードを示したが、最後に一つだけ忠告しておく。どんなに優れたコードも、「動的生成の誘惑」には勝てない時がある。「複雑なクエリだから」「パフォーマンスのためにどうしても文字列結合が必要」という言い訳が、脆弱性の温床になる。
「SQLは文字列結合してはならない」。このルールをチームの憲法として徹底すること。そして、CI/CDパイプラインに「SAST(静的解析ツール)」を組み込み、文字列結合の兆候があれば即座にビルドを失敗させる環境を作ること。
セキュリティはツールではなく「規律」だ。今日のコードを、君のプロジェクトの強固な礎にしてほしい。

コメント