diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 3bf1518..5cc0f17 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -107,16 +107,23 @@ Consequences that were not true when the app was designed: | `GET /api/collections/bookcases/records` | **403** | | `POST /api/collections/users/records` (self-registration) | **403** | | `GET /api/health` | 200 (intended — it is a health check and leaks nothing) | -| `GET /api/collections/users/records` | **200 `{"items":[]}`** — see below | +| `GET /api/collections/users/records` | **403** (was 200 `{"items":[]}` — fixed same day, below) | -**Known gap, not a leak:** `users` LIST answers **200 with an empty array** instead of -403. No data escapes — there are two real accounts in the DB and an anonymous caller -sees neither, because `listRule` filters the rows out — but the status code is wrong -and it is the exact PocketBase quirk `pb_hooks/main.pb.js` exists to paper over. That -hook lists only `bookcases`, `shelves`, `books`; `users` was never added to it. One -word of a fix, and worth doing now that the collection is world-reachable: some -clients read "200 with []" as an allowed request. **Not yet done — do not record it as -done until the curl above returns 403.** +**The `users` gap, found and closed on 2026-09-12.** `users` LIST was answering **200 +with an empty array** instead of 403. No data escaped — there are two real accounts in +the DB and an anonymous caller saw neither, because `listRule` filters the rows out — +but the status code was wrong, and it was the exact PocketBase quirk +`pb_hooks/main.pb.js` exists to paper over. That hook listed only `bookcases`, +`shelves`, `books`; `users` was never added, which was defensible while the server was +localhost-only and is not now that the collection is world-reachable. + +Fixed by adding `"users"` to the hook's collection list and restarting the service. +Re-verified over the internet **after** the restart: all four collections 403 anonymous, +self-registration 403, health 200. Login was re-verified too, because this hook runs on +a collection the app authenticates against: `POST /api/collections/users/auth-with-password` +still returns 200 with a token, and an authenticated LIST of all four collections still +returns 200. The hook only rejects UNAUTHENTICATED list/search, and auth-with-password +is not a list request. ## STATE: what is DONE ### Wave 1A — server: COMPLETE and verified by the orchestrator (not just self-reported) diff --git a/server/README.md b/server/README.md index a5c3b02..97b7792 100644 --- a/server/README.md +++ b/server/README.md @@ -90,12 +90,18 @@ A `200` with a non-empty `items` array from any of those is a breach, and a `200` with an empty one means the `pb_hooks` guard is not loaded — see the quirk described above. -> **Known gap:** `GET /api/collections/users/records` currently answers -> **200 with an empty array** rather than 403. Nothing is disclosed — the -> `listRule` filters every row out, so an anonymous caller sees no accounts, -> no emails, no ids — but it is the same wrong-signal quirk as above, and -> `pb_hooks/main.pb.js` does not yet cover the `users` collection. Adding it -> to that hook's collection list closes it. +Check the accounts collection too — it is covered by the same hook: + +```sh +curl -s -o /dev/null -w '%{http_code}\n' "$BASE/api/collections/users/records" +# -> 403 + +# ...but logging in must still work, which is the thing to re-check after any +# change to pb_hooks (the hook rejects anonymous LIST, not authentication): +curl -s -o /dev/null -w '%{http_code}\n' -X POST "$BASE/api/collections/users/auth-with-password" \ + -H 'Content-Type: application/json' -d '{"identity":"you@example.com","password":"..."}' +# -> 200 +``` ## Where the live server runs diff --git a/server/pb_hooks/main.pb.js b/server/pb_hooks/main.pb.js index a678ea2..e76410f 100644 --- a/server/pb_hooks/main.pb.js +++ b/server/pb_hooks/main.pb.js @@ -10,12 +10,24 @@ // ever get a 200 back from these endpoints, even an empty one, since some // HTTP/JS clients treat "200 with []" as a successful, allowed request. // This hook makes that explicit: anonymous list/search requests against the -// three app collections are rejected with 403, matching create/update/ +// app collections are rejected with 403, matching create/update/ // view/delete (which already 400/404 for unauthenticated callers via the // declarative rules alone). +// +// "users" is in this list as of 2026-09-12. It was omitted originally, when +// the server was only reachable on localhost; the server is now exposed to +// the internet, so an anonymous GET of /api/collections/users/records was +// answering 200 with an empty array. Nothing leaked — the declarative +// listRule filters every row out, so no account, email or id was ever +// visible — but it is the same wrong signal this hook exists to remove, and +// the accounts collection is the last place to leave it. +// +// This blocks only UNAUTHENTICATED list/search. Logging in is unaffected: +// auth-with-password is not a list request, and it is the only users +// endpoint the app calls at all. onRecordsListRequest((e) => { if (!e.auth) { throw new ForbiddenError("Authentication required."); } e.next(); -}, "bookcases", "shelves", "books"); +}, "bookcases", "shelves", "books", "users");