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
You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Realtime permission downgrade (WRITE → READ) is not enforced: a downgraded collaborator keeps editing through the live connection and the edits are persisted #6949
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 realRealtimeConnection 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