第4部 実務とセキュリティ
第9章 データベースのセキュリティ
攻撃者にとってデータベースは通過点ではなく最終目的地です。この章では「なぜ狙われるのか」から始めて、SQLインジェクション・権限管理・暗号化・監査ログとバックアップまで、DBを守る多層防御を一気に組み立てます。
🎯 この章で学ぶこと
- 攻撃チェーンの終着点としてのデータベース — 大規模漏えい事故の典型パターン
- SQLインジェクションの原理(
' OR '1'='1)と、プレースホルダ+入力検証+WAFの3層防御 - 最小権限の原則をDBアカウントで実践する — GRANT/REVOKEの考え方
- 暗号化の3つの場面(通信・保存・カラム)と、パスワードは「ソルト付きハッシュ」であること
- 監査ログと、リストア訓練までを含めたバックアップ(3-2-1ルール)
9.1 なぜDBが最終目的地なのか
報道される情報漏えい事故——「顧客情報◯十万件が流出」——の多くは、おおよそ次のような攻撃チェーンをたどっています。
① フィッシングメール ──→ ② 社員の端末に侵入
│(端末に保存されたパスワードや接続情報を収集)
▼
③ 社内サーバーへ横展開
│(管理者権限の奪取)
▼
④ データベースに到達 ──→ ⑤ 全件を抜き取り(漏えい)
①〜③はすべて準備です。攻撃者が本当に欲しいもの——売れる個人情報、脅迫に使える機密、暗号化して身代金を要求できる基幹データ——は、④のデータベースの中にあります。第1章で「DBは会社で一番狙われる場所」と述べた理由がこれです。
この構造から、重要な結論が2つ導けます。
- DBの防御は多層防御の「最後の砦」 — メール対策や端末防御(入門コースで学んだ領域)が破られたあとでも被害を最小化できるのは、DB自身の防御だけです。
- 「社内からのアクセスだから安全」は成立しない — 攻撃チェーンの④は社内ネットワークの内側から行われます。境界の内側でも認証・権限・記録を要求する設計が必要です。
漏えいの原因は外部攻撃だけではありません。退職間際の社員が顧客名簿を持ち出す、委託先の作業員が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層をやり忘れた箇所があっても被害を防ぐ」ための保険であって、プレースホルダの代わりにはなりません。逆に、プレースホルダを徹底していても、入力検証には「不正なデータを業務ロジックに入れない」という独自の価値があります。
第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つです。
- 用途ごとにアカウントを分ける — アプリ用・帳票用・管理者用。帳票ツールが読み取り専用アカウントで動いていれば、そのツールが乗っ取られてもデータは書き換えられません。
- 接続元も絞る — 上の例の
'app'@'10.0.1.%'は「この IPアドレス帯からしか接続できない」という指定です。パスワードが漏れても、外部からは接続できません。 - 共有アカウントを禁止する — 「部署のみんなでadmin/共通パスワード」は、①漏えい時に全員のパスワード変更が必要、②監査ログを見ても誰の操作か特定できない、の二重で最悪です。1人(1アプリ)に1アカウントが原則です。
9.4 暗号化 — 保存時と通信時
権限管理を破られた場合や、物理的にディスク・ファイルを盗まれた場合に備えるのが暗号化です。場面ごとに整理します。
| 場面 | 何を守るか | 代表的な手段 |
|---|---|---|
| 通信時(transit) | アプリとDBサーバー間の盗聴 | TLSで接続を暗号化する(HTTPSと同じ仕組み) |
| 保存時(at rest) | ディスク・バックアップ媒体の盗難や紛失 | ディスク/ファイル全体の暗号化、DBMSの透過的暗号化機能 |
| カラム単位 | DB管理者にすら見せたくない特定項目(カード番号など) | アプリ側で暗号化してから格納する |
そして最重要の例外がパスワードです。ユーザーのパスワードは「暗号化」して保存してはいけません。暗号化は復号できてしまうからです。正解は、復元できない一方向の変換であるソルト付きハッシュ(Python × セキュリティ入門コースの第12章で学んだ、ユーザーごとに異なるソルトを付けてハッシュ化する方式)で保存し、ログイン時は「入力を同じ方式でハッシュ化して比較」します。DBが丸ごと漏えいしても、元のパスワードがすぐには判明しない状態を保つのが目的です。
標準の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 がそのまま使えます)。「本番データは本番環境から出さない」を原則にしてください。
まとめ
- 攻撃チェーン(フィッシング→端末→サーバー→DB)の最終目的地はデータベース。DBの防御は多層防御の最後の砦であり、「社内からだから安全」は成立しない
- SQLインジェクションは入力が「SQLの構文」として解釈されることで成立する。対策はプレースホルダ(根本)+入力検証+WAFの3層。ORMでも生SQLの文字列連結は同罪
- 最小権限の原則: アプリ用アカウントにDROPやGRANTは不要。用途ごとにアカウントを分け、接続元を絞り、共有アカウントは禁止
- 暗号化は通信時(TLS)・保存時・カラム単位の3場面。ただしパスワードは暗号化ではなくソルト付きハッシュで保存する
- 監査ログは「誰が・いつ・何をしたか」を改ざんできない場所へ。バックアップは3-2-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インジェクション — ユーザー入力の name と password を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していない」「ログイン試行の記録(監査ログ)がなく総当たり攻撃に気づけない」も指摘できれば上級です。
社内の勤怠管理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章)が普通で、消せない設計は監査の観点でも優れています。必要になった時点で必要な範囲にだけ付与すればよい——これが最小権限の運用です。