kanban-dev-webapp/pocketbase/pb_migrations/1789601000_locked_user_rules.js
Dave 2a8d6f2505 fix(security): lock the users collection against privilege escalation
The users collection kept PocketBase's defaults because the Plan 1a rules
migration covered eight collections and missed this one:

  createRule ""                      -> anyone could sign up
  updateRule "id = @request.auth.id"  -> anyone could edit their own record

`role` is an ordinary field here, so both were escalations. Verified
exploitable against the live instance: an anonymous request created an
account with role "owner", and a client manager promoted themselves to
owner by patching their own record. Either route yields read and write
access to every client's board.

Create, update and delete are now superuser-only; list and view are
owner-or-self. Adds four integration tests so a regression fails loudly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 11:18:13 +02:00

53 lines
2.1 KiB
JavaScript

/// <reference path="../pb_data/types.d.ts" />
/**
* Close the users-collection privilege escalation.
*
* REFERENCE ONLY — see pb_migrations/README.md. What actually runs is
* scripts/setup-remote.ts. This file exists so the schema stays readable in
* one place.
*
* 1789600900_applied_api_rules.js applied rules to eight collections and
* missed this one, so `users` kept PocketBase's defaults:
*
* createRule "" -> anyone may sign up
* updateRule "id = @request.auth.id" -> anyone may edit their own record
*
* `role` is an ordinary field on this collection, so each default was a
* privilege escalation. Both were verified exploitable against the live
* instance: an anonymous POST created an account with role "owner", and a
* client manager promoted themselves to owner by patching their own record.
* A promoted account reads every client's board through the normal rules.
*
* Accounts are created and edited only by the owner, through server-side code
* running as a superuser, so all three write rules are superuser-only. Any
* future self-service profile editing must go through a route handler with an
* explicit field whitelist — the same reason /api/client/* exists.
*/
migrate(
(app) => {
const users = app.findCollectionByNameOrId("users");
const ownerOrSelf =
'@request.auth.id != "" && (@request.auth.role = "owner" || id = @request.auth.id)';
users.listRule = ownerOrSelf;
users.viewRule = ownerOrSelf;
users.createRule = null;
users.updateRule = null;
users.deleteRule = null;
return app.save(users);
},
(app) => {
// Deliberately does NOT restore PocketBase's defaults. Rolling back to
// public signup would reopen the escalation, and no rollback is worth
// that. Reset by hand in the admin UI if you truly mean to.
const users = app.findCollectionByNameOrId("users");
users.listRule = "id = @request.auth.id";
users.viewRule = "id = @request.auth.id";
users.createRule = null;
users.updateRule = null;
users.deleteRule = null;
return app.save(users);
}
);