🎯 この章で学ぶこと

  • 攻撃チェーンの終着点としてのデータベース — 大規模漏えい事故の典型パターン
  • SQLインジェクションの原理(' OR '1'='1)と、プレースホルダ+入力検証+WAFの3層防御
  • 最小権限の原則をDBアカウントで実践する — GRANT/REVOKEの考え方
  • 暗号化の3つの場面(通信・保存・カラム)と、パスワードは「ソルト付きハッシュ」であること
  • 監査ログと、リストア訓練までを含めたバックアップ(3-2-1ルール)

9.1 なぜDBが最終目的地なのか

報道される情報漏えい事故——「顧客情報◯十万件が流出」——の多くは、おおよそ次のような攻撃チェーンをたどっています。

① フィッシングメール ──→ ② 社員の端末に侵入
                                │(端末に保存されたパスワードや接続情報を収集)
                                ▼
                        ③ 社内サーバーへ横展開
                                │(管理者権限の奪取)
                                ▼
                        ④ データベースに到達 ──→ ⑤ 全件を抜き取り(漏えい)

①〜③はすべて準備です。攻撃者が本当に欲しいもの——売れる個人情報、脅迫に使える機密、暗号化して身代金を要求できる基幹データ——は、④のデータベースの中にあります。第1章で「DBは会社で一番狙われる場所」と述べた理由がこれです。

この構造から、重要な結論が2つ導けます。

💡 内部不正という「もうひとつのルート」

漏えいの原因は外部攻撃だけではありません。退職間際の社員が顧客名簿を持ち出す、委託先の作業員がUSBにコピーする——正規の権限を使った内部不正も大きな割合を占めます。この章で学ぶ「最小権限」と「監査ログ」は、外部攻撃と内部不正の両方に効く対策です。

9.2 SQLインジェクション — 攻める側の視点から守る

第8章でプレースホルダを「絶対条件」と学びました。ここではなぜそれが効くのかを、攻撃の原理から理解します。ログイン処理を文字列連結で書いてしまった脆弱なコードを見てください。

# 脆弱なログイン処理(教材用。絶対に真似しないこと)
sql = (f"SELECT * FROM users "
       f"WHERE name = '{name}' AND password = '{password}'")
cur.execute(sql)

攻撃者はパスワード欄に、古典的な文字列 ' OR '1'='1 を入力します。すると組み立てられるSQLはこうなります。

SELECT * FROM users
WHERE name = 'admin' AND password = '' OR '1'='1'

OR '1'='1' は常に真なので、パスワードを知らなくても全行がマッチしてログインが成立します。入力した文字列がデータではなく「SQLの構文」として解釈されてしまった——これがSQLインジェクションの本質です。同じ原理で、UNION SELECT による他テーブルの抜き取りや、; DROP TABLE のような破壊も狙われます。

⚠️ 実験は自分の環境でのみ — 不正アクセス禁止法

この原理を自分の practice.db や自作スクリプトで再現してみるのは最高の学習です。しかし、他人のサービスに対して試すことは、たとえ「確認のつもり」でも不正アクセス禁止法等に触れる犯罪行為です。攻撃者の視点は「自環境での実験」と「守るための知識」にだけ使ってください。

対策は3層で重ねる

SQLインジェクション対策は1つではなく、層で考えます。

対策位置づけ
第1層(根本)プレースホルダ(パラメータ化クエリ)入力を「データ」としてしか扱わせない。これだけでSQLインジェクションは原理的に成立しなくなる
第2層入力検証(バリデーション)「社員IDは数字のみ」「statusは3種類のみ」など、想定外の値を入口で弾く。異常入力の早期発見にもなる
第3層WAF(Web Application Firewall)攻撃パターンをネットワーク側で検知・遮断する保険。詳しくはセキュリティ中級・上級コースで扱う領域

第2層・第3層は「第1層をやり忘れた箇所があっても被害を防ぐ」ための保険であって、プレースホルダの代わりにはなりません。逆に、プレースホルダを徹底していても、入力検証には「不正なデータを業務ロジックに入れない」という独自の価値があります。

✅ ORMを使っていても油断しない

第8章で紹介したSQLAlchemyなどのORMは、通常の使い方なら自動的にプレースホルダを使ってくれます。しかしORMにも「生SQLを直接実行する機能」があり、そこで文字列連結をすれば同じように脆弱です。「ORMだから安全」ではなく「値がパラメータとして渡っているから安全」——判断基準を道具ではなく原理に置いてください。

