Files
picpeak/backend
Paul Nothaft 03087c798c fix(security): close GHSA-g94x (cross-gallery photo read) + GHSA-pv6w (admin DB export) (#924)
* fix(security): close two access-control advisories (GHSA-g94x, GHSA-pv6w)

GHSA-g94x-8vv8-3c9f (HIGH) — the secure-image VIEW route
(/secure-images/:slug/secure/:photoId/:token) validated only the token
signature and took the gallery/photo from the URL, so a token minted on
any PUBLIC gallery read every other gallery's photos with no password
(its download sibling has verifyGalleryAccess; the view route can't —
it serves via <img src> with no header). Bind the token to its scope
instead: the URL photoId must equal the token's minted photoId (photos
belong to exactly one gallery, and minting is gallery-scoped), and the
gallery embedded in the token's sessionId must equal the URL gallery.

GHSA-pv6w-rj34-wj9v (MEDIUM) — GET /admin/backup/picpeak/export dumps
every table unredacted (bcrypt hashes, 2FA, SMTP/SSO/WhatsApp/webhook/S3
secrets) and was gated only by backup.create, which the built-in admin
role holds. Gate it behind super_admin, matching the restore side
(backup.restore, already admin-denied) and the masked config APIs.

Regression tests pin both: cross-gallery token reads 403 (photo and
gallery checks), backup export 403 for admin / passes for super_admin.

* test: stub requireSuperAdmin in the backup masking mock

adminBackup now calls requireSuperAdmin() at load (GHSA-pv6w export
gate), and backupSecretMasking mocks the permissions module — add the
new function to the mock so the module loads.

* fix(security): review follow-ups on the export gate (GHSA-pv6w)

- test: place the mocked export in its own mkdtemp dir. The route
  recursively deletes path.dirname(filePath) after download, so a stub
  in bare os.tmpdir() made the super_admin test wipe the whole temp
  root — other jest workers' DB files included (latent CI flake).
- ui: hide PicpeakExportCard from non-super_admins. The role keeps
  settings.view + backup.create, so after the gate its Download button
  always 403'd with a generic toast; gate the card on role super_admin
  to match the endpoint.

* fix(security): keep the token-mismatch audit values within varchar(20) (GHSA-g94x review)

image_access_logs.access_type is varchar(20) (migration 038), but
'token_gallery_mismatch' is 22 chars — on Postgres the audit write
threw value-too-long and logImageAccess swallowed it, so the security
event went unrecorded (the 403 still fired; log is best-effort). Shorten
to 'photo_mismatch' / 'gallery_mismatch' (14/16).

---------

Co-authored-by: Paul Nothaft <paul@MacStudio-von-Paul.local>
2026-07-30 14:24:28 +02:00
..