Upgrade to Pro — share decks privately, control downloads, hide ads and more …

その“隠したつもり”が命取り ── 自前と平文をやめて「正解」に委ねる

その“隠したつもり”が命取り ── 自前と平文をやめて「正解」に委ねる

「MD5 + 固定ソルト」で保存したパスワードは、逆引きサービスに入れた瞬間に平文へ戻ります。`.env` にベタ書きした APP_KEY や AWS キーは、あとで `git rm --cached` しても履歴に残り続けます。守り方も置き場所も、"隠したつもり"が命取りになります。

社内エンジニア向けセキュリティ勉強会(全6回)の第4回です。暗号・データ保護・シークレット管理という広いテーマを「秘密には守り方(どう保管するか)と置き場所(どこに置くか)の両方が要る」という2枚看板で束ね、共通の敗因=自前実装と平文、共通の正解=検証済みのフレームワーク/マネージドサービスに委ねる、という一本の思想に収束させます。Laravel 13.x と FuelPHP 1.x/PHP 標準の両方で、危険なコードと安全なコードを並べます。

疑似デモは2つ。md5("password") のハッシュ値を逆引きに投げて一瞬で平文に戻るところ("複雑そう"な P@ssw0rd も同様)。そして .env をコミットしてから削除しても、`git show <過去コミット>:.env` に平文の鍵がそのまま残るところ。だから対策は「削除」ではなく「ローテーション」だ、という結論まで持っていきます。

扱うトピック
- 前提整理:ハッシュ(一方向・照合向け)と暗号(可逆・鍵で戻せる)は別物。「パスワードを暗号化」も「データをハッシュ」も誤り
- パスワードの守り方:MD5/SHA1 が保存に向かない理由(速い=総当たり向き/ソルト無しはレインボーテーブルで一括/衝突既知)、ソルト・ストレッチング(work factor)・専用アルゴリズムの3つの武器、OWASP 第一推奨の Argon2id と bcrypt、Laravel の Hash::make / check / needsRehash、PHP 標準の password_hash / verify / needs_rehash
- 可逆データの守り方:Laravel Crypt(AES-256-CBC + MAC)と APP_KEY による Cookie・セッションの暗号化、そして APP_KEY が漏れれば暗号は全部無効になるという急所
- 秘密の置き場所:.env は .gitignore + 共有は変数名だけの .env.example、Git の不変履歴、AWS Secrets Manager / Parameter Store への集約と使い分け、IAM 最小権限(特定 ARN に限定)、鍵ローテーション

持ち帰る3つの結論
1. パスワードは Hash::make に委ね、MD5 で保存しない
2. 可逆データは Crypt で暗号化し、APP_KEY を守る
3. 秘密はマネージドに置き、最小権限 + ローテーション

レガシー保守(FuelPHP 1.x)でも「明日からできる」棚卸しと設定確認まで示します(自前ハッシュの grep、.env の履歴確認、本番の鍵を1つマネージドへ)。Appendix には Argon2id/bcrypt の推奨パラメータ、Secrets Manager と Parameter Store の詳細比較、git 履歴から秘密を除去する手順も付けています。

想定読者:「平文じゃないから安全」と思っていた"ちょっと前の自分"。
分類:OWASP A02:2021 Cryptographic Failures、CWE-327/328(脆弱な暗号・ハッシュ)、CWE-259/798(資格情報のハードコード)、CWE-312(機密情報の平文保存)

Avatar for kuroneko

kuroneko

August 06, 2026

More Decks by kuroneko

Other Decks in Technology

