この章で分かること: ログとエラーメッセージという「うっかり情報が漏れる二大ルート」を塞ぐ設計と、攻撃者にヒントを与えない最小開示の実装。
聴く台本
今日の話は、ひとことで言うと「書かれていないものは漏れない」だ。攻撃されて漏れるんじゃなくて、自分たちが普段、よかれと思って書き残しているもの、見せているもの、そこから漏れる。そういう地味だけど一番多い漏れ方を、設計で先回りして潰していく回だよ。
まず一番やられがちなのがログだ。ログって開発してると本当に便利でさ、何か動かないときに「とりあえずここで中身を全部出しておこう」ってやりたくなるよね。リクエストの中身、ユーザーが入力した値、APIのレスポンス、丸ごとログに吐く。デバッグ中はそれで助かる。でもね、その「全部出す」が事故の入り口なんだ。
なぜログが危ないかというと、ログには三つの怖い性質があるからなんだ。一つ目、長期保管される。アプリのデータは消しても、ログは別のサーバーに何ヶ月も、下手したら何年も残る。二つ目、広く読まれる。運用してる人、調査する人、外部の監視サービス、いろんな人とシステムの目に触れる。三つ目、コントロールが効きにくい。一度どこかに転送されたログを「あれ消して」って全部追いかけるのはほぼ無理なんだ。だから、本来アプリの中で厳重に守ってるはずの個人情報や秘密が、ログという裏口からダダ漏れになる。守りの固い金庫を作っても、隣に中身を書き写したメモ帳を置きっぱなしにしてたら意味がないよね。それがログなんだ。
具体的にログに書いちゃいけないものは何か。まず個人情報。名前、メールアドレス、電話番号、住所。それから秘密情報。パスワード、APIキー、トークン、クレジットカード番号。さらに見落としがちなのが、リクエストヘッダーの中の認証情報だ。よく「リクエストを丸ごとログに出す」ってやると、その中のAuthorizationヘッダー、つまりログイン状態を証明するトークンまで一緒に記録されちゃう。これを見た人は、そのユーザーになりすませる。本人のパスワードを知らなくても、だ。
で、実装ではどうするか。原則はシンプルで、ログに出すのは「何が起きたか」であって「中身そのもの」じゃない、と切り分けるんだ。たとえば決済が失敗したとき、「カード番号4242-4242-…の決済が失敗」じゃなくて、「ユーザーID 1234 の決済が失敗、理由コードはcard_declined」と書く。誰がどうなったかは追えるけど、生のカード番号はどこにも残らない。個人情報は記録する前にマスキングする、つまり伏せ字にする。メールなら頭だけ残して「t…@example.com」みたいにね。これは003の事業、つまり他社の機密データをAIに預かるビジネスだと、もう絶対の前提なんだ。お客さんの会話ログや判断データをそのまま生でログに吐いてたら、「データを預けられない会社」と判断されて事業が終わる。だから僕らは、AIに渡す前にも、ログに書く前にもマスクする。「書かれていないものは漏れない」を構造で担保するんだ。
さて、二つ目のルートがエラーメッセージだ。これも親切心が仇になるパターンでね。開発中って、エラーが出たら原因がすぐ分かるように、画面に詳しい情報を出すよね。どのファイルの何行目で落ちたか、どんなSQL文を投げたか、データベースの何ていうテーブルにアクセスしたか。これがスタックトレースってやつだ。スタックトレースっていうのは、プログラムがどこをどう通って落ちたかを示す内部の足跡のことね。開発中はこれが命綱だ。でも、これを本番でユーザーにそのまま見せちゃダメなんだ。
なぜかというと、攻撃者にとってエラーメッセージは「設計図の断片」だからだ。SQL文がそのまま表示されれば、テーブル名やカラム名が分かる。ファイルパスが見えれば、サーバーのディレクトリ構造が読める。使ってるフレームワークやそのバージョンが分かれば、「あ、このバージョンには既知の穴があるな」と狙い撃ちされる。一個一個は些細でも、攻撃者はこういう断片を拾い集めて全体像を組み立てる。つまり、丁寧なエラーメッセージは、知らないうちに敵に地図を渡してるんだ。
じゃあ実装ではどうするか。原則は「詳細はサーバーログへ、ユーザーには最小限」だ。エラーが起きたら、詳しい中身、スタックトレースも例外の内容も全部、サーバー側のログに記録する。これは調査のために絶対要る。でもユーザーの画面に返すのは、「エラーが発生しました。お手数ですがしばらくして再度お試しください」くらいの、当たり障りのない一言だけ。そして、その裏で発行したエラーIDみたいな短い番号を一個だけ添える。ユーザーが問い合わせてきたら、「そのIDで調べますね」とサーバーログを引ける。ユーザーには中身を見せず、でも自分たちは追える。この非対称が大事なんだ。
ここで一個、設計の落とし穴を言っておく。エラーの出し分けで、攻撃者にヒントを与えないって観点だ。たとえばログイン画面で、メールアドレスが存在しないときは「そのメールは登録されていません」、パスワードが違うときは「パスワードが違います」って親切に出し分けたとするよね。一見ユーザー思いだ。でもこれ、攻撃者からすると「このメールアドレスはこのサービスに登録済みだ」と教えてもらえる仕組みなんだ。片っ端からメールアドレスを試せば、誰が会員かが分かってしまう。だから本番では、どっちが間違ってても「メールアドレスかパスワードが正しくありません」と、わざと同じ曖昧な答えを返す。これも「攻撃者にヒントを与えない情報設計」の一つだ。正直すぎる応答が、そのまま漏洩になるんだよ。
最後にもう一つ、攻めの設計を足しておく。「見たらバレる」アクセスログだ。さっきから「漏らさない、見せない」って守りの話をしてきたけど、内部の人間が悪意なく、あるいは悪意を持って機密データを覗くケースもある。これを「絶対見れないようにする」のは現実には難しい。運用上どうしてもアクセスが要る場面があるからね。そこで発想を変える。見れないようにするんじゃなくて、「見たら必ず記録が残る」ようにするんだ。誰が、いつ、どの顧客のどのデータにアクセスしたか、全部自動でログに刻む。本人が消せない形でね。これがあると、覗くこと自体に強い抑止がかかる。003の事業だと、これは「オーナーであっても顧客DBにアクセスしたら全件自動記録される」という形で約束してる。「見れません」より「見たらバレます」のほうが、構造として強くて、しかも正直なんだ。
それから、もう触れておきたいのが削除フローだ。お客さんが「もう解約します、データ消してください」と言ったとき、本当に消えてるか。表向き画面から見えなくしただけで、裏のテーブルやバックアップにこっそり残ってる、っていうのが一番まずい。だから完全削除をちゃんと設計して、しかも「いつ、何を削除しました」という証跡を残して、それをお客さんに渡せるようにする。消したことを証明できて初めて、削除は完了なんだ。ここでも「書かれていないものは漏れない」が効く。完全に消えていれば、その先どんな事故が起きても、もうそのデータは存在しないんだから漏れようがないからね。
この章のまとめ(実装で守る一線)
ログには「何が起きたか」だけを書き、個人情報・トークン・秘密は記録前にマスキングする。リクエスト丸ごとログは認証情報まで漏らすので禁止。本番のエラーは詳細をサーバーログへ、ユーザーには曖昧な一言とエラーIDだけ返し、スタックトレース・SQL・パスを表に出さない。ログインの失敗理由は出し分けず、攻撃者に存在を教えない。そして機密へのアクセスは「見れない」より「見たら全件自動で記録が残る」設計にし、削除は完全削除と証跡の発行までやって初めて完了とする。