Files
bookshelf/server/pb_hooks/main.pb.js
T
Spriteandclaude 0455794603 server: anonymous LIST of users returns 403, not 200 with an empty array
Found while re-verifying the security posture against the public hostname now
that the server is internet-exposed. pb_hooks/main.pb.js covered bookcases,
shelves and books but never users, so an anonymous GET of the accounts
collection answered 200 with an empty array.

Nothing leaked: two real accounts exist and the declarative listRule filtered
both out, so no account, email or id was ever visible to an anonymous caller.
But "200 with []" is the exact wrong signal this hook exists to remove — some
clients read it as an allowed request — and the accounts collection is the last
place to leave it. Defensible while the server was localhost-only; not now.

Re-verified over the internet after restarting the service: books, shelves,
bookcases and users all 403 anonymous, self-registration 403, health 200.

Also re-verified that login still works, since this hook now runs on a
collection the app authenticates against: auth-with-password returns 200 with a
token, and an authenticated LIST of all four collections still returns 200. The
hook rejects unauthenticated list/search only, and auth-with-password is not a
list request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPpdG8VnRfS3KkisR3HUAE
2026-09-12 17:47:43 +00:00

34 lines
1.7 KiB
JavaScript

/// <reference path="../pb_data/types.d.ts" />
// PocketBase's declarative API rules act as row-level filters for the
// "list"/"search" action: an unsatisfiable listRule (e.g. requiring auth)
// still returns 200 with an empty result set rather than an error, because
// the rule is just a SQL WHERE clause under the hood. See
// https://github.com/pocketbase/pocketbase/discussions/6492
//
// Bookshelf is a private, two-person library — no anonymous caller should
// 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
// 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", "users");