推測で直さない:バグ・脆弱性修正に「着手前の実測」と「着手後の独立検証」を義務づける
この記事の要点
ある業務システムの受託開発で、1週間に十数件の修正(バグ・情報露出・移植漏れの疑い)を処理する際、全ての修正に「着手前の実測」「計画承認」「着手後の独立検証」の3ゲートを課す運用を試した。本記事はその手順を、どの現場でも再現できる形に一般化したものだ。
この運用を入れた途端、次のようなことが繰り返し表面化した。
- 「不在カラムが原因」だと思っていたら、不在カラムは症状で、根本原因は別にあった
- 「移植漏れ」として起票したチケットが、調べると原典が意図的に『足さない』と判断済みで、修正不要だった
- 「一部だけ直せばいい」と優先度を付けたら、実測で対象すべてがフロントから呼ばれていた
- 自分が立てた前提が、着手直前の調査で丸ごと誤りだと分かった
要するに、着手前に測らなければ半分近くは間違った方向に労力を使っていた。これは腕や真面目さの問題ではなく、単に「測っていなかった」ことの帰結だった。
想定する環境
- PHP / RDB を使った、ある程度歴史のある業務システム
- 巨大サービスから機能を切り出す移植作業が混在(移植元=「原典」の挙動が正とされる場面がある)
- マルチテナント構成(テナントごとに可視データが変わる認可が絡む)
- Issue 駆動・PR ベースの開発。進捗はステータスドキュメントに実測値つきで記録
何が問題だったのか
困っていたのは修正の品質ではなく、判断の品質だった。次の3つが頻発していた。
1. 症状と原因の取り違え
「このカラムが無いからエラーになる」という報告にカラムを足すと一見直る。だが本当は「そのカラムを参照している設計自体がおかしい」のが根本で、カラム追加は症状対処にすぎなかった。
2. 見積もりと実態の乖離
Issue に書かれた「ここ◯箇所を直す」「影響は限定的」は、起票時点の推測であって着手時点の事実ではない。実測すると箇所数も影響範囲も違う。
3. 同じ名前が文脈で意味を変える
staff_id のようなありふれた名前のカラムが、ある画面では操作者、別の画面では対象者を指していた。名前だけ見て「同じ意味」と思い込むと、認可の穴や取り違えを生む。
これらはすべて、報告・Issue・自分の記憶を"事実"として扱っていたことに起因する。
対応:全修正に課した5ステップ
ステップ1:着手前調査(事実を実証する)
直す前に、問題が本当に起きることを再現・実証する。セキュリティ系なら脆弱性を実際に突いて見せる。同時に「影響範囲を件数で測る」。
-- 「どれだけ漏れうるか」を直す前に数える(カラム名・テーブル名は一般化した例)
SELECT
tenant_id,
COUNT(*) AS exposed_rows
FROM target_table t
LEFT JOIN access_scope s
ON s.record_id = t.id
AND s.tenant_id = :current_tenant
WHERE s.record_id IS NULL -- スコープ外なのに見えてしまう行
GROUP BY tenant_id
ORDER BY exposed_rows DESC;
この時点で「実害0件(露出しうるが実データが無い)」なのか「大多数のテナントに影響」なのかが決まり、優先度が確定する。優先度は推測ではなく件数で決める。
ステップ2:計画を立てて承認を取る
実証した事実をもとに是正計画を書く。「どのクエリを」「どの条件で絞るか」を明記し、ビジネス判断が要るものは照会する。理想は、自分の前提が誤っていれば計画段階で潰れること。実際、「一部だけ直す」という自分の案が丸ごと誤りで、対象をまとめて削除するのが正解だったことがこの段階で判明した。
ステップ3:実装
計画どおりに直す。計画から外れる発見があればステップ1に戻る。
ステップ4:着手後の独立検証(別経路で確かめる)
実装者の「直りました」を信じない。 別の方法で穴が埋まったことを再現する。有効だったのは次の2つ。
- 変異(mutation)的な確認:修正後のガードを一時的に壊すと確かに漏れる/落ちることを確認し、「ガードが実際に効いている」裏を取る
- 別経路での再計測:ステップ1と同じクエリを別の切り口(外部システム経由、NULL を含むデータ)で走らせ、結果が想定どおり変わったことを確認する
// 変異テストの発想:ガードを外した版では必ず落ちることを固定する
public function test_スコープ外アクセスは拒否される(): void
{
$outsider = $this->makeUserInOtherTenant();
$this->actingAs($outsider)
->get('/resources/'.$this->targetId)
->assertForbidden(); // ← これが通ることが「穴が埋まった」証拠
// ガードを無効化したダブルでは 200 が返る=ガードが本当に効いている
}
ステップ5:計画と実測の差分を記録する
最後に、「計画でこう見積もり、実測でこうだった」の差分を必ず残す。「見積もりは◯箇所→実測は違った」「分類が途中で覆った」といった履歴が、チームの判断ログになり、次の見積もり精度を上げる唯一の資産になる。
PR/ステータスに貼るテンプレート
## 着手前調査(実証) - 再現/実証: <どう再現したか・脆弱性なら突いた手順> - 影響範囲(実測・件数): <件数 / 対象テナント> - 実害: 0件 / 限定 / 広範 ## 計画 - 方針: <何をどう直すか> - 要照会のビジネス判断: <あれば> ## 実測との差分 - 見積もり: <起票時の想定> - 実測: <実際にどうだったか> - 覆った点: <前提が誤っていた箇所> ## 着手後の独立検証 - 手法: 変異 / 別経路再計測 - 結果: <穴が埋まったことをどう確認したか>
レビュー時の確認ポイント
- 「着手前調査」と「独立検証」の2ブロックが両方揃っているか
- 影響範囲が件数で書かれているか(「軽微」「限定的」などの主観語だけになっていないか)
- 検証が「実装者本人の目視」ではなく、別経路・変異で取られているか
注意点
- 「忠実な移植」と「機能後退」は紙一重。原典の挙動を再現するつもりが、原典が意図的に落としていた挙動まで再現すると改善が止まる。逆に、原典が実測のうえで「足さない」と判断した項目を「移植漏れ」と誤認して足すと、余計な複雑さが戻る。原典の判断理由まで遡ること。
- 件数は必ず自分で測る。Issue に書かれた件数は起票時点のスナップショットで、着手時には変わっている。
- このワークフローは1件あたりのコストを上げる。だが「間違った方向への実装」を減らす効果のほうが、歴史あるシステムでは圧倒的に大きい。
まとめ
報告・Issue・自分の記憶を"事実"として扱わない。着手前に実測し、優先度は件数で決め、「直った」は変異や別経路で独立に検証し、計画と実測の差分を記録する。測るのはコストだが、測らないと「半分は間違った方向に全力で走る」。歴史あるシステムほど、この差は効いてくる。
受託開発・レガシー改善・セキュリティ是正に関するご相談を承っています。