Transcript

  1. 今日のゴール: 秘密を自前と平文で守らない型を持ち帰る 秘密は守り方も置き場所も、自前と平文をやめて FW/マネージドに委ねる 第1回 認証・認可 第2回 第3回 インジェクション /SSRF

    XSS/CSRF /セッション 第4回 第5回 第6回 暗号/シークレット ネットワーク/WAF ログ/追跡 想定読者は「ちょっと前の自分」。今日は守り方と置き場所の2本柱を扱います。 話者: kuroneko
  2. 今日の道順と、持ち帰る結論3つ 道順: 守り方と置き場所の両方 → パスワードの守り方 → 可逆データの守り方 → 秘密の置き場所 結論①

    結論② 結論③ パスワードは Hash::make に 委ね、MD5 で保存しない 可逆データは Crypt で暗号化 し、APP_KEY を守る 秘密はマネージドに置き、最 小権限+ローテーション
  3. 今日の背骨: 秘密は「守り方」と「置き場所」で守る 守り方=どう保管するか / 置き場所=どこに置くか ── 片方だけでは漏れる 守り方: どう保管するか 置き場所:

    どこに置くか パスワード=ハッシュ データ=暗号 鍵・APIキー・DBパスの 保管先 正解=守り方も置き場所も FW/マネージドに委ねる(両方に共通)
  4. デモで見るもの: 保存されたMD5ハッシュを逆引きに入れる ここに注目 ── 元のパスワードに戻るか STEP 1 STEP 2 保存ハッシュ

    逆引きに投入 MD5+固定ソルトの ハッシュ値 逆引きサービス/ レインボーテーブル STEP 3 結果を見る 元のパスワードに戻るか 注目 ── 不可逆のはずの値が、元のパスワードに戻るか?
  5. 一瞬で平文に戻った ── MD5/SHA1は“隠せていない” ハッシュ値が即 password に戻る(値は説明用フィクスチャ) ❖ ハッシュ値 → password

    が即表示 ❖ 「複雑そう」な P@ssw0rd も同様に戻る ❖ レインボーテーブル・GPU総当たりに無力 md5("password") = 5f4dcc3b5aa765d61d8327deb882cf99 → 逆引き結果: password md5("P@ssw0rd") = 161ebd7d45089b3446ee4e0d86dbcf92 → 逆引き結果: P@ssw0rd
  6. 正しく守る3つの武器: ソルト / ストレッチング / 専用アルゴリズム 武器と、それが効く脅威 武器 効果 効く脅威

    ソルト 毎回ランダムで一意にする 使い回し レインボーテーブル ストレッチング 繰り返して遅くする (work factor) 総当たり 専用アルゴリズム Argon2id / bcrypt GPU総当たり
  7. bcrypt / Argon2id が“3つの武器”を全部内蔵する OWASP 第一推奨=Argon2id(bcrypt はレガシー向け) ソルト同梱 コスト調整 GPU耐性

    自動生成しハッシュに同梱 work factor で遅さを調整 Argon2id はメモリ消費で GPU 総当たりに強い
  8. Laravel: Hash::make が正しく全部やってくれる さっき一瞬で戻った MD5 が、この1行で解決する ❖ Hash::make($pw)=既定 bcrypt・ソルト自動 (argon/argon2id

    選択可) ❖ Hash::check で照合、Hash::needsRehash で work factor 変更を検知 ❖ 自分でソルトもアルゴリズムも書かない use Illuminate\Support\Facades\Hash; // 登録: 既定 bcrypt・ソルト自動 $hash = Hash::make($pw); // 照合 Hash::check($pw, $hash); // work factor 変更で再ハッシュ Hash::needsRehash($hash);
  9. この自前実装が、さっきのデモの正体 ── MD5+固定ソルト before(レガシー自前)── 固定ソルト+MD5 で保存 ❖ md5($salt.$pw) を自前で保存 ❖

    固定ソルトをコードに埋め込み(全員同じ) ❖ 「平文じゃない」だけで安全と誤解 // レガシー自社の自前実装 $salt = 'my_app_secret_salt'; // ↑ 固定ソルト(全員同じ) return md5($salt . $pw); // ↑ MD5=高速・衝突既知=逆引き可能
  10. 正しくは言語標準に委ねる ── MD5/SHA1で保存するな after ── 言語標準 password_hash / password_verify に委ねる

    ❖ password_hash($pw, PASSWORD_DEFAULT)=既定 bcrypt・ソルト自動同梱 ❖ password_verify で照合、password_needs_rehash で更新 ❖ FuelPHP なら Auth(PBKDF2)に任せる手もある // 正しくは言語標準に委ねる $h = password_hash($pw, PASSWORD_DEFAULT); // ↑ ソルト自動生成・ハッシュに同梱 password_verify($pw, $h); password_needs_rehash($h, PASSWORD_DEFAULT);
  11. Laravelは APP_KEY で暗号化を提供 ── Cookieもセッションも自動 自前 AES 実装は書かない ── FW

    の Crypt に委ねる ❖ Crypt::encryptString で機密データを暗号化(AES256-CBC + MAC) ❖ Cookie・セッションは APP_KEY で暗号化・改ざ ん署名(第3回の続き) ❖ 改ざんされた暗号値は DecryptException use Illuminate\Support\Facades\Crypt; // 可逆データを暗号化(AES-256-CBC + MAC) $enc = Crypt::encryptString($token); // 復号(改ざんは DecryptException) $plain = Crypt::decryptString($enc);
  12. APP_KEY が漏れれば、暗号は全部無効 ── 鍵こそ最重要の秘密 漏洩 → 全復号の連鎖 STEP 1 STEP

    2 APP_KEY 漏洩 暗号が全部無効 平文.env/Git に漏れる 署名偽造・全データ復号 STEP 3 置き場所問題へ 守り方の鍵自体が 置き場所問題に つまり“守り方”の鍵自体が、次の“置き場所”問題になる
  13. デモで見るもの: .envをコミット→後で消すだけ ここに注目 ── 後で削除しても履歴に残るか STEP 1 .env に秘密 APP_KEY・DBパス

    APIキー STEP 2 STEP 3 うっかりコミット 後で削除 git add . でコミット 履歴に残るか 注目 ── 後で削除しても、履歴に残るか?
  14. .envをコミット → git rm --cached しても… .gitignore に足して追跡除外 ── 作業ツリーからは消えたが…(値は説明用フィクスチャ)

    ❖ .env を一度コミット ❖ 後から .gitignore 追加 + git rm --cached .env ❖ 作業ツリーからは消えたが… $ cat .env APP_KEY=base64:J63qRTDL...stY= DB_PASSWORD=super-secret-db-pass AWS_SECRET_ACCESS_KEY=wJalrX...KEY $ git add . && git commit -m "cfg" # あとで .gitignore に追加し追跡除外 $ echo ".env" >> .gitignore $ git rm --cached .env
  15. git show/GitHub検索に、平文の鍵が残っていた 第2回で SSRF から抜いた IAM 鍵が、.env にも平文で(値は説明用フィクスチャ) ❖ git

    show <過去コミット>:.env で APP_KEY / AWS キーが平文 ❖ GitHub 上ならコード検索・secret scanning で即 発見 ❖ 第2回で抜いた IAM 鍵が .env にも漏れていた $ git log --oneline 9f2a1c3 remove .env from tracking 3b7d0e1 add config ← 平文の鍵が残る $ git show 3b7d0e1:.env APP_KEY=base64:J63qRTDL...stY= AWS_SECRET_ACCESS_KEY=wJalrX...KEY # ↑ 平文で丸見え
  16. 今の問題: 削除だけでは無意味、ローテーションが要る 平文+Gitの不変履歴 ── 削除では消えない STEP 2 STEP 1 .env

    に平文 見えれば終わり Git 履歴に固定 クローン/フォーク キャッシュ/PRに残存 STEP 3 発見→悪用 git show / GitHub検索 STEP 4 ローテーション 必須 削除だけでは無意味 一度漏れた鍵は、削除だけでは無意味 ── ローテーションが要る
  17. .envはコミットしない ── 漏れたら鍵ローテーション 共有は変数名だけの .env.example ── ローカル開発の正解(本番は次スライド) ❖ .gitignore に

    .env(コミットしない) ❖ 共有は変数名だけの .env.example(値は空) ❖ 漏れたら即ローテーション(削除では戻らない) # .gitignore に .env を必ず入れる echo ".env" >> .gitignore # 追跡済みなら外す(履歴には残る) git rm --cached .env # 共有は .env.example(値は空): # APP_KEY= / DB_PASSWORD= / AWS_SECRET_ACCESS_KEY=
  18. AWSの正解: 秘密は Secrets Manager / Parameter Store に委ねる 秘密をコード/.env から追い出しマネージドに集約

    use Aws\SecretsManager\SecretsManagerClient; ❖ アプリは IAM ロールで起動時に取得(SDK) ❖ 暗号化(KMS)・アクセス制御・監査が付く ❖ 秘密はコード/.env に書かない // 認証は IAM ロールから自動取得 $client = new SecretsManagerClient([ 'region' => 'ap-northeast-1', ]); $res = $client->getSecretValue( ['SecretId' => 'prod/app/db']); // 全文は Appendix A2
  19. 使い分け: Secrets Manager vs Parameter Store 回す資格情報は SM / 静的

    config は PS(詳細は Appendix A5) 観点 Secrets Manager Parameter Store 料金 $0.40/月+$0.05/1万API 標準=無料 / 拡張=有料 自動ローテーション あり(RDS 等統合) 組み込みなし 暗号 KMS SecureString=KMS 最大サイズ ─ 標準4KB / 拡張8KB 向く用途 回す DB 資格情報 静的 config
  20. IAM最小権限: アプリのロールは“必要な秘密だけ”読める 第2回 SSRF と結線 ── 抜かれても被害を限定 ❖ secretsmanager:GetSecretValue を

    特定 ARN に限定 { ❖ 全シークレット読み取り(*)にしない } ❖ SSRF から鍵を抜かれても、最小権限なら被害を 限定 "Effect": "Allow", "Action": "secretsmanager:GetSecretValue", "Resource": "arn:...:secret:prod/app/db-*"
  21. レガシー/FuelPHP でも今日から: .env を外し、SDK で取得 「FuelPHP だから無理」で終わらせない ❖ まず .env

    を .gitignore + 履歴から除去 + ローテーション ❖ AWS SDK for PHP で SM/PS から起動時取得 ❖ レガシーでも段階的に移せる
  22. Take-away: 秘密は守り方も置き場所も、自前と平文をやめて委ねる 核心: 自前と平文をやめて委ねる ── これだけ持ち帰ってください 結論① 結論② 結論③ パスワードは

    Hash::make に 委ね、MD5 で保存しない 可逆データは Crypt で暗号化 し、APP_KEY を守る 秘密はマネージドに置き、最 小権限+ローテーション
  23. Next action: 明日からできる3つ 抽象で終わらせず、明日からの具体に落とす 1. 棚卸し 2. .env 確認 3.

    マネージド MD5/SHA/自前ハッシュを grep → Hash::make / password_hash へ .gitignore にあるか・過去に 上げてないか(あればローテ ーション) 本番の鍵を1つ SM/PS へ・最 小権限ロールで取得
  24. A1: 全文コード ── パスワードハッシュ 本編と同じコード ── before(MD5)/ after(言語標準)/ Laravel Hash

    ❖ レガシー自前 MD5+固定ソルト(before / 本編と 同じ) ❖ 言語標準 password_hash / verify / needs_rehash(after / 下記全文) ❖ Laravel Hash::make / check / needsRehash // after: 言語標準に委ねる(全文) $h = password_hash( $pw, PASSWORD_DEFAULT); password_verify($pw, $h); password_needs_rehash( $h, PASSWORD_DEFAULT);
  25. A2: 全文コード ── 暗号化とシークレット取得 本編と同じコード ── Crypt / .gitignore /

    IAM / AWS SDK 取得(下記) ❖ Laravel Crypt::encryptString / decryptString + APP_KEY ❖ .gitignore + .env.example / IAM 最小権限ポリシー ❖ AWS SDK for PHP で SM/PS(SecureString)から取得( 下記) use Aws\SecretsManager\SecretsManagerClient; $client = new SecretsManagerClient([ 'region' => 'ap-northeast-1', ]); $res = $client->getSecretValue( ['SecretId' => 'prod/app/db']); $db = json_decode( $res['SecretString'], true);
  26. A3: Argon2id/bcrypt パラメータ詳細 OWASP 推奨値と PHP/Laravel の既定 対象 推奨/既定値 OWASP

    推奨順 Argon2id > scrypt > bcrypt > PBKDF2 Argon2id 最小 m=19456, t=2, p=1 bcrypt work factor 最小10 PHP PASSWORD_DEFAULT bcrypt(既定 cost 12) Laravel bcrypt / argon ['rounds'=>12] / ['memory'=>1024,'time'=>2,'threads'=>2] ペッパー 別保管の共有秘密
  27. A4: 暗号理論はCySec復習ログへ 圧縮した暗号理論の受け皿 ── 深掘りは CySec 復習ログ #5 暗号学 /

    #7 危殆化 へ 関数 出力 状態 MD5(参考) 128bit コリジョン発見=保存不適 SHA-0 160bit コリジョン発見 SHA-1 160bit コリジョン発見(SHAttered 2017) SHA-2 112〜256bit 現行 SHA-3 ─ 現行
  28. A5: Secrets Manager vs Parameter Store 詳細比較 料金・容量・ローテーションの詳細比較 観点 Secrets

    Manager Parameter Store 料金 $0.40/月+$0.05/1万API 標準=無料 / 拡張=有料 ローテーション 新バージョン作成(追加課金なし) 組み込みなし 暗号 KMS SecureString=KMS 最大サイズ ─ 標準4KB / 拡張8KB 個数上限 ─ 標準1万 / 拡張10万 ポリシー/クロスアカウント ─ 標準非対応 / 拡張対応
  29. A6: git履歴から秘密を除去する手順 + 用語・出典 本編がスコープを絞って外出しした深掘りの受け皿 ❖ git-filter-repo で履歴書き換え ── まず秘密を失効/ローテーション。削除後もクロー

    ン/フォーク/キャッシュ/PR に残る ❖ OWASP A02:2021 Cryptographic Failures / Password Storage・Secrets Management Cheat Sheet ❖ CWE-327/328/259/331(暗号)・CWE-798/312(秘密のハードコード・平文保存)