二重に防御していたら、片方が壊れていることが見えなくなりました

前回は、権限の検証が「他人のデータが無かったから」通ってしまった話でした。
今回は、同じバグが4日間隠れていた理由のふたつめです。

こちらは、自分が良かれと思ってやったことが原因でした。

目次

絞り込みを2箇所でやっていました

データベースを設計した工程で、こういう方針を決めていました。

アプリを信用せず、DB で守る。

アプリのコードは書き忘れます。条件を1つ落としても、たいてい動いてしまいます。
DB の制約やポリシーなら、どこから来たクエリにも必ず効きます。

なので、絞り込みは2箇所に置くことになりました。

やっていること
DB(RLS) 行レベルで、他人のデータを返さない
アプリ クエリに .eq('user_id', ...) を書いて、自分の分だけ取る

設計としては、これで正しいつもりでした。
むしろ丁寧なほうだと思っていました。

同じ穴が、片方の画面では出て、片方では出ませんでした

ところが DB 側は、最初から効いていませんでした。

シリーズ2本目に書いたとおり、ポリシーが OR で結合されていて、
ログイン中は他人の公開カードまで読める状態がずっと続いていたのです。

面白かったのは、画面によって出方が違ったことでした。

管理画面には出ました。
ここは方針どおり .eq を書かず、絞り込みを RLS に任せていたからです。
DB が守っていなかったので、そのまま他人のカードが並びました。

公開ページには出ませんでした。
こちらは URL のユーザー名から .eq('user_id', profile.id) で絞っています。
RLS が余計な行を返していても、アプリ側が捨てていました。

同じ穴です。同じテーブル、同じポリシー、同じ日。
それでも片方の画面は正常に見えていました。

守っている層が、壊れている層を隠します

もし管理画面のほうにも .eq を書いていたら、どうなっていたか。

両方の画面が正しく表示されて、穴は見つかりませんでした。

そのまま公開まで進んでいた可能性が高いと思います。
「DB でも守っているから安全」という認識だけを持ったまま、
実際には1枚で走っている状態が続いていたはずです。

ここが二重防御の裏側でした。

層を増やすと、片方が壊れても表に出ません。
出ないので直せない。直らないまま、残った1枚に全部を預けることになります。

そして厄介なのは、手応えとしては安心が増えることです。
守りを足したので、感覚としては前より堅くなっています。

片方を外して確かめる手段が要りました

今回それを担ったのは、動作確認用の SQL でした。

begin;
  set local role authenticated;
  set local "request.jwt.claims" = '{"sub":"a12690b6-…"}';

  select count(*) filter (where user_id <> 'a12690b6-…') as 他人の行数
  from public.items;   -- 期待: 0
rollback;

ロールを切り替えて、DB に直接投げています。
アプリを経由しないので、アプリの絞り込みが無い世界を再現できました。

アプリ側のコードを一時的に消して確かめる、という方法もあります。
ただ、それは面倒ですし、消したまま忘れる事故のほうが怖い。

DB 単体で叩ける検証を置いておくと、層を1枚ずつ剥がして見られます。
あってよかったと、はっきり思いました。

おわりに

「アプリを信用せず DB で守る」の裏側に、こういう面がありました。

アプリでも守ると、DB が守っていないことが見えなくなる。

多層防御をやめる、という話ではありません。
今回それが無ければ、公開ページのほうから他人のデータが漏れていました。

そうではなくて、層を増やしたら、層ごとに確かめる手段も増やすという話だと思っています。
2枚重ねたなら、2枚重ねた状態で見るだけでは足りません。

このシリーズでは、RLS でつまずいた4つを順番に書いてきました。

権限は GRANT と RLS の二段構えで、ポリシーだけでは1行も読めませんでした。
ポリシーは足すほど通り道が増えて、他人のカードが見えました。
検証が通ったのは、そのとき他人のデータが存在しなかったからでした。
そして4日間気づけなかったのは、もう1枚が正しく動いていたからでした。

4つとも、間違っていたのは書いたコードではなく読み方のほうです。
足せば締まる、通ったから正しい、守りは多いほど良い。
どれも自然に感じる読み方で、どれも今回は逆でした。

自分の直感がどこで裏返るかは、たぶん踏まないと分かりません。
そのぶん、踏んだ場所はよく覚えています。

目次