セキュリティ・ID

SQLインジェクションとは|文字列を1つ足すだけで、3人分の会員情報が返ってきた

SQLインジェクションは具体的に何が起きているのかエスケープ処理をすれば防げるのか自分のアプリが安全かどうか、どう確かめるのか

検索フォームに名前を入れると、その人の情報が出てくる。よくある機能です。ここに名前ではなく ' OR '1'='1 という文字列を入れると、登録されている全員の情報が返ってくることがあります

今回は実際に手を動かしました。脆弱なコードを書いて動かし、何件返ってくるかを数えています。結果を先に言うと、3人分のデータが返り、その中には管理者アカウントも含まれていました。そこから、修正が1箇所で済むことまでを見ていきます。

この記事の要点

  • 原因は入力を文字列連結でSQLに埋めること。入力が命令として解釈される
  • 実測では、攻撃入力1つで1件→3件(管理者含む)に増えた
  • 対策の第一選択はパラメータ化(プレースホルダ)。エスケープは後回しの手段
  • パラメータ化すると同じ入力でも0件になる。値は命令として読まれない

SQLインジェクションでは何が起きているのか

起きているのは、入力した文字列がデータではなく命令として読まれることです。名前を入れる欄が、そのまま検索条件を書き換える欄になってしまいます。

SQLインジェクションは、アプリがデータベースへ送る命令文(SQL)に、利用者の入力がそのまま混ざってしまう問題です。名前で検索する機能を例にすると、次のような命令文が作られます。

text
SELECT id, name, email FROM users WHERE name = 'Alice'

問題は、この Alice の部分を入力からそのまま埋めている場合です。入力に引用符が含まれていると、条件式そのものを書き換えられます

普通の検索では、何が起きているのか

検索欄に Alice と入れたときは、名前が Alice の行だけが条件に当てはまります。ここまでは意図したとおりの動きです。

引用符ひとつで、条件式の意味が変わる

たとえば ' OR '1'='1 と入力すると、命令文は「名前が空、または 1と1が等しい」という形になります。1と1は常に等しいので、条件は全ての行に当てはまります

同じ入力欄でも、値として扱えばデータのまま。連結すると命令の一部になる。

入力をSQLに文字列連結しているかいいえ入力は値のまま扱われるはい入力に引用符が含まれているかいいえこの入力では成立しないはい全件が返る・データを書き換えられる2つ目が「いいえ」でも安全にはならない。別の入力で成立するだけで、連結している時点で穴は開いている。
図1 ── 入力がデータとして扱われるか、命令として扱われるかの分かれ道

OWASPは、入力値の検証を厳しくする方法と比べて、パラメータ化の方が保守の手間が少なく、安全性の保証も大きいとしています。詳しくは次の節で実際に動かして確かめます。

余談 攻撃に特別な道具は要らない

この記事で使う入力は ' OR '1'='1 という文字列だけです。専用のツールも、通信を書き換える技術も要りません。フォームに文字を打つだけで試せてしまうところが、この脆弱性の厄介な点だと編集部は考えています。

出典OWASP Community「SQL Injection」2026-08-17 確認
parameterized SQL statements require less maintenance and can offer more guarantees with respect to security
原文OWASP Community「SQL Injection」 この内容の有効期限2027-02-17

動かしてみたら、3人分の会員情報が返ってきた

実際に脆弱なコードを書いて動かしました。攻撃の入力を1つ入れただけで、返る件数が1件から3件に増え、その中に管理者が含まれていました。

SQLインジェクションは文章で説明するより動かした方が早いので、手元で試しました。会員が3人(Alice・Bob・Admin)だけの小さなデータベースを作って、名前で検索する関数を2つ用意します。

下のコードは読み飛ばして大丈夫です。違いは1点だけで、片方は入力をSQLに直接つないでいて、もう片方は値として渡していることです。

javascript
// 危険: 入力を文字列連結でSQLに埋める
function findUnsafe(name) {
  const sql = `SELECT id,name,email,is_admin FROM users WHERE name = '${name}'`;
  return db.prepare(sql).all();
}

// 安全: プレースホルダに値を渡す
function findSafe(name) {
  return db.prepare('SELECT id,name,email,is_admin FROM users WHERE name = ?').all(name);
}
text
--- 普通の検索(名前に Alice を入力)---
  組み立てたSQL: SELECT id,name,email,is_admin FROM users WHERE name = 'Alice'
  結果: 1件 Alice
--- 攻撃の入力(名前に ' OR '1'='1 を入力)---
  組み立てたSQL: SELECT id,name,email,is_admin FROM users WHERE name = '' OR '1'='1'
  結果: 3件 Alice, Bob, Admin
--- 同じ入力をパラメータ化した関数へ ---
  結果: 0件 (なし)

返ってきました。3件です。しかも Admin が含まれています。名前を1つ検索したつもりが、会員全員の一覧になりました。

なぜ3件になったのか

組み立てられたSQLを見ると理由が分かります。name = '' OR '1'='1' となっていて、後半が常に成り立つので、全ての行が条件に当てはまります。

一方、同じ入力をパラメータ化した関数に渡すと0件でした。こちらでは ' OR '1'='1 という文字列が、そのまま「名前」として扱われます。そんな名前の人はいないので、何も返りません。

直すのは、値の渡し方だけ

