Repository navigation
Serve editor media of a Private document only to its owner #439
Description
Summary
Decision needed first: the maintainer must choose how a media request proves who asks. Nobody should write code before that choice. The smallest option found is in the fix plan below.
Editor media of a Private document stays readable to anyone who once saw its URL. The media route cannot apply resolvePrivateAccess, because an image request has no identity. The editor loads media with a plain src attribute, so the request carries no token. The Supabase session cookie is host-only on docs.plus and never reaches prodback.docs.plus.
- Severity: Low
- Area:
apps/hocuspocus.servermedia route, and the webapp editor that loads media - Source: security review of 2026-10-06, finding L3 (Private part, split from Stop serving editor media after a document moves to Trash #408)
Where
- Route, public by design:
apps/hocuspocus.server/src/api/routers/hypermultimedia.router.ts:30-35. getMediacalls the store with no metadata, privacy or Trash check:apps/hocuspocus.server/src/api/controllers/documents.controller.ts:410-421andapps/hocuspocus.server/src/api/services/media.service.ts:14-15. Upload does check (documents.controller.ts:431-451).- Responses carry
Cache-Control: public, max-age=31536000, immutable:apps/hocuspocus.server/src/lib/storage/storage.s3.ts:116andapps/hocuspocus.server/src/lib/storage/storage.local.ts:80. - The editor stores
${NEXT_PUBLIC_RESTAPI_URL}/plugins/hypermultimedia/<documentId>/<file>and renders it as a plainsrc:apps/webapp/src/components/TipTap/mediaPopovers/uploadMediaFile.ts:195. - Duplicate copies every object the snapshot names, under any prefix:
apps/hocuspocus.server/src/api/services/documents.service.ts:851-861,media.service.ts:39-68.
Fix plan
Decision (maintainer)
Pick the credential before coding. The smallest option found:
- The webapp calls a new
POST /api/plugins/hypermultimedia/:documentId/sessionwith itsAuthorizationheader when it opens a Private document. - REST runs
resolvePrivateAccessand sets a short-lived, signed,HttpOnly,Secure,SameSite=Laxcookie withPath=/api/plugins/hypermultimedia/<documentId>. The two hosts are same-site, so image requests then carry it. CORS already allows credentials. getMediaserves a Private document's media only with a valid cookie for that document, and sendsCache-Control: private, no-cache.- No change to the published
@docs.plus/extension-hypermultimedianode views.
Duplicate
#408 step 3 drops references whose prefix document is trashed. Extend that filter here. Also drop references whose prefix document is Private and not owned by the requester (resolvePrivateAccess).
Layer: the gate belongs in the REST media controller (getMedia), next to the upload gate. Do not put access logic in lib/storage/*; those adapters only move bytes. Keep the media-read rate-limit bucket as it is.
Out of scope
- Trash and raw object reads. Stop serving editor media after a document moves to Trash #408 owns them.
- Revoking copies already in a browser cache.
immutablecopies stay until they expire. - Media inside chat. Chat media lives in the Supabase
mediabucket (see Let non-members read only chat media that a live message uses #432).
Acceptance criteria
- After a Private flip, a non-owner's request for a known media URL returns
403or404. - The owner still sees every image in their Private document after a reload.
Verify
- Local stack: as the owner, flip a document with an image to Private, then reload. Every image must load.
- Request the same media URL as another account, or with
curl -iand no cookie. Expect403or404.
Related
- Stop serving editor media after a document moves to Trash #408 (Trash and direct object reads, ships first)
- Make a Private document's chat readable and writable only by its owner #396 (Private document chat). Settle this credential together with the Make a Private document's chat readable and writable only by its owner #396 design.
- Let non-members read only chat media that a live message uses #432 (media bucket listing)
Generated by Claude Code
Activity
docs(repo): record the 9 Oct security and email batch in the changelo…
HMarzban commentedon Oct 9, 2026
Built but held, not in production. The media gate fails closed when
MEDIA_COOKIE_SECRETis unset, so every owner's Private images would 404 if that env line were missed. Maintainer decision owed: set the secret first and ship as built, or approve the gate being off while the secret is unset. The cookie TTL is already 1 hour in the patch.