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

平文パスワードはログに“残り” ── 肝心の侵入は“痕跡すら残らない”

Avatar for kuroneko kuroneko
August 20, 2026

平文パスワードはログに“残り” ── 肝心の侵入は“痕跡すら残らない”

Log::info($request->all()) という1行のおかげで、平文パスワードとカード番号、さらに保存してはいけない CVV までログに残ります。一方、認証失敗を記録していなければ、1アカウントに532回の総当たりが来ていても痕跡すら残りません。残ってほしくないものが残り、残ってほしいものが残らない。この非対称を、設計で解きます。

社内エンジニア向けセキュリティ勉強会(全6回)の第6回、最終回です。テーマはセキュアログ設計とインシデント追跡。ログを「書けば安全」から「目的から設計するもの」へ置き換え、消す・残す・守る・追うの4つに分解します。第1回から第5回まで「それはログで追う話、第6回で」と送ってきた伏線を、この回で全部回収します。Laravel 13.x(新規)と FuelPHP 1.x(レガシー)が混在する現場を前提にしています。

疑似デモは1本、before / after の対比です。ログインと決済を1回ずつ通して $request->all() をそのまま出力すると、CloudWatch Logs に password、card_number、cvv が生のまま並びます(値は説明用のダミーで、カード番号は業界で周知のテスト用 Visa 番号です)。これは CWE-532、ログ自体が新しい漏洩源になった状態です。リダクションを通した after では、password は全マスク、PAN は先頭6桁と末尾4桁だけ、cvv はそもそも出力せず、代わりに相関ID を付けます。第4回の「平文で機微データを置かない」を、DB からログにまで広げる話でもあります。

扱うトピック
- ログの目的は3つ(抑止 / 検証・解析 / 原因究明)。ロギング要件は「先に全部出す」ではなく目的から決める
- 責務分離:アプリは構造化ログを stdout に書くだけ、送出と保管はインフラ層(ECS FireLens・awslogs・CloudWatch Agent → CloudWatch Logs)。12-factor の logs
- 必ず残すもの:認証成功・失敗(authn_login_success / authn_login_fail)、認可失敗(authz_fail = CRITICAL)、入力検証失敗、管理者操作・機微アクセス・鍵の使用。イベント名は自己流にせず標準の語彙で、重大度は RFC 5424 の8段階(数字が小さいほど深刻)
- 絶対に書かないもの:パスワード・暗号鍵・カード会員データ(PAN)・PII・セッションID / トークン・DB 接続文字列・ソースコード。ログは本番データより保護が薄い=書けば「より無防備な漏洩経路」を増やす(CWE-532)
- 消す設計:出力前に除外リストで機械的にマスク。パスワードは値を一切残さない(OWASP 除外リスト)、PAN は先頭6+末尾4(PCI DSS の表示要件)。消しすぎると追えなくなるので、消すのは機微データだけでイベント自体は残す
- 残す設計:構造化ログ(JSON)+相関ID で1リクエストを縦断(Laravel は Log::withContext / shareContext、標準は W3C Trace Context の traceparent、AWS では X-Amzn-Trace-Id)+タイムスタンプは UTC・RFC 3339 / ISO 8601 に統一。時刻がずれていると複数ソースを1本の線につなげられない
- 記録しないことのコスト:認証失敗を残していなければ CWE-778(重要イベントの記録漏れ)。検知が困難になり、フォレンジックの証跡が残らず、攻撃元の特定が困難または不可能になる
- 守る設計:S3 Object Lock による WORM(Compliance モードなら root でも消せない)、CloudTrail ログファイル整合性検証(SHA-256+RSA 署名)、CloudWatch Logs の保持は1〜3653日(約10年)で既定は無期限 ── 期間も目的から決める
- 追跡:Logs Insights で authn_login_fail を userid ごとに集計して総当たりを1クエリで可視化 → 人がクエリを叩く偶然の後追いから、メトリクスフィルタ+アラームで仕組みが叩く形へ(OWASP Top 10 の A09。2021年版 Security Logging and Monitoring Failures、2025年版で Security Logging & Alerting Failures に改称されアラートが強調された)→ サブスクリプションフィルタと相関ID で点を線に束ね、第5回の WAF ログ・VPC フローログも同じ集約先で相関
- 検知はゴールではなく受け渡し:ログの役割は「誰が・何を・いつ」を対応チームに正確に渡すこと。追ったログはデジタル証拠にもなり、証拠に必要な完全性を「守る設計」が技術で支える
- シリーズ回収表:第1〜5回の脅威を「残すログイベント」と「追跡手段」に割り付け直す(総当たり→authn_login_fail の集計とアラート、SQLi / SSRF→入力検証失敗ログ、XSS・セッション異常・CSRF 失敗→セッション管理失敗ログ、認証失敗・鍵アクセス→認証・鍵操作ログ、境界を素通りした攻撃→WAF ログ・VPC フローログ)

