SQLインジェクション — 検索欄に「命令」を書くと、全件が返る

検索窓や入力欄に「命令」を混ぜて送ると、データベースがそれを命令として実行してしまう穴。原因は入力を「文の一部」として貼り付けるコード。直すのは入力を「値」として渡す(プレースホルダ)書き方1つ。2026年8月の Metabase の事故、AIが危ない形を書く場所、確かめ方。

動画で見る

結論から

SQLインジェクションは、検索窓や入力欄に「命令」を混ぜて送ると、データベースがそれを命令として実行してしまう穴です。20年以上前からある古典的な穴ですが、2026年8月にも BI ツール Metabase のクラウド版がこの穴で侵入され、複数の企業が被害を公表しました。AIが書くコードにも、同じ形はよく出てきます。

この記事で伝えたいのは、この一点だけです。入力を「文の一部」として貼り付けるコードが穴。入力は「値」として渡す(プレースホルダ)。 直すのは書き方1つで、AIに一言で頼めます。

  • 何が起きるか:検索欄に ' OR 1=1 -- と打つと全件が出る。'; DROP TABLE users; -- なら表が消える。ログイン欄なら、パスワードなしで入れる
  • なぜ:"SELECT * FROM users WHERE name = '" + 入力 + "'" のように、入力を文字列でつなぐと、入力の中の ' が文の区切りになる
  • 直す:入力を文につながず、? や $1 の穴(プレースホルダ)に「値」として渡す。Supabase の通常の呼び出し(.from().select())や ORM は最初からそうなっている。危ないのは raw rpc sql\…\`` で自分で文を組んでいる所
  • 確かめる:自分の検索欄に '(シングルクォート1つ)を入れる。エラー画面が出たら要注意

動画版(約2分半)は本文の上に埋め込んであります。

① 何が起きるのか:検索欄に「命令」を書く

例として、AIで作った予約アプリに「顧客名で検索」の機能があるとします。裏側でデータベースに送られる文(SQL)はこうです。

SELECT * FROM customers WHERE name = '佐藤'

検索欄に 佐藤 と打てば '佐藤' の部分が入れ替わるだけです。では、検索欄にこう打ったら。

' OR 1=1 --

文はこうなります。

SELECT * FROM customers WHERE name = '' OR 1=1 --'

「名前が空、または 1=1(常に真)」なので、全顧客が返ってきます。-- 以降はコメント扱いで無視されます。検索欄に打ったのは「値」のつもりでも、データベースは「命令の続き」として読んだ。これが SQLインジェクションです。

同じ要領で、'; DROP TABLE customers; -- なら表そのものが消え、ログイン欄の ' OR '1'='1 ならパスワードを知らなくても入れることがあります。

入力を「文の一部」として貼ると穴。入力は「値」として渡す

② なぜ起きるのか:入力を「文」につないでいる

原因はコードの書き方1つです。

穴のある書き方(入力を文字列でつなぐ)

"SELECT * FROM customers WHERE name = '" + name + "'"

穴のない書き方(プレースホルダに値として渡す)

"SELECT * FROM customers WHERE name = $1", [name]

後者では、name に何が入っていても「1つの値」として扱われ、' も OR も文の一部にはなりません。IPA の「安全なウェブサイトの作り方」も、対策の根本としてバインド機構(プレースホルダ)を挙げています。エスケープ(' を無害化する処理)は次善で、抜けが出ます。

AIはどちらを書くか。 ほとんどの場合、後者です。Supabase の supabase.from('customers').select().eq('name', name) や Prisma・Drizzle のような ORM は、最初からプレースホルダ方式です。危ないのは、AIが「複雑な検索」「集計」「全文検索」を頼まれて、自分で SQL 文を組み立てるときです。supabase.rpc() に渡す関数の中、sql\SELECT … ${name}\` のようなテンプレート、.raw() executeRaw` と付く呼び出し。ここで文字列連結が出ます。「動く」ので、AIも気づきません。

これは古典的な穴ですが、無くなっていません。2026年8月6日、BI ツール Metabase のクラウド版が、パスワードリセットのAPIに隠れていた SQLインジェクション(CVE-2026-72898)で侵入されました。攻撃者が送った値が、そのまま SQL に埋め込まれる形でした。Framework、Tally、n8n、Anaconda など複数の企業が、これを起点とした被害を公表しています。プロのチームが作った製品でも起きる、ということです。

③ どう直すのか:AIに「プレースホルダで」と頼む

以下をそのまま AI ツールに貼ってください。

「このプロジェクトで SQL 文を組み立てている箇所をすべて列挙してください(raw、rpc に渡す関数、sql テンプレート、executeRaw、文字列連結で SELECT/INSERT/UPDATE/DELETE を作っている所)。ユーザーの入力が文字列として連結されている箇所は、すべてプレースホルダ(パラメータバインド)に書き換えてください。テーブル名や並び順など、プレースホルダにできない部分は、許可リストで固定してください。変更前後の一覧も出してください。」

ポイントは2つ。「すべて列挙」と言う(AIは頼まれた1か所しか見ない)。「テーブル名・並び順は許可リスト」と言う(ORDER BY ${sort} のような、プレースホルダにできない場所が残りやすい)。

Supabase を使っている場合は、加えて RLS を確認してください(→ RLS の記事)。SQLインジェクションで「全件を取る命令」が通っても、RLS が「自分の行だけ」に絞っていれば、被害は本人の分で止まります。RLS は最後の砦です。

④ 直ったかを確かめる:シングルクォートを1つ

  • 検索欄・ログイン欄・URLの `?q=` に `'`(シングルクォート1つ)を入れる。データベースのエラー画面(syntax error、unterminated quoted string)が出たら ✗。「見つかりません」「0件」なら ◯の可能性が高い
  • `' OR 1=1 --` を入れる。全件出たら ✗
  • エラー画面に SQL 文や内部情報が出ないか。出るなら、直し方のヒントを攻撃者に渡している状態(→ 20項目の18番)
  • 分からなければ、AIに「検索欄に ' OR 1=1 -- を入れるテストを書いて、全件が返らないことを確認して」

自分で直せないとき:エンジニアに送る一文

「検索機能に ' を入れるとデータベースのエラーが出ます。SQL を文字列連結で組み立てている箇所を全部洗い出して、プレースホルダに直してください。Supabase の RLS も合わせて確認をお願いします。」

よくある質問

Supabase を使っています。SQL を書いた覚えがありません。

.from().select() だけなら、この穴はまず開きません。AIに「このプロジェクトに raw SQL や rpc 関数はある?」と聞いて、無ければ次のチェックへ。あれば③のプロンプトを貼ってください。

入力欄で `'` を禁止すれば済みますか。

済みません。「O'Brien」さんが登録できなくなりますし、' 以外にも文を壊す方法があります。入力を制限するのではなく、受け取り方(プレースホルダ)を直します。

NoSQL(Firebase・MongoDB)なら無関係ですか。

SQL ではなくても、入力を「命令」として解釈する穴(NoSQL インジェクション)はあります。考え方は同じで、入力を命令の構造に混ぜない、です。

この穴が塞がれば、データベースは安全ですか。

「命令を混ぜられない」だけです。「誰が読めるか」(RLS・本人確認)は別の話で、そちらのほうが AI 製アプリでは頻度が高い穴です(→ URLの数字・/admin・RLS)。