Backup routes are not admin-gated - cloud credentials readable by any authenticated user #80

Closed
opened 2026-07-15 19:49:54 +00:00 by claude-bot · 1 comment
Contributor

Severity: High · Confidence: High · Effort: XS · Category: security

Evidence

  • api/src/app.ts:123-135 - /api/settings and /api/admin are wrapped in requireAdmin; /api/backup at line 124 is behind only requireAuth.
  • api/src/routes/backup.ts - no internal admin check anywhere in the router.
  • api/src/routes/backup.ts:80-88 - GET /destinations returns extra_env.
  • api/src/services/backup.ts:47-59 - extra_env holds cloud secrets (AWS_*/B2_*/AZURE_*/.); only restic_password is excluded.
  • api/src/routes/invites.ts:77-82, api/src/routes/projects.ts:407-435 - local collaborators accept invites and become full authenticated non-admin users.

Problem
Every other server-infra surface (settings, admin) is admin-gated. The backup router - which manages restic repos/credentials, spawns pg_dump/restic, and can stage/apply DB restores - sits behind only requireAuth, and its destinations list returns long-lived cloud-storage secret keys.

Impact
Any logged-in non-admin (e.g. a project collaborator) can read cloud-storage secret keys, create/delete/trigger backup destinations, run restic against repos, and stage/apply restores. No exploit path in a strict single-account instance, but the app genuinely provisions non-admin accounts.

Fix
Mount as app.use('/api/backup', requireAdmin, backupRouter) and drop extra_env values from the list response (return key names / a "configured" boolean).

Acceptance criteria

  • A non-admin session receives 403 from every /api/backup/* route.
  • GET /api/backup/destinations no longer returns extra_env secret values.
  • A test asserts non-admin → 403 on /api/backup/jobs and /destinations.

Filed from the 2026-07-15 codebase audit. Full report: docs/.internal/report-2026-07-15.md (gitignored).

**Severity:** High · **Confidence:** High · **Effort:** XS · Category: security **Evidence** - `api/src/app.ts:123-135` - `/api/settings` and `/api/admin` are wrapped in `requireAdmin`; `/api/backup` at line 124 is behind only `requireAuth`. - `api/src/routes/backup.ts` - no internal admin check anywhere in the router. - `api/src/routes/backup.ts:80-88` - `GET /destinations` returns `extra_env`. - `api/src/services/backup.ts:47-59` - `extra_env` holds cloud secrets (`AWS_*`/`B2_*`/`AZURE_*`/.); only `restic_password` is excluded. - `api/src/routes/invites.ts:77-82`, `api/src/routes/projects.ts:407-435` - local collaborators accept invites and become full authenticated non-admin users. **Problem** Every other server-infra surface (settings, admin) is admin-gated. The backup router - which manages restic repos/credentials, spawns `pg_dump`/`restic`, and can stage/apply DB restores - sits behind only `requireAuth`, and its destinations list returns long-lived cloud-storage secret keys. **Impact** Any logged-in non-admin (e.g. a project collaborator) can read cloud-storage secret keys, create/delete/trigger backup destinations, run restic against repos, and stage/apply restores. No exploit path in a strict single-account instance, but the app genuinely provisions non-admin accounts. **Fix** Mount as `app.use('/api/backup', requireAdmin, backupRouter)` and drop `extra_env` values from the list response (return key names / a "configured" boolean). **Acceptance criteria** - [ ] A non-admin session receives 403 from every `/api/backup/*` route. - [ ] `GET /api/backup/destinations` no longer returns `extra_env` secret values. - [ ] A test asserts non-admin → 403 on `/api/backup/jobs` and `/destinations`. --- _Filed from the 2026-07-15 codebase audit. Full report: `docs/.internal/report-2026-07-15.md` (gitignored)._
Author
Contributor

Fixed in 9a87882 (v7.1.0 wave 1).

  • Admin gate: /api/backup is now mounted app.use('/api/backup', requireAdmin, backupRouter) in api/src/app.ts — same server-infra tier as settings/admin. Every /api/backup/* route returns 403 to a non-admin.
  • Secret redaction: GET /api/backup/destinations no longer returns extra_env values. It maps each row to extra_env_keys (key names only); restic_password was already excluded. Editing is write-only — the frontend omits extra_env from a PATCH unless the admin re-enters JSON, so existing secrets are preserved (mirrors the existing restic-password "leave blank to keep" pattern). BackupPanel.tsx updated accordingly (shows configured key names as a hint).
  • Test: api/src/test/integration/backup.test.ts asserts non-admin → 403 on /jobs and /destinations, and that a created destination's secret values (extra_env) are never serialized (only extra_env_keys exposed).

All three acceptance criteria met. CI #3640 green; deployed to dev.

Fixed in `9a87882` (v7.1.0 wave 1). - **Admin gate:** `/api/backup` is now mounted `app.use('/api/backup', requireAdmin, backupRouter)` in `api/src/app.ts` — same server-infra tier as settings/admin. Every `/api/backup/*` route returns 403 to a non-admin. - **Secret redaction:** `GET /api/backup/destinations` no longer returns `extra_env` values. It maps each row to `extra_env_keys` (key names only); `restic_password` was already excluded. Editing is write-only — the frontend omits `extra_env` from a PATCH unless the admin re-enters JSON, so existing secrets are preserved (mirrors the existing restic-password "leave blank to keep" pattern). `BackupPanel.tsx` updated accordingly (shows configured key names as a hint). - **Test:** `api/src/test/integration/backup.test.ts` asserts non-admin → 403 on `/jobs` and `/destinations`, and that a created destination's secret values (`extra_env`) are never serialized (only `extra_env_keys` exposed). All three acceptance criteria met. CI #3640 green; deployed to dev.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/TeaLeaves#80
No description provided.