Skip to content

Keep a purged document id gone after a PUT or a reconnect #407

Description

@HMarzban

Summary

After a document is purged, DocumentPurgeTombstone should stop it from coming back. But PUT /api/documents/:docId creates a metadata row for any id and never reads the tombstone. Once a row exists, the WebSocket gate also skips the tombstone. A stale browser tab that was offline during the purge can then reconnect and upload its local copy, so the erased content is stored again. No attacker is needed; a stale tab's title edit is enough.

  • Severity: Low
  • Area: apps/hocuspocus.server
  • Source: security review of 2026-10-06

Where

  • Route: apps/hocuspocus.server/src/api/routers/documents.router.ts:57-62 (optionalUser, so anonymous is allowed). No :docId param schema.
  • updateDocument checks only deletedAt (apps/hocuspocus.server/src/api/services/documents.service.ts:545-549), then upserts any id (:603-615).
  • WebSocket gate reads the tombstone only when no row exists: apps/hocuspocus.server/src/hocuspocus.server.ts:553-559.
  • What the tombstone is for: apps/hocuspocus.server/src/api/services/documentPurge.service.ts:86-90.

Fix plan

  1. In updateDocument, when no row exists, look up DocumentPurgeTombstone for the id. If found, return 404 and create nothing.
  2. In the WebSocket onAuthenticate, check the tombstone even when a metadata row exists, and refuse the connection.
  3. Add a documentIdSchema param validator to the documents routes that take :docId or :documentId (update, delete, restore, duplicate). This also helps the local-storage path issue.
  4. Before you enforce the id format, count existing rows that do not match it, so you do not lock out real documents.

Acceptance criteria

  • After a purge, PUT /api/documents/<purgedId> returns 404 and creates no row.
  • After a purge, a WebSocket connection to that room is refused, even if a row was created earlier.
  • Creating and updating normal documents still works, for both signed-in and signed-out users.

Verify

Add a test under apps/hocuspocus.server/src/api/services/__tests__/ that purges a document, then calls updateDocument with the same id.


Generated by Claude Code

Activity

  1. added theissue type on Oct 6, 2026
  2. added a commit that references this issue on Oct 6, 2026
    02f7530
  3. HMarzban commented on Oct 6, 2026

    @HMarzban
    CollaboratorAuthor

    Fixed in 02f7530: updateDocument returns 404 for a purged id with no row, and the WebSocket gate reads the tombstone even when a row exists. A regression case was added to tests/integration/documents.test.ts.

    Not done on purpose: the documentIdSchema param validator. Production hocuspocus rows were not checked, so older ids that do not match the format cannot be ruled out. Enforcing it could lock out real documents. The tombstone check closes this issue on its own.


    Generated by Claude Code

  4. changed the title [-][Security] PUT /api/documents/:docId can bring back a purged document id and bypass the purge tombstone[/-] [+]Keep a purged document id gone after a PUT or a reconnect[/+] on Oct 6, 2026
  5. HMarzban commented on Oct 6, 2026

    @HMarzban
    CollaboratorAuthor

    Two facts the comment above does not record:

    • Only the PUT path has a test. The WebSocket gate change (acceptance criterion 2) has no automated test. Check it by hand after deploy: purge a test document, then reconnect a tab that was offline during the purge. The server must refuse the connection.
    • The WebSocket gate now reads the metadata row and the tombstone in parallel on every connection. Before, it read the tombstone only when no row existed.

    Verify the PUT case: cd apps/hocuspocus.server && bun test tests/integration/documents.test.ts.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Offline-ModeSecuritySecurity, access control, and data exposurebugSomething isn't working

    Type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions