秘密情報の非露出を担保する設計(API・ログ・テスト)
秘密を含む接続文字列は、組み立てる前に構成要素を検証する — パーサのエラー型が raw 入力を文字列化してログへ残す
5日前
パスワードを含む URL 形式の接続文字列(DB の DSN 等)を組み立てるとき、構成要素を無検証で埋めるとパーサのエラーメッセージ経由で資格情報がログへ残る。「DSN をログに出さない」という契約をコードに書いていても、同じ関数の中で破られる。
機序
一般的な URL パーサは、入力に制御文字(改行等)が含まれるとパースを拒否し、そのエラー型に元の入力文字列を保持する。エラーの文字列表現はその raw 入力を含むため、パスワードごと出力される。さらにドライバによってはこの失敗を戻り値ではなく panic で返すため、スタックトレースとともに標準エラーへ出て、コンテナログに永続化される。
発火点は特殊な攻撃ではなく、管理画面の環境変数欄へ値を貼るときの末尾改行のような日常的な事故でよい。
判断基準
- 「エラーログに DSN を出さない」だけでは不十分。自分が書いたログ出力だけを見ていると、ライブラリが同じ情報を別経路で出す
- operator が自由文字列を与えられる構成要素(ホスト名・TLS モード・オプション等)を設定化するときは、受け付ける値の集合を列挙して検証する。集合が小さい項目なら完全列挙ができる
- 検証の位置は設定読み込み層。接続文字列を組む直前ではなく、値がプロセスに入った直後に弾く。設定読み込み関数がエラーを返すシグネチャを持ちながら常に nil を返しているなら、そこが最初の実用途になる
- 副次効果として、設定ミスが panic ではなく読めるメッセージになる
確認方法
制御文字を含む値・大文字違い・typo・前後の空白を入力にしたテストを置き、設定読み込みがエラーを返すことを固定する。同時に、受け付けるべき値を全部通すテストも置く(検証が厳しすぎて正当な設定を拒む退行を防ぐ)。
AI に管理基盤を操作させるときは「秘密が混ざる出力経路」を先に潰す — 値をファイル参照で渡し、レスポンスとログは投影してから出す
5日前
AI エージェントに PaaS・オーケストレーターの管理 API や本番ログを操作させると、秘密は「渡し方」ではなく戻り方から漏れる。実作業で1セッション中に同じ本番 DB パスワードを3回会話ログへ流出させた経験から、経路は3つに類型化できる。
漏れる3経路
- リソース作成 API のレスポンス: 生成した資格情報を作成結果に含めて返す実装が多い。作成のたびに平文で返る
- サービス・コンテナのログ取得: 起動スクリプトが接続文字列を引数に持つツールを呼ぶと、その行がログに残る。ログの末尾を取っただけで資格情報が出る
- リソース詳細取得 API: 環境変数一式をそのまま返す。「一覧が見たいだけ」の読み取り呼び出しが最も油断しやすく、実際にこれが3回目の流出だった
判断基準
- 渡す側より受け取る側を疑う。秘密を引数として渡さない工夫(値をファイルへ置き、コマンド内でその場読み出しに展開する)は有効だが、対策として不十分。それは入力面しか塞がない
- 読み取り系の呼び出しほど危険と扱う。書き込み系は身構えるが、読み取り系は「確認するだけ」と素通しにしがちで、環境変数や接続情報を含むペイロードが丸ごと出る
- 秘密を返しうる API は、呼び出しの直後に投影を挟むのを既定にする。応答をそのまま出力せず、必要なフィールドだけを抜き出して表示する。「まず生で見て、必要なら絞る」の順にしない
- ログ取得は既知の秘密パターンをマスクする整形を必ず通す。接続文字列の書式は限られるので、その形にマッチする部分を伏せ字へ置換してから出す
落とし穴
- ファイルへ置いた秘密をコマンド内で読み出す方式は、値が会話ログに出ない点で有効だが、プロセス引数には現れる。共有ホストでは別の露出面になる。標準入力へ流し込む形にできるならそちらが強い
- 流出したら必ずローテーションする。会話ログ・セッション記録は永続化されるので「見なかったことにする」は成立しない。ローテーションは DB 側の変更だけでなく、管理基盤に保存された値とアプリの環境変数の両方を更新しないと、次回の再作成で古い値が復活する
確認方法
作業前に「この API は秘密を返しうるか」を一度だけ確かめ、返すなら投影のひな型を決めてから本番作業に入る。事故後の再発防止として、同じ API を次に呼ぶときも投影を通す。
「非露出」保証のガードテストは、危険な値の発生源まで遡ってセンチネルを流す—手作り fake は意図しない値を構造上送れない
5日前
「あるフィールドはAPIレスポンスに絶対に含まれてはならない」という不変量を回帰テストで守るとき、ガードが本番コード(handler・DTO構築子)を実際に呼び出していても、その入力を作る層に手作り fake(usecaseモック等)が介在すると、ガードは実質的に空通しになることがある。
原因: 手作り fake はテスト作者が想定したフィールドだけを値付けして構築する。守りたい不変量の対象フィールドが、fake を作った時点ではまだDTOに存在しなかった(もしくは意図的に除外されていた)場合、fake はそのフィールドを永遠にゼロ値のまましか返さない。将来、実装側の構造体(usecaseのモデル型)に同名フィールドが追加され、DTOへ配線される退行が入っても、fake は自分の構築ロジックを自動で更新しないため、ガードは依然としてgreenのままになる。この失敗モードは、「本番データでは値が非ゼロだが、テストのfakeだけはゼロ値のまま」という最も危険な乖離を生む。
これは既知の「否定形ガードは本番コードを実呼びしなければ空通し」という教訓を一段進める。「本番コードを呼ぶこと」は必要条件であって十分条件ではない—その本番コードの入力を作るすぐ下の層が手作りの場合、その層はテスト作者が想定しなかった値を構造上送れない。
対策: 「絶対に漏れてはならない」種の不変量を守るガードは、値が本当に発生する場所(実 DB の行・外部 API の実レスポンス等)まで遡って配置する。具体的には、守りたいフィールドにセンチネル値(他と衝突しない、ひと目でそれと分かる文字列)を持つ行を実データストアへ投入し、実 repository → 実 usecase (もしくは同等の中間層)→ 実レスポンス構築を一本のパイプラインとして通し、最終出力にセンチネル値が現れないことを表明する。これはintegrationテスト相当のコストがかかるが、「絶対に漏らしてはいけない」種の不変量(PIIの非露出等)は、このコストを支払う価値がある。
併用: ユニットテスト層の許可リスト型ガード(レスポンスのキー集合完全一致など、別の失敗クラスを担当)は破棄せずに並行して残す。両者は直交する失敗クラス(契約外キーの出現 vs 特定フィールドの実データ経由の漏洩)を担う。
検証方法: 守りたいフィールドを実際に実経路(SELECT・中間モデル・シリアライズ層)へ配線する変異を作り、(a) センチネル境界テストが落ちることと、(b) 同じ変異でユニットテスト層の許可リストガードは通ってしまうこと、の両方を確認する。後者が実際に起きれば、ユニットテスト層だけでは不十分という識別反証になる。
適用範囲: データベース列だけでなく、外部APIの実レスポンスやファイルメタデータなど、「本番では値が乗るが、テストの手作り入力はutil的にゼロ値しか作らない」構造を持つすべてのレイヤー間境界に一般化できる。
Go の omitempty はゼロ値フィールドをJSON出力から丸ごと消すため、レスポンス観測型の契約テストは検知方式を問わず盲目になる
5日前
struct field に json:"...,omitempty" を付けた場合、そのフィールドがゼロ値(空文字・0・nil・空スライス等)のままだと、encoding/json はキーごとJSON出力から省略する。DTOへ新しいフィールドを追加しても、値を組み立てる側(コンストラクタ/presenter)で非ゼロ値を配線しない限り、レスポンスJSONにそのフィールドの痕跡は一切現れない。
この結果、レスポンスを外側から観測する契約テストは、検知手法を変えても効果が変わらない。文字列非包含チェック(特定のキー名がボディに含まれないことを表明する)でも、JSONをmapへデコードしてキー集合の完全一致を取る許可リスト方式でも、出力バイト列が変異前後で完全に同一になるため、どちらも検知できない。「より厳密な検証方式に置き換えれば防げるはずの穴」に見えて、実際には防げない。
検証方法: 対象のDTOにフィールドを追加し、(a) omitempty付き・値は未配線(ゼロ値)のまま、(b) omitemptyを外す、(c) omitemptyは維持したまま実際に非ゼロ値を配線する、の3パターンでレスポンスJSONの差分を比較する。(a)だけが変異前後で出力が同一になり、(b)(c)はどちらの検知方式でも検知できる。
実務上の含意: 「新しいフィールドを追加しても既存の許可リスト検査で守られる」という期待は、そのフィールドが将来ゼロ値のまま出荷される可能性がある限り成立しない。この種の退行を本当に防ぎたいなら、レスポンス観測ではなく、DTOのstruct定義そのものを静的に検査する(許可されたフィールド一覧との照合、reflectによるタグ列挙など)方式を別途組み合わせる必要がある。ただし静的検査はDTOをstructから組み立てない経路(map直返し等)には効かないため、両方式はトレードオフの関係にあり、片方だけで完全な検知にはならない。
適用範囲: Go以外でも、シリアライズ時に「値が空なら出力しない」という指定を持つ仕組み全般(JavaScriptの独自replacer、JacksonやGsonのNON_EMPTY/NON_DEFAULT系include設定等)に一般化できる可能性がある。「値が空のフィールドを省略する」直列化オプションを使っている箇所では、レスポンス観測型のテストがこの種の退行に対して原理的に無力になりうる、という前提でテスト設計を見直す価値がある。
公開レスポンスから外す属性は DTO ごと消す — 分岐で隠すと不在テストが素通りする
6日前
未認証で誰でも読める公開応答に載せないと決めた属性(個人名など)の扱い方。
判断
条件分岐やマスキングで隠すのではなく、取得クエリからも DTO からもフィールドごと消す。露出しないことを「実装が正しく隠し続けること」ではなく「そもそも値がその経路に存在しないこと」で担保する。表示側の実装ミスや、別クライアントの追加で漏れる経路が構造的に無くなる。
公開ページの全件が検索インデックスに載る設計(一覧から個別ページを機械生成する類)では、露出面が最大になるため、この差が特に効く。
見落としやすい検証の穴
「レスポンスに当該キーが含まれないこと」を文字列の非包含で表明する回帰テストは、シリアライザの空値省略オプションと組み合わさると素通りする。テストのフェイクやスタブが当該フィールドにゼロ値しか返さない限り、キー自体が出力されないため、後から DTO にフィールドを足しても落ちない。
反証力を持たせるには次のどちらかにする。
- 応答オブジェクトのキー集合を許可リストと完全一致で突き合わせる(未知のキーが増えたら落ちる)
- 保存層にセンチネル値を置き、データ取得から応答生成までの実経路を通す境界テストにする
適用条件
「その属性を出すかどうか」を利用者ごとに切り替える必要がある場合は分岐が要るので、この判断は当てはまらない。全経路で一律に出さないと決められる属性にだけ使う。
「現れないこと」を守るテストには、守る対象が実在したというポジティブコントロールを対で置く
5日前
機密値・個人情報・禁止語が出力へ現れないことを回帰テストで守るときの判断。
問題の型
安全性の表明が「出力に X が含まれない」という否定形だけで構成されていると、入力側が壊れたときにテストが黙って通り続ける。X を仕込むはずの seed ・ fixture ・前提条件が壊れて X がそもそも存在しなくなっても、否定形のアサーションは当然に成立するからである。
たちの悪さは、壊れ方が「赤」ではなく「永久に緑」であること。守っているつもりの検知器が何も検証していない状態が、誰にも気づかれずに続く。ヘルパーの統合やリファクタで前提の一行が落ちる、というただの整理が引き金になる。
判断
否定形の安全性表明には、「守る対象が確かにそこにあった」という肯定形の表明を対で置く。センチネル値を仕込んだなら、仕込んだ直後にそれを読み戻して一致を表明する。1・2行で済み、これがあると前提が壊れたときに静かな緑ではなく赤になる。
同じ発想は否定形一般に効く。「エラーが出ないこと」を見るなら対象処理が実行されたことを、「遷移しないこと」を見るなら操作が届いたことを、別途表明する。
確かめ方
検知器を信じる前に、守る対象を意図的に漏らせる変異を当てて落ちることを見る(検知力の実証)。合わせて、前提を壊す変異(seed の仕込みを外す)でも落ちることを見ると、肯定側と否定側の両方に感度があることを確かめられる。
適用条件
守る対象が重大なほど割に合う。逆に、否定形が軽い補助的なチェックなら、ポジティブコントロールのコストが見合わないこともある。