持ち帰る3つの結論
1. ログは目的から設計する
2. 機微データ(パスワード・クレカ・PII)はログに残さない
3. 攻撃・認証失敗の痕跡は必ず残す ── 追跡できないと侵入に気づけない

レガシー保守(FuelPHP 1.x)でも「明日からできる」形にしています。FuelPHP 1.x の Log も Monolog ベースなので重大度は付けられ、除外リストも相関ID も書けます(既定の log_threshold は L_WARNING、log_date_format は UTC の ISO 8601 ではないので揃える余地がある)。Next action は3つ、$request->all() や password・card・token を grep で棚卸ししてリダクションを通す、authn_login_fail と閾値アラートを有効にする、ログの保持期間と改ざん防止を確認する。Appendix には CloudWatch Logs の保持値一覧と料金構造、送出方式の比較(stdout→インフラ集約 と Monolog+AWS SDK での直送)、Monolog 3 の processor によるリダクション実装全文、Logs Insights クエリ集、改ざん防止・時刻同期・正規化の詳細と出典、そして IR・フォレンジック概観を付けています。

全6回は、ぜんぶ「作る側の設計」で守る話でした。次の一歩である組織的なインシデント対応(NIST SP 800-61・RFC2350・X.1060・JPCERT/CC)は、別シリーズの CySec 復習ログに委ねています。

想定読者:「いざという時のために、リクエストは丸ごと出しておけばいい」と思っていた"ちょっと前の自分"。
分類:OWASP A09:2021 Security Logging and Monitoring Failures(2025年版では Security Logging & Alerting Failures)、CWE-532(ログへの機微情報の書き込み)、CWE-778(重要イベントの記録漏れ)

Avatar for kuroneko

kuroneko

August 20, 2026

More Decks by kuroneko

Other Decks in Technology