9.3 アカウントと権限管理

侵入されたとき・アカウントが乗っ取られたときの被害を決めるのは、そのアカウントが何をできたかです。ここで効くのが、セキュリティの基本原則である最小権限の原則(必要最小限の権限だけを与える)です。

考えてみてください。Webアプリが日常業務でDBに対して行うのは、SELECT・INSERT・UPDATE・(場合により)DELETEだけです。テーブルを削除する DROP も、他人に権限を与える GRANT も、アプリの通常動作には不要です。それなのにアプリ用アカウントに管理者権限を与えていると、SQLインジェクションが1か所あっただけで「全テーブル削除」まで許してしまいます。

SQLiteは1ファイル=1DBでアカウント機能を持たないため、ここはMySQL風の例で考え方を示します(PostgreSQLなどでもほぼ同じです)。

-- アプリ用アカウントを作り、必要な操作だけ許可する
CREATE USER 'app'@'10.0.1.%' IDENTIFIED BY '(強いパスワード)';
GRANT SELECT, INSERT, UPDATE, DELETE ON company.* TO 'app'@'10.0.1.%';

-- 集計・閲覧しかしない帳票ツールには読み取り専用アカウント
CREATE USER 'report'@'10.0.2.%' IDENTIFIED BY '(強いパスワード)';
GRANT SELECT ON company.* TO 'report'@'10.0.2.%';

-- 与えすぎた権限は取り消す
REVOKE DELETE ON company.* FROM 'app'@'10.0.1.%';

ポイントは3つです。

9.4 暗号化 — 保存時と通信時

権限管理を破られた場合や、物理的にディスク・ファイルを盗まれた場合に備えるのが暗号化です。場面ごとに整理します。

場面何を守るか代表的な手段
通信時(transit)アプリとDBサーバー間の盗聴TLSで接続を暗号化する(HTTPSと同じ仕組み)
保存時(at rest)ディスク・バックアップ媒体の盗難や紛失ディスク/ファイル全体の暗号化、DBMSの透過的暗号化機能
カラム単位DB管理者にすら見せたくない特定項目(カード番号など)アプリ側で暗号化してから格納する

そして最重要の例外がパスワードです。ユーザーのパスワードは「暗号化」して保存してはいけません。暗号化は復号できてしまうからです。正解は、復元できない一方向の変換であるソルト付きハッシュ(Python × セキュリティ入門コースの第12章で学んだ、ユーザーごとに異なるソルトを付けてハッシュ化する方式)で保存し、ログイン時は「入力を同じ方式でハッシュ化して比較」します。DBが丸ごと漏えいしても、元のパスワードがすぐには判明しない状態を保つのが目的です。

💡 SQLiteファイルを守るのはOSのファイル権限

標準のSQLiteには暗号化もアカウント機能もないため、company.db を守る仕組みは実質OSのファイル権限です。chmod 600 company.db のように所有者だけが読み書きできるようにする、Webサーバーの公開ディレクトリ(URLで直接ダウンロードできてしまう場所)に置かない、の2点は最低限守ってください。「DBファイルがURL直打ちでダウンロードできた」は実際に繰り返されている事故です。

9.5 監査ログとバックアップ

監査ログ — 「誰が・いつ・何をしたか」

防御をすり抜けられたときに、被害範囲の特定と原因究明を可能にするのが監査ログです。最低限「誰が(アカウント)・いつ・どこから(接続元)・何をしたか(実行SQLや対象テーブル)」を記録し、DB管理者自身も改ざんできない場所(別サーバーのログ基盤など)へ転送します。9.3節の「共有アカウント禁止」はここに直結します——アカウントが個人単位でなければ、ログがあっても犯人をたどれません。

バックアップ — リストア訓練までがバックアップ

ランサムウェア(データを暗号化して身代金を要求する攻撃)への最後の対抗手段はバックアップです。定石は3-2-1ルール——コピーを3つ持ち、2種類の媒体に分け、1つはオフサイト(遠隔地)に置く。さらにランサムウェア対策としては、ネットワークから切り離されたオフライン保管(あるいは書き換え不可のストレージ)を1つ含めることが重要です。攻撃者はバックアップサーバーも探して暗号化しにきます——オンラインで常時つながっているバックアップは「4本目の人質」になりかねません。

⚠️ 「取っているつもり」バックアップの典型

