認証の境界を1か所に決める (#472) - #479
Conversation
`/api/events/*` の認証は `routes/events.ts` の `use("*", requireAuth)` だけが
持つ。`worker.ts` で `api.route("/events", eventRoutes)` が `/events` への登録の
1本目なので、後ろに並べた配下ルートは全部これを通る。にもかかわらず配下の
サブアプリ22本が各自 `use("*", requireAuth)` を持っていた。
Hono のミドルウェアはパターン一致で積まれるため、これは「2回」では済まない。
`/api/events/*` に `use("*")` を持つサブアプリを並べると、どのサブアプリの
ハンドラを叩いても並べた全部の requireAuth が順に走る。最も後ろに並ぶ
`/api/events/:id/staff-invites` では 23回、requireAuth 1回あたりセッションと
ユーザーで2クエリなので、1リクエストで D1 を46回引いていた。
22本の重複を落とし、登録済み533本を実際のルーターに引かせて、認証の有無が
1本も変わらないことを確認した(requireAuth の実行回数は延べ 2893 → 448、
認証必須の448本すべてがちょうど1回)。
戻りを防ぐため test/auth-boundary.test.ts を追加した。登録済みルートを1本ずつ
歩いて、requireAuth がちょうど1回であること・未認証で通る経路が表のとおりで
あることを見張る。境界の削除・境界の外への新ルート・worker.ts の並び替え・
重複の復活の4つの変異でそれぞれ落ちることを確認済み。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SLkuubF8cHzSnnsv4fN9DA
## OPEN_ROUTES はこの検査の唯一の抜け道
境界の外へ経路を1本出しても、その鍵を表に足せば検査は通る(サブアプリ丸ごとは
全パスぶん足すことになるので通らないが、1本なら通る)。守りが「表への追加を
レビューが認可の変更として読むこと」に載っているので、それを前口上に命令形で
書いた。「テストを通すために足す」は禁止、と明記している。
事故で足せないよう EXPECTED_OPEN_COUNT も突き合わせる。表を1行足すと必ずこの数も
動かすことになるので、こっそり1本開ける差分が必ずレビューに現れる。
表そのものも用途ごとの塊に並べ替えて、足すときにどの塊かを選ばせる。
## 歩けない2つの形を塞ぐ
これまで ALL メソッドとワイルドカードを含むパスを一律で飛ばしていた。今は
`api.all("/x", h)` のような ALL の終端ハンドラも歩く(ワイルドカードでなければ
代表パスを作れる)。これで5本(broadcasts・todos・staffing・name-cards・
timetable/editing の requireEventRole)も検査対象に入り、歩く数は 533 → 538。
ワイルドカードのパスは代表パスを作れないので依然歩けないが、そこに入るのは
ミドルウェア登録で、ミドルウェアは終端ではないため穴にならない。穴になるのは
ワイルドカードに終端ハンドラを載せた場合だけなので、next を受け取るか
(引数の数)で見分けて、終端ハンドラが worker.ts 末尾の ASSETS フォールバック
1本だけであることを固定した。
## 変異
新しく3つ確認した(いずれも内容ハッシュで当たりと復元を検証、使い捨てコピーで実行):
- `api.all("/leak-all", h)` を境界の外に追加 → 落ちた(旧実装では見えなかった)
- `api.get("/leak-wild/*", h)` を境界の外に追加 → 落ちた(同上)
- 表にだけ1行足して件数は据え置き → 落ちた
既存の4つ(境界の削除・境界の外への新ルート・worker.ts の並び替え・重複の復活)
も引き続き落ちることを再確認した。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SLkuubF8cHzSnnsv4fN9DA
|
レビューの2点に対応した( 1. 表への追加は認可の変更である、と命令形で書いた。 前口上に「 2. 歩けない2つの形を塞いだ。 ワイルドカードのパスは代表パスを作れず依然歩けないが、そこに入るのはミドルウェア登録で、ミドルウェアは終端ではないので穴にならない。穴になるのはワイルドカードに終端ハンドラを載せた場合だけなので、 追加の変異(いずれも内容ハッシュで当たり/復元を検証、使い捨てコピーで実行)
既存の4つ(境界の削除・境界の外への新ルート・ 検証
|
Fixes #472
境界の規則
/api/events/*の認証はroutes/events.tsのuse("*", requireAuth)だけが持つ。worker.tsでapi.route("/events", eventRoutes)が/eventsへの登録の1本目なので、後ろに並べた配下ルート(scoring・photos・chat …22本)は必ずこれを通る。配下のサブアプリは自前のrequireAuthを持たない。認証なしで通したい経路は
api.route("/events", eventRoutes)より前に登録する(既存の公開画像・公開コメント一覧などと同じ形)。/api/events以外の接頭辞(/api/me・/api/communitiesなど)は、従来どおり各ルートファイルが自分の境界を持つ。なぜ「二重」では済まなかったか
Hono のミドルウェアはパターン一致で積まれる。
/api/events/*にuse("*")を持つサブアプリを22本並べると、どのサブアプリのハンドラを叩いても、並べた全部の requireAuth が順に走る。最も後ろに並ぶ/api/events/:id/staff-invitesでは23回。requireAuth 1回につきセッションとユーザーで2クエリなので、1リクエストで D1 を46回引いていた。GET /api/events/:id/staff-invites(最悪)GET /api/events/:id/todosGET /api/events/:id/bingoGET /api/events・/api/meなどルート表の差分
app.routesを歩き、各ルートのパスを実際のルーター(app.router.match)に引かせて、そのハンドラに届くまでに通るrequireAuthを数えた。読んだだけの推測ではなく、Hono 自身の解決結果。AUTH/OPEN分類が前後で完全一致)authN=1変異テスト
test/auth-boundary.test.tsが本当に効くことを、共有ツリーとは別の使い捨てコピーで確認した。各変異はファイルの内容ハッシュで「当たったこと」と「戻ったこと」を検証している。events.tsのuse("*", requireAuth)を削除9fa1bd4ea864→ffe7d6d3b50e→9fa1bd4ea864api.get("/leak")を追加a0398b7ec7de→183fad3751dd→a0398b7ec7deworker.tsでscoringRoutesをeventRoutesより前へa0398b7ec7de→feee55fb7caa→a0398b7ec7deeventBingo.tsでrequireAuthを重ねる2ddef3d0d160→114267aae568→2ddef3d0d160未認証で通る85本は表に列挙してあり、認証の要らない経路を足すときは表にも足さないと落ちる。逆に、認証必須のはずの経路を境界の外に生やしても落ちる。
検証
pnpm -r typecheck通過worker.ts653行)docs
docs/design.md§3.1 に「配下は自前の requireAuth を持たない」根拠と実測、テストが見張る範囲を追記。docs/staff-todo.md§6.1・docs/staff-roles.md§6.1 のコード片も実装後の姿に合わせた。🤖 Generated with Claude Code
https://claude.ai/code/session_01SLkuubF8cHzSnnsv4fN9DA