SQL インジェクションとは?プレースホルダーで防ぐ理由と、DB の権限を絞る方法(SQLite・PostgreSQL)

PR
SQL インジェクションとは?プレースホルダーで防ぐ理由と、DB の権限を絞る方法(SQLite・PostgreSQL)
この記事は約33分で読めます。

SQL インジェクションは、アプリが DB(データベース)に送る SQL(DB への命令文)を、利用者の入力をつなげて組み立てるときに起きる問題です。根本の防ぎ方は、SQL の中の値を「プレースホルダー」に任せて、文字列をつないで SQL を作らないことです。さらに、アプリが DB につなぐユーザーの権限を絞っておくと、破られたときの被害を小さくできます。ただし、こちらは根本の対策の代わりにはなりません。

この記事は、DB を使うアプリを作り始めた人向けです。SQL インジェクションがなぜ起きるのかを、IPA(情報処理推進機構)と OWASP(Web アプリの安全を扱う非営利の団体)の文書に沿って整理します。そのうえで、プレースホルダーの使い方、プレースホルダーにできない所の考え方、DB の権限の絞り方を、手元の出力つきで説明します(2026年10月時点)。

手元の出力は、macOS 14.4.1・Python 3.12.3(中の SQLite は 3.43.2)・Docker 28.3.2 で動かした PostgreSQL 18.6 で取りました。PostgreSQL の最新のマイナー版は、2026年10月時点で 18.6 です。エラーの文面は、版によって違うことがあります。

この記事には、攻撃に使える入力は載せていません。仕組みは、人名 O'Reilly から組み上がる SQL と、IPA の説明で見ます。

この記事でわかること
  • SQL インジェクションとは何か、なぜ起きるのか(値が「リテラルの外」にはみ出す、という見方)
  • プレースホルダーで防ぐ理由と、Python(SQLite)での書き方
  • テーブル名・列名・並び順はプレースホルダーにできないこと。エスケープの扱いが IPA と OWASP で違うこと
  • DB ユーザーの権限を絞る考え方と、PostgreSQL(GRANT)・SQLite(mode=ro)で確かめた結果
  • 2026年の JPCERT/CC の注意喚起にある実例(Metabase・Cisco Secure Email Gateway)と、書かれていないこと
  1. SQL インジェクションとは?
  2. なぜ起きる?文字列をつなぐと、値が「リテラルの外」にはみ出す
    1. 人名 O'Reilly で、SQL が壊れる
    2. はみ出した部分が、SQL の続きとして読まれる
    3. 画面にエラーをそのまま出さない
    4. 文字列連結の何が問題か
  3. プレースホルダーで防ぐ理由は?
    1. なぜ、これで防げるのか
    2. プレースホルダーを使っていても、気をつける所
  4. プレースホルダーにできない所と、エスケープの扱い
    1. エスケープは、IPA と OWASP で扱いが違う
    2. その他の IPA の対策
  5. DB ユーザーの権限を絞るには?(PostgreSQL と SQLite で確かめる)
    1. 権限を絞る考え方(OWASP の Least Privilege)
    2. PostgreSQL:SELECT だけを渡したユーザーを作る
      1. 権限を渡すときの注意(PostgreSQL の文書より)
    3. SQLite:読み取り専用で開く(mode=ro)
  6. 2026年に、SQL インジェクションの注意喚起はあった?(2026年10月時点)
    1. Metabase の件(JPCERT-AT-2026-0023)
    2. Cisco Secure Email Gateway の件(JPCERT-AT-2026-0027)
    3. この 2 件から、言えないこと
    4. 過去の例:MOVEit Transfer(2023年)
  7. よくある質問
    1. プレースホルダーを使えば、攻撃の入力も防げますか?
    2. ORM を使えば、SQL インジェクションは起きませんか?
    3. DB の権限を絞れば、SQL インジェクションを防げますか?
    4. Metabase や Cisco の件は、文字列連結が原因でしたか?
    5. SQLite にも、ユーザーと GRANT はありますか?
  8. まとめ
  9. 参考資料

SQL インジェクションとは?

IPA は、次のように説明しています。DB と連携した Web アプリの多くは、利用者の入力をもとに SQL 文を組み立てます。その組み立て方に問題があると、攻撃によって DB を不正に使われることがあります。この問題が「SQL インジェクションの脆弱性」で、それを悪用する攻撃が「SQL インジェクション攻撃」です。OWASP は、クライアントからアプリへの入力を通して SQL を差し込む(inject する)ことだと説明しています。

起きる条件は、OWASP によると 2 つです。

  1. 信用できない所からのデータが、プログラムに入ってくる
  2. そのデータを使って、SQL を動的に(実行のたびに)組み立てている

IPA が挙げる「起こりうる被害」は、次のとおりです。

  • DB に入れてある非公開の情報を見られる(個人情報の漏えいなど)
  • DB の情報を書き換えられる・消される
  • 認証を避けて、不正にログインされる
  • ストアドプロシージャ(DB の中に保存しておく処理)などを使って、OS のコマンドを動かされる

IPA は、運営している組織やサイトの性質を問わず、DB を使う Web アプリがあるサイトなら起きうる問題だと書いています。個人情報のような重要な情報を DB に入れているサイトは、特に注意が必要です。