Transcript

  1. 昔の私は“全部出せば役立つ”と思っていた 「いざという時のために、リクエストは丸ごと出しておけばいい」── 本当に? ❖ 「いざという時のために、リクエストは丸ごと 出しておけばいい」と考えていた ❖ ログイン・決済のリクエストを Log::info($request->all()) でそのまま出力

    ❖ そのログ、“何を残し・何を消したか”を設計し ましたか? // ログイン・決済コントローラ内 Log::info($request->all()); // ↑ リクエストを丸ごと出力していた // 何を残し、何を消したか未設計
  2. 今日のゴール: ログを設計し、侵入を追える状態にする ログは“消すもの(機微データ)”と“必ず残すもの(攻撃・認証失敗)”を、 目的から設計する 第1回 認証・認可 第2回 第3回 インジェクション /SSRF

    XSS/CSRF /セッション 第4回 第5回 第6回 暗号/シークレット ネットワーク/WAF ログ/追跡 想定読者は「ちょっと前の自分」。今日は「何を・どう書くか(設計)」と「残したログでどう追うか(追跡)」に重心を置きます。 話者: kuroneko
  3. 今日の道順と、持ち帰る結論3つ 道順: S1 なぜ・何を残すか → S2 デモ(機微データが漏れる) → S3 消す/残す設計

    → S4 追跡 結論① 結論② 結論③ ログは“目的”から設計する 機微データ(パスワード・ク レカ・PII)はログに残さない 攻撃・認証失敗の痕跡は“必 ず”残す ── 追跡できないと侵 入に気づけない
  4. アプリは“書く”だけ ── 集約はインフラ層 責務分離: アプリは“何を書くか”に集中/送出・保管はインフラ層(12-factor) ① 書く ② 集約 ③

    保管 アプリ (Laravel/FuelPHP) インフラ層 CloudWatch Logs FireLens/awslogs /CloudWatch Agent 集中ロギング =改ざん対策の土台 構造化ログを stdout に書くだけ アプリは“何を書くか”に集中 ── “どこへ送り・どう保管するか”はインフラの仕事
  5. 「必ず残す」ものを、名前と重大度で 重大度=RFC 5424 の8段階 ── 数字が小さいほど深刻 必ず残すイベント 標準イベント名 重大度 認証成功/失敗

    authn_login_success / authn_login_fail INFO / WARN 認可(アクセス制御)失敗 authz_fail CRITICAL 入力検証失敗 OWASP: 常時ログ対象 WARN 重要操作(管理者/機微アクセス/鍵) authz_admin 等 重要度に応じる
  6. デモ: ログイン+決済を“1回通すだけ” 疑似デモ ── ログ出力と CloudWatch Logs の中身を追う(値は説明用フィクスチャ) STEP 1

    STEP 2 STEP 3 ログイン+決済を通す $request->all() を出力 CloudWatch Logs を見る メール・パスワード ・カード番号 リクエストを 丸ごとログへ “何が”残るか 注目 ── ログに“何が”残るか?
  7. ★ 平文PWとカード番号がログに残った ロググループ /ecs/app/production にこう出力された(値は説明用フィクスチャ) ❖ password に平文が残る(CWE-532) ❖ card_number

    に PAN が残る(※テスト用 Visa 番号 ・実在カードではない) ❖ cvv まで残る=ログ閲覧権限を持つ全員に見える =ログ自体が漏洩源 Log::info($request->all()); { "level": "INFO", "message": "checkout request", "context": { "email": "[email protected]", "password": "P@ssw0rd!", "card_number": "4111111111111111", "cvv": "123" } }
  8. 対比: 消せば、ログに残らない 同じリクエストが、リダクション後は password=****・PAN=先頭6/末尾4 に変わる ❖ password は全マスク(値を一切残さない=OWASP 除外) ❖

    card_number は先頭6桁+末尾4桁のみ(PCI DSS の PAN 表示要件) ❖ cvv は保存要素ではない=そもそも出力しない。 request_id(相関ID)を付与 // after(リダクション後) { "level": "INFO", "event": "checkout", "context": { "email": "[email protected]", "password": "****", "card_number": "411111******1111", "request_id": "018f2a...-uuid" } }
  9. 消す設計: リダクション+除外リスト 除外リストで機械的に落とす=$request->all() を無検査で出さない(PW 全マスク・PAN は PCI 準拠) ❖ 出力前に機微キーを機械的にマスク(リダクショ

    ン) ❖ 除外リスト=パスワード・トークン・カード番 号・PII 等を列挙して落とす ❖ PAN は先頭6桁+末尾4桁のみ(PCI DSS)/ パスワー ドは値を一切残さない。実装全文は Appendix A3 // 概念コード(実装全文は A3) foreach ($context as $k => $v) { // 除外キーは全マスク if (in_array($k, $mask)) $context[$k] = '****'; // PAN は先頭6+末尾4のみ if ($k === 'card_number') $context[$k] = pci_mask($v); }
  10. 残す設計①: 構造化+相関ID+UTC 後で“追える”形で書く ── 構造化ログの1レコード例(相関ID request_id が複数ログを貫く) ❖ 構造化ログ(JSON)=機械で検索・集計できる ❖

    相関ID(request-id)を全ログに付与=1リクエスト を縦断(Log::withContext / shareContext) ❖ タイムスタンプは UTC・RFC 3339/ISO 8601 で統 一=複数ソースを相関できる { "@timestamp": "2026-07-04T09:15:32Z", "level": "INFO", "event": "checkout", "userid": "[email protected]", "request_id": "018f2a...-uuid" }
  11. 残す設計②: 認証失敗を必ず記録する authn_login_fail+重大度で総当たりの痕跡を残す ── 記録なし=CWE-778(検知・証跡が残らない) ❖ 認証失敗を標準イベント名 authn_login_fail で記 録(重大度

    WARN、連続失敗上限は authn_login_fail_max) ❖ 記録しなければ「重要イベントの記録漏れ」 =CWE-778 ❖ 第1回の総当たり・第4回の認証失敗/鍵アクセ スは、この1行の記録があって初めて追える Log::warning('authn_login_fail', [ 'event' => 'authn_login_fail', // userid は PII(方針次第でマスク) 'userid' => $email, 'ip' => $request->ip(), ]); // 記録なし = CWE-778(追跡不能)
  12. 残したログを“守る”設計 “必ず残す”には“消せない場所に守って残す”まで含む(結論③) ── WORM・整合性検証・保持 消せない場所(WORM) 改ざんを検知 保持を決める S3 Object Lock=書いたら削除

    ・上書き不可 (Compliance/Governance/リー ガルホールド) CloudTrail ログファイル整合 性検証=SHA-256+RSA 署名で 改変・削除を検知 CloudWatch Logs の保持は 1〜 3653日(≒10年)/既定は無期限 。目的から期間を決める
  13. ◆ Logs Insights で認証失敗を集計 1アカウント532回=総当たりが1クエリで見える(値は説明用フィクスチャ) ❖ CloudWatch Logs Insights で

    authn_login_fail を userid ごとに stats count(*) fields @timestamp, event, userid | filter event = "authn_login_fail" | stats count(*) as fails by userid | sort fails desc ❖ 直近1時間で1アカウントに532回の失敗=総当た り(ブルートフォース)が1クエリで見える # 結果(直近1時間): # [email protected] # [email protected] # [email protected] ❖ 「残していたから、追えた」 532 3 2 ← 総当たり
  14. 点を線に ── 相関で攻撃を1本に束ねる 手段: サブスクリプションフィルタ + 相関ID / トレースID 点

    束ねる 線 個別ログ 相関ID・トレースID 1つの攻撃の物語 認証失敗 / WAF / VPC フロー 同じ集約先へ配信 (サブスクリプションフィルタ) 素通りした攻撃を 1本の線で追う 第5回の WAF ログ・VPC フローログも同じ集約先で相関する
  15. 追ったログは“デジタル証拠”になる 証拠に必要なのは完全性 ── だから“守る設計”(WORM+整合性検証)が効く 追ったログ 証拠化 支える 認証ログ 等 デジタル証拠

    守る設計 追跡の成果 完全性が要る =改ざんされていない WORM+整合性検証 (S3 Object Lock 等) “守る設計”が「追えるだけでなく“証拠として使える”」を技術で支える
  16. シリーズ回収: 脅威→ログ→追跡に収束 各回の脅威を“残すログイベント”に割り付け、S4 の追跡手段(集計/アラート/相関)につなぐ 回 脅威 残すログイベント 追跡手段 第1回 総当たり(認証・IDOR)

    authn_login_fail(+重大度) userid 集計/閾値アラート 第2回 攻撃試行(SQLi/SSRF) 入力検証失敗ログ Logs Insights 検索/相関 第3回 XSS/セッション異常 /CSRF 失敗 セッション管理失敗ログ Logs Insights 検索 第4回 認証失敗/鍵アクセス 認証・鍵操作ログ (機微はリダクション) 集計・相関 第5回 素通りした攻撃(WAF/境界) WAF ログ・VPC フローログ 同じ集約先で相関
  17. FuelPHP でも今日から実装できる 「FuelPHP だから無理」で終わらせない ── 除外リスト・重大度・相関IDは実装できる ❖ FuelPHP 1.x も

    Monolog ベース =Log::warning/error で重大度を付けられる ❖ 除外リスト(機微データを落とす)・相関 ID(request-id を付ける)はレガシーでも書ける ❖ 「FuelPHP だから無理」で終わらせない=まず $request->all() の丸出しをやめる // FuelPHP 1.x も Monolog ベース // log_threshold=Fuel::L_WARNING // 時刻は UTC/ISO8601 に揃える Log::warning( 'authn_login_fail user='.$email .' ip='.Input::ip() );
  18. 脅威→対策マッピング(全編の背骨) シリーズ回収表と同じ看板を“対策視点”で再掲 ── ログは“消すもの”と“必ず残すもの”を目的から設計する 脅威 残す/消す 追跡・対策 機微データのログ混入 消す(リダクション+除外リスト) PAN=先頭6/末尾4、PW

    は残さない 攻撃・認証失敗が記録されず 気づけない 残す(標準イベント名+重大度+相関) 集計/アラート/相関 ログ改ざん・証拠にならない 守る(WORM+整合性検証) S3 Object Lock/CloudTrail 検証 共通 目的から設計する “書けば安全”ではない=設計する
  19. Next action: 明日からできる3つ 1=消す / 2=残す・追う / 3=守る ── 結論3点に対応

    1. 消す 2. 残す・追う 3. 守る $request->all()・password・ card・token を grep で棚卸し →あればリダクション 認証失敗ログ (authn_login_fail)+閾値アラー トを有効化 ログの保持期間と改ざん防止 (WORM/整合性検証)を確認
  20. シリーズ総括と、次の一歩 全6回が「作る側の設計」で守る話だった ── 深い IR/CSIRT は CySec へ ❖ 全6回の地図を回収(第1回

    認証・認可→第2回 インジェクション/SSRF→第3回 XSS/CSRF/セッション→第4回 暗号/シークレット→第5回 ネットワーク/WAF→第6回 セキュアログ/追跡) ❖ 全6回が「作る側の設計」で守る話だった。次の一歩=組織的なインシデント対応 ❖ 深い IR/CSIRT(NIST SP 800-61・RFC2350・X.1060・JPCERT/CC)は CySec 復習ログ #8〜 #11 へ ❖ Qiita / Speaker Deck でスライド・記事を公開
  21. A1: CloudWatch Logs の保持と料金 retentionInDays の指定可能値(G2)。既定は無期限。料金は公式料金ページを参照 ❖ 保持は retentionInDays で指定、期限切れは自動削

    除(G1) ❖ 既定は無期限(設定しなければ消えない =DeleteRetentionPolicy)(G2) ❖ 料金は取り込み/保管/分析(Logs Insights はスキャン 量課金)の3構造(G2) ❖ 具体的な単価は公式料金ページを参照(単価は変動 するため断定しない) 区分 retentionInDays 短期 1,3,5,7,14,30,60,90 中期 120,150,180,365,400,545 長期 731,1096,1827,2192 最長 2557,2922,3288,3653(≒10 年) 既定 無期限(未設定)
  22. A2: CloudWatch 送出方式の比較 (i)stdout→インフラ集約【本編主軸】 vs (ii)アプリ直送(Monolog+AWS SDK) 観点 (i) stdout→インフラ集約

    【本編主軸】 (ii) アプリ直送 (Monolog+AWS SDK) 責務分離 アプリは書くだけ/送出はインフラ アプリがルーティングに関与 12-factor 適合 適合(stdout に書く) 外れる IAM 権限 インフラ層が保持 アプリが CloudWatch 権限を持つ 手段 ECS FireLens/awslogs /CloudWatch Agent Monolog 専用ハンドラ +AWS SDK for PHP
  23. A3: リダクション実装全文(Monolog 3) processor は LogRecord を受け取り LogRecord を返す。context は

    readonly→with() で不変更新 ❖ class RedactSensitive implements ProcessorInterface に実装、__invoke(LogRecord $r): LogRecord ❖ $mask=['password','token','authorization','cvv'](OW ASP 除外)、card_number は先頭6+末尾4(PCI) ❖ context は readonly→複製し with(context: ...) で不 変更新。config/logging.php の processors に登録 function __invoke(LogRecord $r) { $ctx = $r->context; foreach ($ctx as $k => $v) { if (in_array($k, $mask)) $ctx[$k] = '****'; if ($k === 'card_number') $ctx[$k] = substr($v,0,6) .'******'.substr($v,-4); } return $r->with(context:$ctx); }
  24. A4: Logs Insights クエリ集+フィルタ 集計・相関のクエリと、件数抽出→アラーム・リアルタイム配信のフィルタ(G3) ❖ authn_login_fail / authz_fail の集計、相関IDでの

    縦断クエリ(filter request_id = ... | sort @timestamp) ❖ メトリクスフィルタ=件数抽出→アラーム接続 (G1/G3) ❖ サブスクリプションフィルタ=一致ログをリア ルタイムに他の宛先へ配信(G3) # 認証失敗を userid 集計(総当たり検出) fields @timestamp, event, userid | filter event = "authn_login_fail" | stats count(*) as fails by userid | sort fails desc # 認可違反を集計 | filter event = "authz_fail" | stats count(*) by userid, resource
  25. A5: 改ざん防止・時刻同期・正規化の詳細 守る/揃える設計の詳細と出典(F/G) 項目 内容 出典 改ざん防止 S3 Object Lock=WORM(Compliance/Governance/Le

    gal hold) AWS(G4) 完全性検証 CloudTrail ログファイル整合性検証 =SHA-256+RSA 署名・digest AWS(G4) 時刻 UTC・RFC 3339(=ISO 8601 プロファイル )・Z 表記 RFC3339(G14) 重大度 RFC 5424: 0 Emergency〜7 Debug(小さ いほど深刻) RFC5424(G13) 時間同期・正規化 全システムを信頼できる時間ソースで 同期・正規化・相関 CCT(F3)
  26. A6: IR・フォレンジック概観と CySec 誘導 本編(検知の受け渡し・ログ=証拠)が外出しした深掘りを集約 → 深い IR/CSIRT は CySec

    へ ❖ IH&R(CCT Module19)=準備〜封じ込め〜証拠収集〜根絶〜回復の9ステップ・一次 対応者・証拠保全 vs 被害拡大防止(F5) ❖ コンピュータフォレンジック(Module20)=証拠5ルール・Chain of Custody・認証ロ グ=証拠(F6) ❖ OWASP A09(G8)、CWE-532 / CWE-778(G9) ❖ 深い IR/CSIRT(NIST SP 800-61・RFC2350・X.1060・JPCERT/CC)は CySec 復習ログ #8〜 #11