設計

個人情報をログに残さない — 出力の直前に、自動で伏せる

黒い扉にかけられた南京錠
Photo by Nishess Shakya on Pexels

個人情報を扱う業務ツールを作っていると、データの守り方にいろいろ気を配ります。通信を暗号化する、保存先を限定する、外部に送るものは最小限にする。ひと通りやったつもりでいても、意外と見落としやすいのが「ログ」なんですよね。

この記事は、アプリのログに個人情報を残さないために考えたことの話です。考え方が中心で、コードは最後にひとつだけ載せます。

ログは個人情報の盲点になりやすい

開発中、動きを確認するために、つい変数やオブジェクトをまるごとログに出すことがあります。「この案件、いま何が入ってるんだろう」と、オブジェクトをそのまま出力してみる、みたいな場面ですね。

これが盲点で、そのオブジェクトの中には顧客の名前や連絡先が入っていることがあります。デバッグのつもりで出したログに、実名や電話番号がそのまま記録されてしまう。しかもログは消し忘れて本番にも残りがちで、ログ基盤の中に個人情報がじわじわ溜まっていきます。

アプリ本体でどれだけ気をつけても、ログという裏口から個人情報が出ていたら台無しです。

ログ関数に「自動で伏せる」を一枚かませる

そこで、ログを出す関数を直接使うのをやめて、間にラッパーを一枚かませることにしました。

開発者はこれまで通り、オブジェクトをログ関数に渡すだけです。ただし渡された中身を、ログに書き出す直前にチェックして、名前や連絡先のような項目を自動で *** に置き換えてから出す。開発者が毎回「これは個人情報だから出しちゃダメ」と意識しなくても、仕組みのほうが勝手に伏せてくれる、という形にしました。

logger.info("案件を作成しました", { dealId, customer })
        │  ログに書き出す直前に、中身を自動チェック
        ▼
{ dealId: "a1b2c3…", customer: { name: "***", phone: "***" } }
        │
        ▼
ログ基盤へ(名前や電話は残らない。ID は残る)

ポイントは、開発者の手間を増やさないことです。「気をつける」をルールにすると、いつか誰かが忘れます。仕組みのほうで自動的に伏せるようにしておけば、忘れても安全側に倒れます。

何を伏せて、何を残すか

少し考えたのが、何を伏せて、何を残すか、です。

伏せるのは、名前・フリガナ・電話・メール・住所、それから自由記述のメモ欄や、顧客名が混ざりがちなファイル名。要するに、個人を特定できそうな項目はまとめて伏せます。

逆に、残すものもあります。案件や顧客を識別するための ID です。これは「a1b2c3…」のような、それ単体では誰のことか分からない記号です。ログを見ただけでは、どの ID が誰なのかは分かりません(もちろん、社内のデータベースと突き合わせれば辿れるので、ID も大切に扱う情報ではあります)。むしろ、何か問題が起きたときに「どの案件で起きたのか」を追うには、この ID が手がかりになります。

なので方針はこうしました。迷ったら伏せる。ただし、調査に要る ID だけは残す。 個人を特定できる情報は消して、追跡に使う記号は残す、という線引きですね。

残っても困らないように

仕組みで伏せても、それで終わりにはしませんでした。

ひとつは、ログ自体を一定期間で自動的に消す設定にしたことです。ログをいつまでも貯めておく理由はありませんし、残っている期間が短いほど、万一のときの被害も小さくて済みます。

もうひとつは、それでも個人情報を扱う以上、漏れたときにどう動くかを先に決めて文書にしておいたことです。影響範囲をどう確認するか、ご本人にどう連絡するか、どこに報告するか。起きてから慌てて調べるのではなく、手順を先に書いておく。使うことがないのが一番ですが、備えがあるだけで落ち着いて動けます。

仕組みは、これくらいシンプルです

実装はやはり短いです。渡されたオブジェクトを再帰的にたどって、伏せたいキー名だったら値を *** に差し替えるだけです。

const PII_KEYS = new Set(["name", "phone", "email", "address", "memo", "fileName"]);

function maskPII(value, depth = 0) {
  if (value == null || depth > 6) return value;            // 深すぎる入れ子は打ち切り
  if (Array.isArray(value)) return value.map((v) => maskPII(v, depth + 1));
  if (typeof value === "object") {
    const out = {};
    for (const [key, v] of Object.entries(value)) {
      out[key] = PII_KEYS.has(key) && v ? "***" : maskPII(v, depth + 1);
    }
    return out;
  }
  return value; // 文字列・数値などはそのまま
}

あとは、ログを出す関数の手前でこの maskPII を必ず通すようにして、生のログ関数を直接呼ばないことだけ決めておきます。キー名で判定しているので、ID のような「残したい記号」は自然と残り、名前や連絡先だけが伏せられます。

まとめ

個人情報の扱いは、つい「保存するデータ」や「外部に送るデータ」に目が行きますが、ログも同じくらい気を配る価値があります。考え方はシンプルで、ログに名前は要らない、ID さえあれば追える。だったら名前は最初から出さない、というだけです。

  • ログは個人情報の盲点になりやすい(デバッグの出力に実名が紛れる)
  • 「気をつける」ではなく、仕組みで自動的に伏せる
  • 特定できる情報は消し、調査に要る ID は残す
  • 伏せた上で、保持期間を短く+漏れたときの手順を先に用意しておく

ログは普段あまり注目されない部分ですが、個人情報を扱うなら、ここを締めておくと安心感が違います。