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>
53 lines
2.1 KiB
JavaScript
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);
|
|
}
|
|
);
|