OWASPは、パラメータ化を使うと開発者はSQLを先に全部書き、値は後から渡す形になると説明しています。命令文の形が先に確定するので、後から渡す値がその形を変えることはできません。

  1. 文字列連結でSQLを組み立てている箇所を洗い出す。テンプレート文字列や + でSQLを作っている処理が対象
  2. プレースホルダに置き換える。値は連結せず、実行時に引数として渡す
  3. テーブル名や並び順は許可リストにする。これらはプレースホルダで渡せない
  4. 入力値の検証も続ける。パラメータ化と併用するもので、どちらかで足りるものではない
エスケープ処理は第一の選択肢ではない引用符を打ち消すエスケープ処理でも防げる場合はあります。ただし規則がデータベースごとに違ううえ、書き漏らした1箇所がそのまま穴になります。パラメータ化なら、値が命令として解釈される余地が構造的にありません。失敗したときに安全な側へ倒れる書き方を選んでください。
出典OWASP Cheat Sheet Series「SQL Injection Prevention」2026-08-17 確認
parameterized queries force the developer to define all SQL code first and pass in each parameter to the query later
原文OWASP Cheat Sheet Series「SQL Injection Prevention」 この内容の有効期限2027-02-17

自分のアプリが安全かどうか、どう確かめるのか

確かめ方は単純です。検索欄に攻撃の文字列を入れて、返る件数が増えないかを見るだけ。増えたら、そこに穴があります。

SQLインジェクションの確認で見るべき点は1つだけです。OWASPは、パラメータ化しておけば攻撃者が命令の意図を変えることはできない、としています。つまり「意図が変わらないか」を確かめればよいことになります。

確認の手順は3つ

  1. 検索欄やURLのパラメータに ' OR '1'='1 を入れて送る
  2. 返ってくる件数が増えないかを見る。増えたらその時点で穴がある
  3. エラー画面にSQLの断片が出ていないかも見る。出ていれば、攻撃者に構造の手がかりを与えている

件数が増えず、エラーも出ないなら、その入り口については問題ありません。ただし入力を受け取る場所は1つではないので、検索欄・絞り込み・並び替え・URLの数値と、順に確かめていくことになります。

「対策済み」と言える条件

パラメータ化が済んだと言えるのは3条件すべてを満たしたとき。生SQLの残りが最も多い。

充足 2 / 3文字列連結でSQLを組み立てていない1箇所でも残っていればそこが入口になるテーブル名や並び順は許可リスト方式これらはプレースホルダで渡せないため別の対策が要る攻撃の文字列を入れて確認したコードを読むだけでは生SQLの残りを見落とす3つ目は実行による確認。前2つが設計上そろっていても、実際に入れてみるまでは見落としが残る。
図2 ── SQLインジェクション対策が「済んだ」と言えるかの判定

認可の考え方はIDORの記事でも扱っています。SQLインジェクションが「命令を書き換えられる」問題なのに対し、あちらは「命令は正しいが、誰のデータかを見ていない」問題です。

余談 1回やって終わりにしない

この確認が効くのは、実施した時点までです。機能を足すたびに入力を受け取る場所は増え、そのたびに同じ穴が開きます。手作業の確認は最初の棚卸しに使い、以降はテストコードに任せるのが現実的です。攻撃の文字列を入れて件数を数えるだけなので、自動テストにも落としやすい部類です。

出典OWASP Cheat Sheet Series「SQL Injection Prevention」2026-08-17 確認
prepared statements ensure that an attacker cannot change the intent of a query, even if SQL commands are inserted by an attacker
原文OWASP Cheat Sheet Series「SQL Injection Prevention」 この内容の有効期限2027-02-17

よくある質問

エスケープ処理をすれば防げませんか?
防げる場合もありますが、OWASPは第一の対策としてパラメータ化を挙げています。エスケープは対象のデータベースごとに規則が違い、書き漏らした1箇所がそのまま穴になるためです。パラメータ化なら、値がSQLの命令として解釈される余地が構造的にありません。
ORMを使っていれば安全ですか?
基本的な検索や更新であれば、多くのORMが内部でパラメータ化を行います。ただし、生のSQLを書ける機能(raw query)を使った箇所は自分で書いたのと同じ扱いになります。安全なのはORMだからではなく、パラメータ化されているからです。
入力値の検証だけでは足りませんか?
足りません。検証は入力の形式を確かめる処理で、SQLの組み立て方は変えないためです。OWASPも、検証は有用だが単独の対策にはならないとしています。検証とパラメータ化は両方行うものです。
テーブル名や並び順を動的に変えたいときは?
テーブル名や列名はプレースホルダで渡せません。この場合は、許可する値の一覧をコード側に持ち、入力がその中にあるときだけ使う方法をとります。入力をそのままSQLに埋めない、という原則は変わりません。

まとめ

  • SQLインジェクションは入力がSQLの命令として解釈されることで起きる
  • 実測では、' OR '1'='1 の1入力で全3件(管理者含む)が返った
  • 対策の第一選択はパラメータ化。同じ入力で0件になることを確認済み
  • 生SQLを書いている箇所を全件洗い出すのが最初の作業

今日から始められること

  1. SQLを文字列連結で組み立てている箇所を全件洗い出す
  2. 洗い出した箇所をプレースホルダ(パラメータ化)に置き換える
  3. テーブル名や並び順を動的に変えている箇所は、許可リスト方式に変える
  4. 検索フォームに ' OR '1'='1 を入れて、件数が増えないことを確かめる

実務で組んだSQLインジェクションのワークフローには、値段が付きます

同じ課題を持つ会社にとって、動いている設定は「作る時間」を買えるということです。ServiceDockは自作のワークフローやテンプレートを出品できるマーケットプレイスです。手数料や出品の流れは出品者向けページにまとまっています。

出品の仕組みを見る