メルマガを配信していると、読者から返信が届きます。この返信を管理画面で読めるようにしたとき、私は少し得意になっていました。届いた返信が、送った相手と自動で結びついて一覧に並ぶ。よくできた仕組みだと思ったのです。
こんにちは。AIでDXを加速する専門家、ばんのです。
その仕組みには穴がありました。届いたメールの差出人欄を、そのまま信じて読者と結びつけていたのです。差出人の欄は、送る側が好きな文字を書けます。読者のアドレスを書いたメールを送れば、誰でもその読者になりすまして私の管理画面に並べられる、ということです。
第2回では、AIが書いたコードの確かめ方を書きました。第3回は、そこで予告したとおり、本物の返信だけを通すために何を確認すればいいのかという話です。結論から言うと、私はここで設計を4回間違えました。
先にお断りしておくと、今回は連載の中でいちばん専門的な回です。メールの本人確認という、普段まったく意識しない仕組みの話が出てきます。細かい部分は、読み飛ばしていただいてかまいません。
それでもこの回を書くのは、お伝えしたいことがはっきりしているからです。この4つの間違いを、私は1つも自分では見つけていません。すべてAIが指摘してくれたものです。専門知識が必要な領域こそ、AIと組む価値がいちばん大きい。読んでいただきたいのはそこです。
この記事を読むとわかること
- メールの差出人欄が、なぜ信用できないのか
- なりすましを弾こうとして、正当な返信まで消しかけた話
- 「検査済み」の判定を、攻撃者自身が書けてしまう仕組み
- 安全機能を、あえて止めた状態で公開した理由
- 専門知識がなくても、この領域に踏み込めた理由
差出人の欄は、封筒の裏の手書きと同じ
意外に思われるかもしれませんが、メールの差出人欄は自己申告です。封筒の裏に書く差出人名と同じで、書いた内容が正しいかどうかは、郵便屋さんが確かめてくれるわけではありません。
私たちが普段それで困らないのは、受信側のサービスが裏側で検査してくれているからです。迷惑メールフォルダに勝手に振り分けられるのは、その検査の結果です。
ところが私が作ったのは、その検査済みのメールを保管する場所から、プログラムで直接メールを取り出す仕組みでした。迷惑メール判定の結果を見ずに、届いたものを全部取りに行っていたわけです。
これが具体的に何を招くのか、私が想定した順序で並べます。
| 起きること | 何が困るか |
|---|---|
| 読者を装ったメールが一覧に並ぶ | 本物の返信と見分けがつかない |
| 私がそれに返信する | 読者との過去のやり取りを、相手に読ませてしまう |
| 読者ごとの記録が汚れる | 「この人は何を聞いてきた人か」が信用できなくなる |
2行目がいちばん怖いところです。管理画面の返信ボタンは、届いたメールの差出人にそのまま返します。偽物に返信すれば、そのやり取りは偽物に届きます。
受け取ったメールに、5つの検問を置いた
対策として、取り込む前に順番に確認する検問を置きました。ここを通らなかったメールは、管理画面に出さずに捨てます。
| 検問 | 確認していること |
|---|---|
| 1 | そもそもメールとして壊れていないか |
| 2 | 自動応答や配信システムからの通知ではないか |
| 3 | 差出人欄が、アドレスの形をしているか |
| 4 | そのアドレスが、実際に登録されている読者のものか |
| 5 | 差出人が本人だと、受信サーバーが確認できているか |
1から4までは以前からありました。今回足したのが5番目です。そして、この1つを足すのに4回設計を間違えました。以下はその記録です。
補足しておくと、この検問の並び自体、私が考えたものではありません。「読者からの返信を管理画面に取り込みたい」と伝えたうえで、第2回に書いた「これが最悪の壊れ方をすると何が起きますか」を聞いた結果、AIが挙げてきたものです。私がやったのは、挙がってきた項目のうちどれを実際に入れるかを決めることでした。
間違い1. 検査項目を細かく見たら、正当な返信が消えた
メールの本人確認には、大きく3つの検査があります。仕組みの説明は省いて、性質だけ書きます。
| 検査 | 性質 |
|---|---|
| SPF | 送ってきたサーバーが正しいかを見る。転送されると必ず壊れる |
| DKIM | メールに付けた署名を見る。複数付いていて、1つ落ちるのは普通 |
| DMARC | 上の2つを踏まえた総合判定。どちらか片方が通れば合格 |
最初の設計は、SPFとDKIMを個別に見て、どちらかが失敗していたら捨てるというものでした。厳しくすれば安全だろうという発想です。
これは完全に間違いでした。読者が別のアドレスに転送して読んでいると、SPFはほぼ確実に失敗します。転送の途中でメールを持っているサーバーが変わるからです。この設計だと、転送で読んでいる読者からの返信が、なりすまし扱いで全部消えます。
正解は、総合判定であるDMARCの結果だけを見ることでした。この判定は、転送でSPFが壊れても署名のほうが通っていれば合格になります。細かく見るほど安全になるわけではない、という話です。
この指摘が出てきたときの説明は、「転送されるとSPFは必ず壊れます。個別に見ると、転送で読んでいる読者を全員失います」という一文でした。言われれば納得できますが、転送で読んでいる読者がいるという前提が私の頭になかった。ここは知識の差がそのまま出るところで、自分ひとりなら、読者から「返信しても反応がない」と言われるまで気づかなかったはずです。
なお、判定には「失敗」のほかに「検査できなかった」という結果もあります。これは相手側の一時的な不調で起きるので、捨てずに通します。第2回で書いた、こちら側の障害と相手側の問題を区別するという話と同じです。
間違い2. 「検査済み」の紙は、攻撃者も書ける
これがいちばん厄介でした。
検査の結果は、メールの中に一行の記録として書き込まれています。私はその記録を読んで判断しようとしました。ここで気づいていなかったのは、この記録を書けるのが1者ではないという点です。
| 書く人 | 信用できるか |
|---|---|
| 自分の受信サーバー | できる。これが本物の検査結果 |
| 途中を経由したサーバー(転送やメーリングリスト) | 正当だが、自分の検査結果ではない |
| メールの送り主 | できない。攻撃者が自分で「検査に合格した」と書ける |
つまり、送る側が本文に「このメールは検査済みです」と書き足しておけば、それを読んだ私のプログラムは合格と判断してしまいます。検問を置いたつもりが、自己申告を読んでいただけということです。
ここで私は2回続けて間違えました。
1度目は「いちばん上の1行だけ読む」という案です。自分の受信サーバーが書いた行は最後に足されるので、いちばん上に来るという理屈でした。ですがこれだと、転送されたメールで正当な中継サーバーの記録が上に来たときに、判断を誤ります。
2度目は「全部の行を読んで判断する」という案です。これは攻撃者が書いた行まで読むので、もっと悪い。
たどり着いた答えは、自分の受信サーバーの名前が書いてある行だけを読むというものでした。検査結果の記録には、書いた人の名前が入っています。自分のサーバーの名前と一致する行以外は、全部無視します。
身分証を確認するとき、本人が「私は本人です」と書いた紙ではなく、発行元が誰かを見る。当たり前のことですが、コードにするまでこの当たり前を落としていました。
ここは、知らなければ手も足も出ない領域です。検査結果の記録を誰が書けるのかは、メールの規格を定めた文書に書いてあります。ただし、そこに「攻撃者はこう悪用できます」とまとめて書いてあるわけではありません。仕様を読める人が、悪用のされ方まで想像して、はじめて出てくる指摘です。
私がやったのは「これで安全ですか」と聞き直しただけです。それだけで、1度目の案も2度目の案も否定されて、3度目に正解が出てきました。専門書を読み込む時間があれば自力でも行き着けたかもしれませんが、本業の合間にやっている身としては、現実的な選択肢ではありませんでした。
間違い3. 「空欄」を「無し」の代わりに使った
これは地味ですが、実害が大きいたぐいの間違いです。
プログラムの中で「自分のサーバー名はまだ分かっていない」という状態を表すために、空欄を使っていました。名前が入っていなければ未確定という意味です。
ところが、サーバー名が空欄の記録というのが実在します。すると「未確定」と「空欄という名前が確定している」が区別できません。この取り違えが起きると、読む行が1行ずれます。1行ずれた先には、攻撃者が書いた行があるかもしれないわけです。
解決は「未確定」を表す専用の状態を使うだけでした。日常の言葉でいえば、アンケートの「無回答」と「回答したが空欄」を同じ扱いにしない、ということです。集計する立場なら、この2つを混ぜたら困るとすぐ分かります。それがコードの中になると、途端に見えなくなります。
間違い4. 枠を確保したつもりが、上限になっていた
捨てたメールは、理由とともに記録に残すようにしました。黙って消える仕組みは怖いからです。記録は直近50件までとして、古いものから消えていきます。
ここで、なりすまし判定で捨てたものが、他の理由で捨てたものに押し流されて消えないよう、専用の枠を確保しようとしました。ところが書いたコードは、確保ではなく上限として働いていました。守るはずの枠が、逆に上限を課していたわけです。
この手の間違いは、実際に数を入れて確かめれば一発で分かります。40件入れたら40件、60件入れたら50件、両方の理由で40件ずつ入れたら25件ずつ。この確認をして、はじめて意図どおりだと言えます。
なお、この数字を入れて確かめる作業も、私は指示しただけです。「意図どおりか、数を入れて確かめてください」と伝えれば、確認用の手順を作って結果を表にして返してきます。手を動かす部分は任せて、私は返ってきた数字が期待どおりかを見るだけで済みました。
検査そのものが失敗したら、通すのか止めるのか
監査で出てきた指摘の中に、私の発想になかったものがありました。検査の処理そのものが失敗したときの話です。
記録を読み取る処理には、複雑な文字列の照合を使っています。この照合は、極端に長い入力を与えられると処理を諦めることがあります。そして諦めたとき、「該当なし」と同じ結果を返します。
つまり、攻撃者がわざと巨大なメールを送れば、検査を諦めさせられる。そして諦めた結果は「問題なし」と解釈され、素通りします。検問所が、処理しきれない量の車が来ると、ゲートを開けたまま止まるようなものです。
対策として、照合の前に余計な空白などを削ぎ落として、処理が重くならない形に整えました。625キロバイトの極端なメールを流して4ミリ秒、諦めは発生しないところまで確認しています。
安全機能を作るときは、その機能が正しく動いたときだけでなく、動けなかったときにどちらへ倒れるかまで決めておく。ここは覚えておく価値のある観点でした。
作った安全機能を、あえて止めた状態で公開した
ここまで作り込んだ検問5ですが、公開時の初期設定はオフにしました。設定画面で自分でオンにするまで、動きません。
理由は単純で、この検問は受信サーバーが検査結果を書いてくれていることが前提だからです。書いていないサーバーで有効にすると、確認する材料がないまま全部を捨てかねません。なりすましを防ぐつもりで、本物の返信を一通も受け取れなくなります。
なので順序を分けました。まず捨てた記録が見える画面を用意して、自分の環境で検査結果がちゃんと付いていることを確認する。それからオンにする。今も私の環境ではオフのまま様子を見ています。
安全機能を作った直後は、早く有効にしたくなります。ですが、守るはずのものを壊す設定を、確認せずに入れる理由はありません。
専門知識がなくても、この領域に踏み込めた理由
ここまで読んで、難しいと感じた方が多いと思います。実際、難しい領域です。ですが、注目していただきたいのは中身そのものではありません。この難しい領域を、私が調べ回ることなく、対処まで持っていけたという事実のほうです。
今回の作業で、私とAIの分担は次のようになっていました。
| 私がやったこと | AIがやったこと |
|---|---|
| やりたいことを日本語で伝える | 必要な検問を挙げる |
| 最悪どう壊れるかを聞く | 規格に沿った判定方法を選ぶ |
| 出てきた案に「これで安全ですか」と聞き直す | 自分の案の穴を指摘する |
| どこまでやるかを決める | 確認用の手順を作り、結果を出す |
私の側に、専門用語を覚える工程は入っていません。SPFやDMARCという名前は、この記事を書くために整理して、はじめて自分の言葉で説明できるようになったものです。作っている最中に必要だったのは、用語の知識ではなく「本物かどうかを確かめずに読者と結びつけていいのか」という、日本語の問いのほうでした。
セキュリティの話になると、素人が手を出すと危ないと言われます。それは半分当たっていて、半分は今の状況に追いついていない指摘だと感じています。危ないのは、危ないところがどこにあるか分からないまま作ることです。そして、その場所を教えてもらうことは、今はもう難しくありません。
現に、この記事に出てきた4つの間違いは、どれも実際に事故を起こす前に潰せています。世に出す前に、専門家に近い視点でひととおり見てもらえる。個人で何かを作る立場からすると、これは数年前とはまったく違う条件です。
もちろん、丸投げで安全になるわけではありません。第2回に書いたとおり、AIは指示していないことは守りませんし、書いた本人に採点させれば「問題ありません」と返ってきます。効いたのは、書く役とは別に検査役を立てて、指摘がゼロになるまで回すという段取りのほうです。この機能では3周かかりました。
段取りさえ決めておけば、知識のない領域でも一定の水準まで持っていける。今回いちばん実感したのはここでした。
よくある質問
Q1. 実際になりすましメールは届いたのですか
私の環境では、今のところ確認していません。この対策は、被害が出たから作ったものではなく、仕組みとして可能だと気づいたから作ったものです。
起きてから直すのでは、汚れた記録を後から選り分ける作業が発生します。読者とのやり取りを扱う場所なので、先に閉じておきたいと考えました。
Q2. 既製の配信サービスを使えば、こういう心配は不要ですか
この部分に関しては、そのとおりです。返信の扱いはサービス側が用意しているので、利用者が考えることではありません。
自作は、こうした見えない部分まで自分の担当になります。第1回で自作を選んだ理由を書きましたが、その代償がここです。全部を自分で決められることと、全部を自分で決めなければならないことは同じ意味です。
Q3. 設計を4回も間違えて、よく気づけましたね
私が気づいたわけではありません。第2回で書いた、書く役と検査役を分ける方法で出てきた指摘です。
この機能では検査を3周させて、指摘がゼロになるまで回しました。1周目で「厳しすぎて正当な返信が消える」、2周目で「検査結果は攻撃者も書ける」というように、直すたびに次の角度から指摘が出てきます。
Q4. セキュリティの知識がないまま作って、本当に大丈夫でしょうか
「知識がないまま」と「確認しないまま」は別の話だと考えています。危ないのは後者です。
私も、この記事の内容を事前に知っていたわけではありません。それでも危ない箇所を潰せたのは、作る前に「最悪どう壊れるか」を聞き、作った後に別の役として検査させ、指摘がなくなるまで回したからです。順番を決めておけば、知識は都度もらえます。
逆に、この段取りを飛ばして「動いたから完成」とすると、知識のあるなしにかかわらず危ないものができます。実際、私の最初の案は3回とも動いてはいました。動いていたけれど、守れていなかったのです。
Q5. 同じことを自分の環境で確認するには
お使いのメールソフトで、受信したメールの詳細な情報を表示させてみてください。Gmailなら「メッセージのソースを表示」です。
その中に本人確認の結果が並んでいます。差出人欄に書かれた名前と、実際に送ってきたサーバーの情報が別々に載っているのが見えるはずです。この2つが別物だという感覚が持てれば、この記事で書きたかったことは伝わっています。
ばんのの視点:疑う対象を、間違えないこと
今回の4つの間違いは、種類が違うようで根は同じでした。誰が書いた情報なのかを確かめずに、書いてある内容だけを読んでいたという一点です。
差出人欄は送り主が書いたものでした。検査結果の記録も、送り主が書き足せるものでした。第2回で紹介したエラー文言も、相手のサーバーが書いたものでした。判断材料の中身を精査する前に、その材料を誰が用意したのかを見る。順序として、こちらが先です。
これはプログラムに限った話ではありません。取引先から届いた請求書の口座が変わっているとき、書いてある内容ではなく、それを書いたのが本当に取引先かを疑えるかどうかで結果が変わります。実際、その手口の被害は今も起き続けています。
自分でシステムを作ると、この判断を毎回自分で下すことになります。面倒ではありますが、普段の仕事で何を信じて動いているのかが、はっきり見えてくる面白さもあります。
そして、その判断に必要な材料は、今はAIに出してもらえます。難しくて手が出せないと思っていた領域が、聞き方さえ決まっていれば入っていける場所に変わりました。今回の記事でいちばん伝えたかったのは、なりすまし対策のやり方そのものより、そちらのほうです。
次回予告
第4回は、作った機能が数字を生まなかった話です。
記事の中にメルマガの登録バナーを自動で入れる機能を作り、どのバナーが効いているかを比べられるようにしました。ところが、しばらく運用してレポートを開くと、表示回数は千件を超えているのに、登録数だけが全バナーでゼロのままでした。
原因は、バナーのデザインでも文言でもありませんでした。数字が出ないとき、どこを疑えばいいのか。その調べ方をお届けします。