From 097952b5f9cbefe3c1dd0606da3d6d8e66e440c2 Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Tue, 29 Sep 2026 05:25:03 +0000 Subject: [PATCH] Cloud container: the production PocketBase's superuser and _system collections are closed on /api-prod too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /pb already refused them to anyone who is not an admin. /api-prod, the production PocketBase's public API, passed every /api/ path, so its superuser sign-in was reachable from the public side: no data was exposed, but it was a place to guess the production admin's password. Now any /api-prod/api/collections/_… path is refused (403), checked on the decoded path like the /pb gate. No public client calls these; the one caller in the repo (pb-oidc.mjs) talks to PocketBase directly. Found by dx-base's independent review of the public Studio (V2). --- deployment/cloud/README.md | 2 +- deployment/cloud/admin.mjs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/deployment/cloud/README.md b/deployment/cloud/README.md index 002eec2d5..26c53713a 100644 --- a/deployment/cloud/README.md +++ b/deployment/cloud/README.md @@ -92,7 +92,7 @@ With the sign-in on, the container also runs PocketBase and can publish a produc |---|---|---| | `/` | signed-in | the dev server (Plugin Studio, hot reload) | | `/pb/` | signed-in (its `/_/` dashboard: `OIDC_ADMIN_GROUPS` only). With `PB_OIDC_CLIENT_SECRET`, its `/api/` is public and PocketBase signs people in itself (see below); `_superusers` and system collections stay admin-only | PocketBase **dev**, the team's shared backend | -| `/api-prod/api/` | **public** | PocketBase **prod**'s API; its dashboard is never exposed | +| `/api-prod/api/` | **public** | PocketBase **prod**'s API; its dashboard, its superuser and its `_` system collections are never exposed | | `/cdn/dx-maps/` | **public** | the last published build: `manifest.json` at a stable URL, CORS open for embeds | | `POST /admin/build` | `OIDC_ADMIN_GROUPS` only | build and publish the pulled commit | | `GET /admin/status` | signed-in | the published and pulled commits, and whether a build is running | diff --git a/deployment/cloud/admin.mjs b/deployment/cloud/admin.mjs index 1f5064b7a..204bfeed9 100644 --- a/deployment/cloud/admin.mjs +++ b/deployment/cloud/admin.mjs @@ -1,6 +1,6 @@ // admin.mjs — the container's internal router (127.0.0.1:5175), behind oauth2-proxy. // /cdn/* the last good build (/data/cdn/current), PUBLIC (oauth2-proxy skips auth here) -// /api-prod/* PocketBase PROD, PUBLIC; only /api/ passes, never the /_/ dashboard +// /api-prod/* PocketBase PROD, PUBLIC; only /api/ passes, never the /_/ dashboard, _superusers or _system collections // /pb/* PocketBase DEV, signed-in; its /_/ dashboard only for OIDC_ADMIN_GROUPS. With PB_OIDC_CLIENT_SECRET, // its /api/ is PUBLIC (PocketBase signs people in itself), except _superusers and _system collections // /admin/build POST: build PRODUCTION_BRANCH now (OIDC_ADMIN_GROUPS only); /admin/status GET. @@ -178,6 +178,8 @@ Bun.serve({ const bp = backendPath(p) if (bp === null) return text('bad path', 400) if (!p.startsWith('/api-prod/api/') || !bp.startsWith('/api-prod/api/')) return text('only /api/ is public', 403) + // As on /pb: the superuser and every _system collection stay closed. No public client uses them (dx-base, V2 review). + if (bp.startsWith('/api-prod/api/collections/_')) return text('for admins only', 403) return proxy(req, url, '/api-prod/', 8091) } if (p.startsWith('/pb/')) {