いざ障害が起きてから「バックアップジョブが半年前から失敗していた」「リストア手順を誰も知らない」「戻したら文字化けした」が発覚するのは、恥ずかしいほどよくある話です。定期的にリストア訓練を行い、復元できて初めてバックアップと呼べると覚えてください。あわせて「何時間前の状態まで戻れれば業務が成り立つか(目標復旧時点)」を決めておくと、バックアップの頻度が論理的に決まります。

🛡️ セキュリティ・運用の視点 — 本番データを開発環境に持ち出さない

「テストのリアリティを上げたいから、本番DBのコピーを開発環境に入れよう」——これは情報漏えいの定番ルートです。開発環境は本番より防御が緩いのが普通で(検証用に外部公開されていたり、開発者全員がフルアクセスだったり)、そこに本番の個人情報を置いた時点で、会社の守りは「一番弱い環境」まで下がります。開発メンバー全員が顧客情報を閲覧できる状態は、それ自体が内部不正・誤送信のリスクでもあります。代替手段は2つ——①マスキング(氏名や電話番号を無意味な値に置換してから渡す)、②ダミーデータ生成(Pythonで本番と同じスキーマにテストデータを流し込む。第8章の executemany がそのまま使えます)。「本番データは本番環境から出さない」を原則にしてください。

まとめ

練習問題

問題 9-1

次のログイン処理のコードには、この章で学んだ観点から見て問題が少なくとも3つあります。指摘してください。

import sqlite3

DB_PASSWORD = "P@ssw0rd123"   # 管理者アカウントのパスワード

def login(name, password):
    conn = sqlite3.connect("app.db")
    cur = conn.cursor()
    cur.execute(f"SELECT * FROM users WHERE name = '{name}' "
                f"AND password = '{password}'")
    return cur.fetchone() is not None
解答を見る

主な問題は次の3つです(3つ挙げられれば正解です)。

① SQLインジェクション — ユーザー入力の namepassword をf文字列でSQLに連結しています。パスワード欄に ' OR '1'='1 を入力されると認証をすり抜けられます。プレースホルダに書き直します:

cur.execute(
    "SELECT * FROM users WHERE name = ? AND password_hash = ?",
    (name, hash_password(password, salt)),
)

② パスワードの平文保存password = '...' で直接比較しているということは、usersテーブルにパスワードがそのまま入っています。DBが漏えいすると全ユーザーのパスワードが流出します。ソルト付きハッシュで保存し、入力をハッシュ化して比較すべきです(9.4節)。

③ 認証情報のコード直書きDB_PASSWORD がソースコードに直書きされています。リポジトリにpushされれば漏えいします。環境変数や.gitignore済みの設定ファイルから読み込むべきです(第8章)。

そのほか、「接続をcloseしていない」「ログイン試行の記録(監査ログ)がなく総当たり攻撃に気づけない」も指摘できれば上級です。

問題 9-2

社内の勤怠管理Webアプリ用に、MySQLのDBアカウント kintai_app を作ります。次の権限のうち、このアカウントに与えるべきものをすべて選び、残りを与えるべきでない理由を説明してください。
(a) SELECT (b) INSERT (c) UPDATE (d) DROP (e) GRANT (f) 全データベースへのアクセス

解答を見る

与えるべきは (a) SELECT・(b) INSERT・(c) UPDATE の3つです(打刻の登録・修正・参照という日常業務に対応します)。

  • (d) DROP — テーブル削除はアプリの通常動作に不要です。与えていると、SQLインジェクションや乗っ取りが1件あっただけで全テーブルを破壊されます。テーブル定義の変更は管理者アカウントで、変更作業のときだけ行います。
  • (e) GRANT — 他アカウントへの権限付与はDB管理者の仕事です。アプリが持つと、侵入者が「自分用の強い権限アカウント」を作って居座る足がかりになります。
  • (f) 全データベースへのアクセス — 勤怠アプリに必要なのは勤怠DBだけです。GRANT SELECT, INSERT, UPDATE ON kintai.* TO 'kintai_app'@'10.0.1.%'; のように対象DBと接続元を絞ります。同じサーバーに載っている人事評価DBまで読める状態は最小権限の原則に反します。

なおDELETEも今回は含めていません。勤怠記録は「消す」のではなく訂正履歴を残す設計(第6章)が普通で、消せない設計は監査の観点でも優れています。必要になった時点で必要な範囲にだけ付与すればよい——これが最小権限の運用です。