8000 Realtime permission downgrade (WRITE → READ) is not enforced: a downgraded collaborator keeps editing through the live connection and the edits are persisted · Issue #6949 · hedgedoc/hedgedoc · GitHub
[go: up one dir, main page]

Skip to content

Realtime permission downgrade (WRITE → READ) is not enforced: a downgraded collaborator keeps editing through the live connection and the edits are persisted #6949

Open

Description

@leograntquinn-crypto

Originally reported as security advisory GHSA-52fm-7j73-662f; re-filing as a regular issue as requested there (per the security policy for the 2.0 development branch).

Affected: HedgeDoc 2.0 development codebase, 2.0.0-alpha.3 / commit c937fd2 (the realtime stack involved does not exist in the 1.x codebase).

Summary

When a note owner downgrades a collaborator's permission from WRITE to READ while that collaborator is connected to the note's realtime session, the server keeps accepting and applying the collaborator's edits for the entire lifetime of the existing connection. The edits are merged into the shared document and are written to the database by the periodic persistence mechanism. Only a full downgrade to DENY (which disconnects) or a reconnect of the client restores enforcement.

Root cause

In backend/src/realtime/realtime-note/realtime-connection.ts, the constructor declares the TypeScript parameter property public acceptEdits: boolean (line 40). The edit-acceptance callbacks handed to the sync and user-status adapters close over the constructor parameter, not the property:

  • Line 47–51: new YDocSyncServerAdapter(this.transporter, doc, () => acceptEdits)
  • Line 61: the user-status adapter receives the same stale callback.

In commons/src/y-doc-sync/y-doc-sync-server-adapter.ts (lines 20–25), this provider callback is the only gate for incoming edits: applyIncomingUpdatePayload drops an update only when the provider returns false — i.e. it permanently returns the value captured at connect time.

The permission-change path, RealtimeNoteService.updateOrCloseConnection in backend/src/realtime/realtime-note/realtime-note.service.ts (lines 158–176, triggered by PERMISSION_CHANGE at lines 126–136), instead writes the property: connection.acceptEdits = userPermissionLevel > PermissionLevel.READ (line 172). Property and gate never meet, so the downgrade has no effect on the live connection. (The reverse direction is broken too: a READ → WRITE upgrade does not take effect until reconnect, which corroborates that this is an implementation defect rather than intended behavior.)

Verification (dynamic, against the real classes)

Using the project's own jest setup with the real RealtimeConnection and YDocSyncServerAdapter (not the mocked connection used by the existing test utils):

  • Control: a connection constructed with acceptEdits = false correctly drops an incoming Yjs update.
  • Test: a connection constructed with acceptEdits = true applies an update; after performing exactly the assignment the permission-change handler performs (connection.acceptEdits = false), a second update is still applied to the shared server document (final content contains both edits, where the second must have been dropped).

Impact

A collaborator whose write access has been revoked down to read-only retains full write access to the note for as long as their client stays connected — indistinguishable in effect from the permission change never having happened, including persistence to the database.

Suggested fix

Make both callbacks read the property: () => this.acceptEdits. With that change the existing gate in the sync adapter enforces permission changes immediately on live connections.

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      0