OWASP Top 10:2025(Web アプリの危ない点を並べた一覧の 2025 年版)では、SQL インジェクションは「A05:2025 Injection」に入っています。インジェクションは、前の版(2021)の 3 位から 5 位に下がりました。A05 の説明は、SQL インジェクションを「頻度は低いが、影響が大きい」ものとして挙げ、その CVE(脆弱性に付く番号)は 1 万 4 千件を超えると書いています。数え方はこの版のデータの集計によります。

OWASP は、この攻撃が、SQL が「命令」と「データ」をはっきり分けていないことに付け込むものだと説明しています。データのところに、命令の一部を紛れ込ませるのです。次の節で、これを具体的な例で見ます。

なぜ起きる?文字列をつなぐと、値が「リテラルの外」にはみ出す

起きる仕組みは、IPA の別冊「安全なSQLの呼び出し方」の説明がわかりやすいので、それに沿って見ます。

まず言葉を 2 つ。SQL 文は、キーワード・演算子・識別子(テーブルや列の名前)・リテラルなどでできています。リテラルは、'YAMADA' や 20 のように、SQL 文の中に直接書く値(定数)のことです。文字のリテラル(文字列リテラル)は、全体をシングルクォート(')で囲んで書きます。

人名 O'Reilly で、SQL が壊れる

IPA は、人名 O'Reilly を次のように SQL に書くと、構文エラーになると説明しています。

SELECT * FROM employee WHERE name = 'O'Reilly'

'O' までで文字列リテラルが終わってしまい、後ろの Reilly' がリテラルの外にはみ出すからです。文字としてのシングルクォートを書くには、'O''Reilly' のようにクォートを 2 つ重ねる(エスケープする)のが SQL の文法です。なお、エスケープが必要な文字はシングルクォートだけとは限らず、DB エンジンの種類や設定によって違う、とも IPA は書いています。

手元の Python(SQLite)で、同じことを確かめました。まず shop.db に users テーブルを作り、Tanaka と O'Reilly の 2 人を入れます。次に、フォームから O'Reilly が来たつもりで、文字列をつないで SQL を作ります。よくない書き方です。

import sqlite3

con = sqlite3.connect("shop.db")
con.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
con.executemany("INSERT INTO users (name) VALUES (?)", [("Tanaka",), ("O'Reilly",)])
con.commit()

name = "O'Reilly"  # フォームから来た名前のつもり

# 文字列をつないで SQL を組み立てる(よくない書き方)
sql = "SELECT id, name FROM users WHERE name = '" + name + "'"
print("組み上がった SQL:", sql)
try:
    print(con.execute(sql).fetchall())
except sqlite3.Error as e:
    print(type(e).__name__ + ":", e)

(流したファイルには、接続の直後に前回の表を消す 1 行と、最後に con.close() がありますが、ここでは省きました。2 回目に流すときは、shop.db を消してから流してください。)出力は次のとおりです。

組み上がった SQL: SELECT id, name FROM users WHERE name = 'O'Reilly'
OperationalError: near "Reilly": syntax error

組み上がった SQL は、IPA の例と同じ形です。'O' でリテラルが終わり、 Reilly' がはみ出して、構文エラーで止まりました。この出力は、macOS・Python 3.12.3(SQLite 3.43.2)のものです。

はみ出した部分が、SQL の続きとして読まれる

IPA は、SQL インジェクションを次のように定義しています。パラメータ(SQL の中の変えられる部分)を実際の値に展開するときに、リテラルとして文法的に正しく作らないと、値がリテラルの外にはみ出します。はみ出した部分は、リテラルの後ろに続く文として解釈されます。この現象が SQL インジェクションです。

つまり、O'Reilly で起きたのは、うっかり入ったクォートで SQL が壊れる、という失敗です。IPA は、文字列のリテラルの中に別の SQL 文の断片を含ませることで、元の SQL 文の意味を変えられる場合がある、と書き、はみ出した部分で元の SQL 文の意味を変えられる場合を「SQL インジェクションの脆弱性」と呼んでいます。この記事では、その断片の例は載せません。

OWASP の SQL Injection のページにも、同じ種類の「壊れるだけ」の例があります。名前に evil'ex を入れると、組み上がるクエリのクォートが合わなくなり、DB が構文エラーを返す、というものです。

画面にエラーをそのまま出さない

ここで出したエラーの文面は、手元の練習で見るためのものです。IPA は「保険的対策」のひとつとして、DB のエラーメッセージをそのままブラウザに表示しないことを挙げています。エラーの内容に、DB の種類・エラーの原因・実行に失敗した SQL 文などが含まれると、攻撃の手がかりになりうるからです。

文字列連結の何が問題か

IPA は、アプリから SQL を呼ぶ方法を、「文字列連結による組み立て」と「プレースホルダによる組み立て」の 2 つに分けています。文字列連結には、アプリを作る人のミスでエスケープが漏れる危険があります。さらに、エスケープすべき文字が DB エンジンの種類や設定によって違うため、それに合わせて作るのが容易でない、という問題もあります。Python の文書も、Python の文字列の操作で SQL を組み立てると、SQL インジェクションに弱くなるので気をつけるように書いています。

プレースホルダーで防ぐ理由は?

IPA の「根本的解決」の 1 つ目は、SQL 文の組み立てを、全てプレースホルダーで実装することです。OWASP の SQL インジェクション対策の Cheat Sheet(対策をまとめた文書)の一番目の守りも、プリペアドステートメント(パラメータ化クエリ)です。Python の sqlite3 の文書は、値を SQL に入れるときは、文字列の書式化ではなく、必ずプレースホルダーを使うように書いています。

プレースホルダーは、SQL の雛形の中で、値が入る場所を示す記号です。雛形を先に書いておき、実際の値は、あとから別に渡します。この「値を割り当てる」ことを、バインドと呼びます。

先ほどの O'Reilly を、プレースホルダー(?)で渡すと、次のようになります。

# プレースホルダー(?)で値を渡す
sql2 = "SELECT id, name FROM users WHERE name = ?"
print("SQL の雛形:", sql2, " 値:", repr(name))
print(con.execute(sql2, (name,)).fetchall())
SQL の雛形: SELECT id, name FROM users WHERE name = ?  値: "O'Reilly"
[(2, "O'Reilly")]

雛形は ? のままで、値は execute() の 2 つ目の引数に、別にタプル(値を並べたもの)で渡しています。文字列連結のように、値のまわりにクォートを自分で書く場面はありません。同じ名前で、エラーにならずに id が 2 の O'Reilly が返りました。Python の sqlite3 の文書によると、プレースホルダーの書き方は、? を使う書き方(qmark)と、名前を付ける書き方(named)の 2 種類です。

なぜ、これで防げるのか

IPA の別冊は、プレースホルダーを 2 つの方式に分けて説明しています。

静的プレースホルダー動的プレースホルダー
値を割り当てる場所DB エンジンの側アプリ側のライブラリの中(エスケープして、はめ込む)
流れプレースホルダーのままの SQL を先に DB エンジンへ送り、構文の準備を済ませる。実行のときに、値だけを送るライブラリが、値をエスケープして雛形にはめ込む
SQL インジェクション脆弱性が生じない(IPA の説明)ライブラリの実装に問題があると、生じる可能性を否定できない(IPA の説明)

静的プレースホルダーは、SQL の ISO/JIS 規格では「準備された文(Prepared Statement)」と呼ばれます。IPA は、静的プレースホルダーでは、SQL を準備する段階で構文が確定し、あとから変わらないので、パラメータの値がリテラルの外にはみ出す現象が起きない、と説明しています。構文がバインドの前に決まるので、値をクォートする必要も、エスケープする必要もありません。IPA は、セキュリティの面で最も安全だと書いています。

OWASP の言い方も同じ向きです。プリペアドステートメントでは、SQL のコードを全部先に書いて、各パラメータをあとから渡します。こうすると、入力が何であっても、DB はコードとデータを必ず区別します。たとえ SQL の命令を入れられても、クエリの意図は変えられない、と OWASP は書いています。

IPA は、動的プレースホルダーを俗に「クライアントサイドのプリペアドステートメント」と呼ぶことがあるが、JIS/ISO の「準備された文」ではないので注意が必要、とも書いています。使うライブラリがどちらの方式なのかは、そのライブラリの文書で確かめてください。この記事では、Python の sqlite3 がどちらの方式かは確かめていません。

この記事で、手元で流して確かめたのは、O'Reilly を文字列連結で渡すと SQL が壊れ、プレースホルダーで渡すと検索できる、という差までです。攻撃の入力は流していません。「プレースホルダーなら止まる」という理由の説明は、IPA と OWASP の文書によるものです。

プレースホルダーを使っていても、気をつける所

  • ストアドプロシージャの中: OWASP Top 10:2025 の A05 は、パラメータ化していても、ストアドプロシージャの中(PL/SQL や T-SQL)でクエリとデータをつなぐと、SQL インジェクションが起きうると書いています。EXECUTE IMMEDIATE や exec() で、信用できないデータを実行する場合も同じです。
  • ORM: ORM(DB の操作をプログラムの言葉で書けるようにする仕組み)に移ることも、A05 は安全な API の選択肢に挙げています。一方で、A05 は「ORM の検索の値に、無害化していないデータを使う」ことも、当てはまる条件として挙げています。ORM の裏でどんな SQL が出るかをログで見る方法は、Rails の N+1 問題の記事で扱っています。
  • 数値: IPA は、PHP や Perl のように変数に型がない言語では、数値のつもりの変数に、数値以外の文字が入ると文字列として扱われるので注意が必要と書いています。静的プレースホルダーなら、数値リテラルもそのまま適切にバインドされます。

Python で Web API を作る入門は、FastAPI の記事にあります。API から DB につなぐ部分を書くときに、この節の書き方を使ってください。

プレースホルダーにできない所と、エスケープの扱い

プレースホルダーで渡せるのは、SQL の中の「値」です。テーブル名・列名・並び順(ASC か DESC か)のように、SQL の「構造」にあたる所は、値としてバインドできません。OWASP の Cheat Sheet は、そういう所には、入力の検証か、クエリの作り直しがもっとも適切な守りだと書いています。テーブル名や列名が要るときは、できれば利用者から来る値ではなく、コードの側から来るようにします。

OWASP Top 10:2025 の A05 も、テーブル名や列名などの SQL の構造はエスケープできないので、利用者が渡した構造の名前を使うのは危険だと書いています。帳票を作るソフトでよくある問題だそうです。

入力の検証(許可リストなどで、入ってよい値を決めて確かめること)について、OWASP は 2 つのことを書いています。

  • 検証は、バインドできない所では主な守りになる。ほかの守りに加えて、二次的な守りにもなる
  • ただし、検証したデータでも、文字列の組み立てで SQL に入れてよいとは限らない。注意して進める

A05 も、サーバー側の入力の検証は、完全な守りではないと書いています。テキストの入力欄やモバイルアプリの API のように、特殊な文字が要るアプリが多いからです。

エスケープは、IPA と OWASP で扱いが違う

文字列連結で SQL を組み立てるときに、特殊な文字を無害にする処理がエスケープです。この扱いは、IPA と OWASP で違います。

IPAOWASP
一番目の対策プレースホルダー(根本的解決の 1-(i)-a)プレースホルダー(プリペアドステートメント)
エスケープ文字列連結で組み立てるなら、エスケープなどをする DB エンジンの API で、リテラルを正しく作る(根本的解決の 1-(i)-b)。外部の入力だけでなく、SQL 文を作る全てのリテラルに対して行う「STRONGLY DISCOURAGED」(強く勧めない)。DB ごとに実装が違い、ほかの守りより壊れやすく、全ての場面で防げるとは保証できない
新しく作るなら—パラメータ化クエリ・ストアドプロシージャ・ORM で作る(作り直す)

食い違って見えますが、まずプレースホルダーを使うという点は、IPA も OWASP も同じです。IPA はエスケープを「次の手」に置き、OWASP は勧めません。この記事では、プレースホルダーが使える所では、迷わずプレースホルダーを使う、という立場で書きます。

その他の IPA の対策

IPA の「根本的解決」には、もう 1 つあります。Web アプリに渡すパラメータに、SQL 文を直接書かないことです。IPA は「論外」の実装としながら、hidden パラメータ(画面に出ないフォームの項目)に SQL 文をそのまま入れている事例の届出があったので、避けるべき例として紹介しています。

DB ユーザーの権限を絞るには?(PostgreSQL と SQLite で確かめる)

プレースホルダーを正しく使っていれば、SQL インジェクションは起きにくくなります。それでも、どこかに作り込みの抜けが残る可能性はあります。そこで、アプリが DB につなぐときのユーザーの権限を、必要最小限にしておく、という追加の守りがあります。

IPA は、これを「根本的解決」ではなく「保険的対策」に置いています。Web アプリが DB につなぐアカウントの権限が必要以上に高いと、攻撃による被害が深刻になるおそれがあるからです。Web アプリから DB に渡す命令文を洗い出し、それを実行するのに必要な最小限の権限だけを与えるように、と IPA は書いています。

OWASP の Cheat Sheet も、4 つの主な守りに加えて、多層の守り(1 つが破られても次で止める守り)として「最小権限」を勧めています。OWASP の SQL Injection のページには、被害の大きさは、攻撃する側の腕と想像力で決まり、DB への接続の権限を低くするといった多層の守りで、少しは抑えられる、と書かれています。IPA も OWASP も、これをプレースホルダーの代わりにはしていません。根本の対策(プレースホルダー)の上に重ねる、破られたときの被害を小さくするための守りです。

権限を絞る考え方(OWASP の Least Privilege)

OWASP の Least Privilege(最小権限)は、次のことを書いています。

  • 「何を取り上げるか」ではなく、「アプリのアカウントに何が要るか」から決める(権限のない状態から、足していく)
  • 読み取りだけでよいアカウントには、要るテーブルの読み取りだけを渡す
  • アプリのアカウントに、DBA(DB の管理者)や管理者の権限を渡さない。そうすると全部が動いて楽だが、とても危険
  • テーブルの一部だけ要るなら、その部分だけを見せる「ビュー」(SELECT の結果を、テーブルのように名前を付けて見せる仕組み)を作り、ビューへの権限を渡す。作成や削除の権限は、(ほぼ)渡さない
  • Web アプリごとに、別の DB ユーザーを使う。管理者や所有者のアカウントを、Web アプリで共有しない
  • DBMS(DB を動かすソフト)を動かす OS のアカウントも絞る。root や system で動かさない

別のユーザーを使う理由として、OWASP は例を挙げています。ログインの画面は、ユーザー名とパスワードの列を読むだけでよく、書き込み(insert・update・delete)は要りません。一方、登録の画面は、そのテーブルに insert が要ります。この違いは、別々の DB ユーザーでつながないと分けられません。

OWASP はもう 1 つ書いています。権限を絞ると、SQL インジェクションでない攻撃(たとえば、画面で選べる値を別の値に書き換えて、本人には許されないがアプリには許されているデータに届く、など)も起きにくくなります。この種類の問題は、認証と認可の記事で扱っています。

PostgreSQL:SELECT だけを渡したユーザーを作る

PostgreSQL の用語を、先に整理します。ロール(役割)は、権限を持たせる単位です。LOGIN の属性が付いたロールが、ふつう言う「ユーザー」にあたります。GRANTは、ロールに権限を渡す SQL 文です。テーブルなどを作ると所有者が決まり、たいていのオブジェクトは、最初は所有者(とスーパーユーザー)しか何もできません。ほかのロールに使わせるには、GRANT で権限を渡します。

次の手順は、PostgreSQL 18.6 で確かめたものです。Docker のコンテナで PostgreSQL を立てました。ポートを外に出さず、docker exec で中から psql(PostgreSQL の対話用のコマンド)を動かしています。

docker run -d --name sqli-pg -e POSTGRES_PASSWORD=<管理者のパスワード> -e POSTGRES_DB=shop postgres:18

管理者の postgres で、items テーブルを作り、SELECT だけを渡したロール app_reader を作りました。

CREATE TABLE items (id serial PRIMARY KEY, name text NOT NULL);
INSERT INTO items (name) VALUES ('notebook'), ('pen');
CREATE ROLE app_reader LOGIN PASSWORD 'localonly-reader';
GRANT CONNECT ON DATABASE shop TO app_reader;
GRANT USAGE ON SCHEMA public TO app_reader;
GRANT SELECT ON items TO app_reader;

(流したファイルには、先頭に SELECT version();、最後に \dp items もありますが、ここでは省きました。)上の SQL を pg_setup.sql というファイルに保存して、次のように流します。

docker exec -i sqli-pg psql -U postgres -d shop -X -f - < pg_setup.sql

CONNECT は、その DB につなぐ権限です。USAGE ON SCHEMA public は、スキーマ(テーブルなどを入れる名前の入れ物)の中のものを使う権限です。public スキーマは、既定で全員が USAGE を持っているので、この行は、何を渡したかをはっきりさせるために書いています。LOGIN PASSWORD の値は練習用です。

\dp items(テーブルの権限を見る psql のコマンド)では、app_reader=r/postgres と出ました。r は SELECT(read)を表し、「postgres が app_reader に SELECT を渡した」という意味です。所有者の postgres の欄は postgres=arwdDxtm/postgres で、文字は a=INSERT・r=SELECT・w=UPDATE・d=DELETE・D=TRUNCATE・x=REFERENCES・t=TRIGGER・m=MAINTAIN を表します(PostgreSQL の文書の対応表)。\du app_reader で見ると、app_reader の属性は空で、スーパーユーザーではありません。

次に、app_reader でつないで、次の 7 行の SQL を流しました。pg_reader.sql というファイルに保存し、-U を app_reader に替えて流します。

SELECT current_user;
SELECT id, name FROM items ORDER BY id;
INSERT INTO items (name) VALUES ('eraser');
UPDATE items SET name = 'x' WHERE id = 1;
DELETE FROM items;
DROP TABLE items;
CREATE TABLE notes (body text);
docker exec -i sqli-pg psql -U app_reader -d shop -X -f - < pg_reader.sql

1〜2 行目は通りました。current_user は app_reader で、items の 2 行(notebook と pen)が返りました。3 行目以降は、次のように断られました(7 行目のエラーには、psql が LINE 1: と位置の印も付けて表示します)。

psql:<stdin>:3: ERROR:  permission denied for table items
psql:<stdin>:4: ERROR:  permission denied for table items
psql:<stdin>:5: ERROR:  permission denied for table items
psql:<stdin>:6: ERROR:  must be owner of table items
psql:<stdin>:7: ERROR:  permission denied for schema public
LINE 1: CREATE TABLE notes (body text);
                     ^
流した文結果
INSERT(行を足す)permission denied for table items
UPDATE(行を書き換える)permission denied for table items
DELETE(行を消す)permission denied for table items
DROP TABLE(テーブルを消す)must be owner of table items
CREATE TABLE(public スキーマに作る)permission denied for schema public

(psql -f は、途中でエラーが出ても最後まで流す設定のままだったので、7 行目まで動いています。)

ここから、次のことが言えます。

  • SELECT だけを渡すと、書き込み・書き換え・削除は断られる。 権限は GRANT で足されるもので、渡していないものは使えません
  • DROP TABLE は、DELETE とは別の理由で断られる。 PostgreSQL の文書によると、テーブルの削除や定義の変更は、GRANT できる権限ではなく、所有者に付いているものです。アプリが使うロールを、テーブルの所有者にも、所有者のロールのメンバーにも、スキーマの所有者にもせず、スーパーユーザーにもしないことで、この操作を渡さずに済みます(PostgreSQL の DROP TABLE の文書は、テーブルを削除できるのは、テーブルの所有者、スキーマの所有者、スーパーユーザーだけと書いています。権限は所有するロールのメンバーに引き継がれることもあります)
  • public スキーマへの CREATE TABLE も断られた。 PostgreSQL 15 以降は、public スキーマに全員が CREATE できる既定ではなくなりました。14 以前から上げた DB では全員が持っているので、REVOKE CREATE ON SCHEMA public FROM PUBLIC で外します(PostgreSQL の文書)

手元の接続は、docker exec でコンテナの中からつなぎました。この接続は、公式のイメージの設定でパスワードを見ないので、app_reader のパスワードでのログインは確かめていません。練習のあとは、コンテナと、コンテナが作った名前のないボリュームを消しておきます。データの残し方と消し方は、Docker の volume と bind mount の記事にあります。

権限を渡すときの注意(PostgreSQL の文書より)

  • 渡せる権限の種類は、SELECT・INSERT・UPDATE・DELETE・TRUNCATE・REFERENCES・TRIGGER・CREATE・CONNECT・TEMPORARY・EXECUTE・USAGE などです。GRANT は、すでにある権限に足されます
  • INSERT を渡すアプリのユーザーで、serial の列を使うときは注意が必要です。テーブルに権限を渡しても、そのテーブルが使うシーケンス(番号を振る仕組み。serial の列のものも)には広がらず、シーケンスの権限は別に設定します。この記事の app_reader には INSERT を渡していないので、手元では確かめていません
  • スキーマの中のテーブルにまとめて渡す GRANT … ON ALL TABLES IN SCHEMA があります。ただし、あとから作ったテーブルにも効くかは、この記事では確かめていません
  • CREATE ROLE の既定は、スーパーユーザーでない・DB を作れない・ロールを作れない、です。スーパーユーザーはあらゆるアクセスの制限を越えられるので、本当に必要なときだけ使います
  • pg_read_all_data という、すべてのテーブルを読める決まったロールがあります。便利ですが、OWASP の「要るテーブルだけ」より広くなるので、この記事では使っていません
  • ビューを使って列を絞るとき、OWASP によると、PostgreSQL はふつう、元のテーブルへの権限を、ビューの所有者の権限で確かめます。OWASP は、ビューへのクエリでも、パラメータ化が要ると書いています(この 1 つは OWASP の Cheat Sheet の説明で、PostgreSQL の文書では確かめていません)

SQLite:読み取り専用で開く(mode=ro)

SQLite は、PostgreSQL のようにサーバーに接続するのではなく、1 つのファイルを開いて使います。PostgreSQL の GRANT のようなユーザーごとの権限の仕組みは、この記事では確かめていません。ただし、DB を読み取り専用で開くことはできます。表示だけの処理は、読み取り専用で開くようにする、というのは OWASP の「読み取りだけでよいアカウントには、読み取りだけを渡す」と同じ考え方です。

SQLite では、ファイル名を file: で始まる URI(資源の場所を表す文字列)で書き、後ろに ?mode=ro を付けると、読み取り専用で開きます。ro は read-only の略です。Python の sqlite3.connect() では、uri=True を必ず付けます。付けると、database の文字列を URI として読みます。

import sqlite3

ro = sqlite3.connect("file:shop.db?mode=ro", uri=True)
print(ro.execute("SELECT id, name FROM users").fetchall())
try:
    ro.execute("INSERT INTO users (name) VALUES (?)", ("Suzuki",))
except sqlite3.Error as e:
    print(type(e).__name__ + ":", e)
try:
    ro.execute("DELETE FROM users")
except sqlite3.Error as e:
    print(type(e).__name__ + ":", e)
ro.close()
[(1, 'Tanaka'), (2, "O'Reilly")]
OperationalError: attempt to write a readonly database
OperationalError: attempt to write a readonly database

SELECT は通り、INSERT も DELETE も attempt to write a readonly database(読み取り専用の DB に書こうとした)で断られました。

uri=True を必ず書く理由があります。SQLite の文書は、古いアプリとの互換のため、URI のファイル名は既定では無効だと書いています。有効にするには、コンパイルのときの設定(SQLITE_USE_URI=1)、起動のときの設定、接続ごとの指定(SQLITE_OPEN_URI)があります。手元の Python 3.12.3 の SQLite は、USE_URI 付きでコンパイルされていて、uri=True を付け忘れても mode=ro が効きました。ただし、ほかの環境で同じ結果になるとは限りません。Python の文書の例も uri=True を付けているので、付けてください。

2026年に、SQL インジェクションの注意喚起はあった?(2026年10月時点)

JPCERT/CC(日本のセキュリティの情報を扱う組織)の 2026 年の注意喚起は、2026年10月14日の分までで 32 件あります。そのうち、題に「SQLインジェクション」がある注意喚起は 2 件です。Metabase と Cisco Secure Email Gateway の件です。

また、JPCERT/CC の注意喚起(JPCERT-AT-2026-0030、2026年10月8日公開、10月9日更新)は、直近の 2026年9月前後に、国内の組織で不正アクセスによる個人情報などの漏えいが「相次いでいます」と書いています。この注意喚起が挙げる攻撃の手口のうち、「ケースC」が Metabase の SQL インジェクションの脆弱性です。同じ注意喚起は、これらの手口が「すべての事案で同一の攻撃手法が使用されていることを示すものではありません」と断っています。

ここでは、JPCERT/CC と当事者の文書に書かれていることだけを並べます。コードのどこでどう SQL を作っていたか、DB ユーザーの権限がどうだったかは、書かれていません。

Metabase の件(JPCERT-AT-2026-0023)

  • Metabase は、2026年8月6日(日本時間)に、SQL インジェクションの脆弱性(CVE-2026-72898)を公表しました。JPCERT/CC の注意喚起は 2026年8月14日付です
  • 悪用されると、遠隔の第三者が認証なしで細工したリクエストを送り、Metabase のアプリケーションデータベースに不正な SQL を実行して、Metabase の管理者権限を取得するおそれがあります
  • Metabase によると、ゼロデイ攻撃(修正が出る前に悪用される攻撃)が確認されています。海外では複数の組織が被害を公表している、と JPCERT/CC は書いています
  • Metabase の告知(GitHub の Security Advisory)は、深刻度を critical とし、認証なしの攻撃者が任意の SQL を入れて、管理者になれるとしています。そこから、設定を変える・接続先の DB の保存された資格情報を盗む・その接続で届くデータを読む・データを書き出す、ができうると書いています
  • 対象は、58 系から 63 系の、それぞれ決まった版より前です。直った版は、63 系が x.63.5、62 系が x.62.9、61 系が x.61.11、60 系が x.60.17、59 系が x.59.21、58 系が x.58.24 です。これらの版以降に上げます。58 系より前は影響を受けないと Metabase は説明しています。Metabase Cloud は対策済みです
  • 侵害された可能性がある場合、Metabase は、ユーザーセッションや API キー、管理者アカウントなどを確認するとともに、接続先データベースの認証情報を変更することを勧めています

Cisco Secure Email Gateway の件(JPCERT-AT-2026-0027)

  • JPCERT/CC の注意喚起は 2026年9月15日付で、Cisco が同日に、SQL インジェクションの脆弱性(CVE-2026-76461)を公表したと書いています(Cisco の告知の最初の公開は、GMT で 2026年9月14日 16:00 です)
  • 悪用されると、認証されていない遠隔の攻撃者が、悪意のある SQL 文を含む細工したメールを送り、対象のシステムがそのメールを受け取ると、root 権限で任意のコマンドが実行されるおそれがあります。Cisco は 2026年9月に悪用を確認しています
  • Cisco の告知は、原因を「メールの解析の部分で、検証が足りないこと」と書いています。深刻度は Critical です
  • 対象は、Cisco AsyncOS(Secure Email Gateway 向け)の、16.5 系の 16.5.0-780 より前、16.0 系の 16.0.4-302 より前、15.5 系以前の 15.5.5-014 より前です。回避策は提供されていません
  • root 権限でコマンドを動かされると、ログや侵害の痕跡が消されている可能性があります。JPCERT/CC は、機器のログだけでなく、周辺機器のネットワークログとも突き合わせて調べるよう書いています

この 2 件から、言えないこと

  • Metabase の告知は「認証なしの入口(endpoint)」までを書き、Cisco の告知は「検証が足りない」までを書いています。どちらの文書にも、文字列連結で SQL を作っていたとは書かれていません。 この記事も、それを原因とは書きません
  • 接続先の DB ユーザーの権限がどうだったか、権限を絞っていれば被害が変わったかは、どの資料にも書かれていません。 この 2 件を、「権限を絞る」ことの成功例や失敗例として読むことはできません
  • JPCERT/CC の 10月8日の注意喚起は、BI ツール(データを集めて見るためのツール)や従業員向けの管理システムなど、不特定多数のアクセスを想定していないシステムが被害に遭い、内部の情報が漏れるケースもある、と書いています。この文は Metabase を名指ししていません
  • 同じ注意喚起の対策には「API利用者やAPIトークンには必要最小限の権限のみを付与する」があります。これは API の利用者とトークンの権限で、DB ユーザーの権限の話ではありません

自分で使っているソフトウェアに SQL インジェクションの脆弱性の告知が出たときにできることは、JPCERT/CC が Metabase の注意喚起で書いているとおり、提供元の最新の情報を確認し、修正を含む最新の版を適用することです。

過去の例:MOVEit Transfer(2023年)

少し前の例として、MOVEit Transfer(Progress Software 社のファイル転送ソフト)の件があります。CISA(アメリカの政府機関)と FBI の共同の勧告 AA23-158A(2023年6月7日公開)は、公開されている情報によると、2023年5月27日から CL0P ランサムウェアのグループ(TA505)が、知られていなかった SQL インジェクションの脆弱性(CVE-2023-34362)を悪用した、と書いています。インターネットに出ていた MOVEit Transfer に、LEMURLOOT という名前の Web シェル(遠隔から OS のコマンドを動かせるようにする仕掛け)を置き、その下の DB からデータを盗みました。SQL インジェクションで Web シェルを置き、サーバーで OS のコマンドを動かしてデータを盗んだ、と勧告は説明しています。SQL インジェクションが、DB の中の情報にとどまらず、サーバーの OS のコマンドの実行にもつながった例です。

よくある質問

プレースホルダーを使えば、攻撃の入力も防げますか?

IPA は、静的プレースホルダー(準備された文)なら、SQL を準備する段階で構文が確定し、あとから変わらないので、SQL インジェクションの脆弱性は生じない、と説明しています。動的プレースホルダーは、バインドを行うライブラリの実装に問題があると、生じる可能性を否定できない、とも書いています。OWASP も、プリペアドステートメントなら、入力が何であっても DB はコードとデータを区別する、と書いています。

この記事で手元で確かめたのは、O'Reilly を文字列連結で渡すとエラーになり、プレースホルダーで渡すと検索できる、という差までです。攻撃の入力は流していません。

ORM を使えば、SQL インジェクションは起きませんか?

OWASP Top 10:2025 の A05 は、ORM に移ることを、安全な API の選択肢のひとつに挙げています。一方で、A05 は、ORM の検索の値に、無害化していないデータを使うことも、当てはまる条件として挙げています。ORM が既定でプレースホルダーを使うかどうかは、この記事では公式の文書を開いて確かめていません。使っている ORM の文書で確かめてください。

DB の権限を絞れば、SQL インジェクションを防げますか?

防げません。IPA は権限を絞ることを「保険的対策」に置いています。OWASP も、4 つの主な守りに加えて、多層の守りとして勧めています。破られたときに被害を小さくするための守りで、根本の対策はプレースホルダーです。

Metabase や Cisco の件は、文字列連結が原因でしたか?

当事者の告知には、書かれていません。Metabase の告知は「認証なしの入口」まで、Cisco の告知は「メールの解析の部分で、検証が足りないこと」までを書いています。この記事は、文字列連結が原因だったとは書きません。

SQLite にも、ユーザーと GRANT はありますか?

PostgreSQL のようなユーザーと GRANT の仕組みが SQLite にあるかどうかは、この記事では一次資料で確かめていません。SQLite では、mode=ro を付けて開くことで、読み取り専用にできます(Python では uri=True も付けます)。MySQL や SQL Server など、ほかの DB の権限の渡し方は、公式の文書を開いていないので扱っていません。

自分のアプリで確かめること(チェックリスト)
  • SQL 文の組み立てを、文字列連結ではなく、プレースホルダー(できれば静的プレースホルダー)で行っているか(→「プレースホルダーで防ぐ理由は?」)
  • 使っているライブラリのプレースホルダーが、静的と動的のどちらの方式か、そのライブラリの文書で確かめたか(→「なぜ、これで防げるのか」)
  • ストアドプロシージャの中、ORM の検索の値、数値のつもりの変数で、無害化していない値をつないだり使ったりしていないか(→「プレースホルダーを使っていても、気をつける所」)
  • テーブル名・列名・並び順を、利用者から来た値でそのまま決めていないか(→「プレースホルダーにできない所と、エスケープの扱い」)
  • プレースホルダーが使える所で、エスケープだけに頼っていないか(→「エスケープは、IPA と OWASP で扱いが違う」)
  • DB のエラーメッセージを、そのまま画面(ブラウザ)に出していないか(→「画面にエラーをそのまま出さない」)
  • SQL 文そのものを、hidden パラメータなどの Web アプリへのパラメータに入れていないか(→「その他の IPA の対策」)
  • アプリが DB につなぐユーザーの権限を、必要最小限にしているか。権限を絞ることを、プレースホルダーの代わりにしていないか(→「DB ユーザーの権限を絞るには?(PostgreSQL と SQLite で確かめる)」)

まとめ

  • SQL インジェクションは、利用者の入力をつないで SQL を組み立てると、値がリテラル(SQL の中に直接書く値)の外にはみ出して起きる問題です
  • 根本の対策は、プレースホルダー(できれば静的プレースホルダー=準備された文)です。Python の sqlite3 では ? を使い、値は別に渡します
  • テーブル名・列名・並び順は、プレースホルダーにできません。入力の検証か、クエリの作り直しで守り、できればコードの側から名前を出します
  • エスケープの扱いは、IPA(次の手)と OWASP(強く勧めない)で違いますが、まずプレースホルダーという点は同じです
  • DB ユーザーの権限を絞るのは、破られたときの被害を小さくする保険的な守りです。プレースホルダーの代わりにはなりません
  • PostgreSQL では、GRANT で必要な権限だけを渡します。手元の 18.6 では、SELECT だけの app_reader は、書き込み・テーブルの削除・テーブルの作成を断られました。SQLite は file:…?mode=ro と uri=True で、読み取り専用で開けます
  • 2026年の JPCERT/CC の注意喚起の実例(Metabase・Cisco)は、原因や権限の状態が資料に書かれていません。文字列連結が原因とも、権限を絞れば変わったとも、読まないでください
  • 製品に SQL インジェクションの脆弱性の告知が出たら、提供元の最新の情報を確認し、修正を含む最新の版を適用します。その製品の入口がインターネットから届く状態だったなら、侵害がないかも調べます

同じ回の記事として、API の守り方を扱う認証と認可の記事と、全体の点検をまとめるWeb 開発のセキュリティの基本の記事があります。

参考資料

※この記事は、公式の資料をもとに AI(Claude)で下書きし、2026年10月20日時点の公式の資料と照らして確かめてから公開しています。誤りに気づいたらお問い合わせからお知らせください。

PR
PR
タイトルとURLをコピーしました