あらまし
僕が個人会社で開発している別のウェブアプリで、利用者が持ち込んだ API キーが、エラーの応答とサーバのログという 2 本の経路から漏れうると分かり、その両方を塞ぎました。このアプリには利用者が自分の API キーを持ち込んで外部の API を呼ぶ機能があり、預かった鍵をサーバに保存せず、ログにもデータベースにも残さないことを設計の前提に置いています。今回選んだのは、見つかった二箇所を直して終わりにせず、この前提そのものを、常設のテストと、アプリのデータを書き出す先をそもそも作らないガードの側に預けて守り続ける、という設計判断です。
見つかった欠陥は、外部の API を呼ぶ二つのエンドポイントの例外処理が、捕まえた例外のメッセージ文字列を応答ボディとコンソール出力へ出していたことです。そのメッセージにリクエスト由来の文字列、たとえば鍵の混じった URL が乗ると、預かった鍵が、鍵を持ち込んだ利用者本人への応答と、僕が運用するサーバのログの両方へ漏れます。この経路が 2 本ありました。
修正は三層で組みました。第一層は実装の修正で、応答ボディを汎用の失敗メッセージへ固定して詳細フィールドを削り、応答とログの両面を一度に塞ぎました。第二層は、漏れたら一目で分かる目印を埋めた偽の鍵を全分岐に流し、外部の API への認証ヘッダー以外のどの送信先にも目印が現れないことを実行時に突き合わせる常設のテストです。第三層は、永続化しそうな名前の識別子を宣言された時点で弾く静的解析と、アプリのデータを残す先を端末の中のあらかじめ決めた保存先だけに絞るガードで、書き出す先そのものを作らせません。三層は連続した 3 つのコミットで積み、既存のテストを一つも変えずに、テストを 199 件から 234 件へ、すべて通ったまま増やしています。
この一件で書きたかったのは、鍵の漏れを二箇所直したこと自体よりも、直したことをどう守り続けるかです。例外のメッセージに鍵が入っていないか確かめてから出せば安全に見えますが、鍵が乗るかどうかは動かす環境と失敗の起き方で変わるので、メッセージを見ただけでは鍵の有無を判断しきれません。そこで、鍵を残さないという運用上の約束を人が覚えておくのに頼らず、そもそもデータを書き出す先をコードから作らせない形にしました。
購読で読める本文は、判断した順にたどる作りです。まず、鍵をサーバに持たないと決めた設計と、その前提を破りかけた例外処理の漏れ道から始めて、応答とログを一度に塞いだ直し方をたどります。次に、直したことを守り続けるために積んだ、偽の鍵を流す常設のテストと、書き出す先そのものを作らせない静的解析とガードの組み方、と続きます。最後に、三層のそれぞれが何を捕まえ何を取りこぼすかの限界と、まだ確認できていない点を書いて、この鍵の一件をこれまでの検査の積み上げの上に載せた、という話で締めています。
対象読者と持ち帰れるもの
利用者から預かった秘密、たとえば API キーやトークンのような認証の秘密を保存しない前提でサービスをつくっていて、その保存しないという前提を、運用上の約束ではなく実装の挙動として守り続けたい開発者に向けた記事です。例外のメッセージ文字列のように、外から来た文字列がそのまま出力へ載る失敗を、一つの型として捕まえて塞ぐ考え方と、実装の修正に、目印を埋めた偽の鍵を全分岐に流す実行時テストと、永続化しそうな識別子を宣言時に弾く静的解析とガードを重ねる三層の組み方、そして各層が何を取りこぼすかの限界の見極めを持ち帰れます。永続化の API を使うのをやめるという運用上の約束と、そもそも書き出す先を作らないという構造での封じがどう違うかも、具体的な設計判断として持ち帰れます。この記事は、僕が個人会社で開発している別のウェブアプリで実際に見つけて直した鍵漏れと、その修正を守り続けるために積んだテストとガードの記録に基づいています。
ここから先は有料部分です
続きは、この記事だけの単発購入か、すべての有料記事が読める購読で読めます。
有料部分はおよそ 2,900 字で、読み終える目安は 6 分です。
価格は税込みです。購入と購読は、ログインしてから進みます。購入した記事は、アカウントでいつでも読み返せます。購読中の方と購入済みの方はログインして全文を読むページへ、プランの説明はプランのページへどうぞ。
販売の対象地域は利用規約に記載しています。請求はすべて日本円で行います。