CVE-2026-34600
Description
Joplin is an open source note-taking and to-do application that organises notes and lists into notebooks. Versions 3.5.2 and prior contain a logic error in the delta API that allows share recipients to download notes that are no longer shared with them, related to but not fully fixed by the prior patch in #14289. In ChangeModel.delta, when DELTA_INCLUDES_ITEMS is enabled (the default), the latest state of items is attached to delta output without verifying that those items are still shared with the requesting user, and the existing removal logic only filters items deleted for all users. Additionally, the change compression logic incorrectly reduces create - delete to NOOP, which is unsafe because compression is applied per page and an item can have multiple create events; if an earlier create falls on a separate page from a later create -> delete pair, the deletion is dropped and the sequence collapses to a create. As a result, the delta API returns a create event for a deleted item with the full latest content attached, exposing notes the user no longer has access to. This issue has been fixed in version 3.5.3.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Joplin Server 3.5.2 and prior contain a logic error in the delta API that allows share recipients to download notes no longer shared with them.
Vulnerability
Joplin Server versions 3.5.2 and prior contain a logic error in the ChangeModel.delta API that fails to validate whether items returned in delta output are still shared with the requesting user. With DELTA_INCLUDES_ITEMS enabled (the default), the API attaches the latest state of items without checking current access rights, and the removal logic only filters items deleted for all users. Additionally, the change compression logic can incorrectly reduce a sequence of events (e.g., create followed by delete) to a NOOP when events span different pages, causing the deletion to be dropped and exposing deleted notes. This issue was partially addressed in PR #14289 but remained exploitable, with the full fix released in version 3.5.3 [1][2][3].
Exploitation
An attacker who is a legitimate share recipient of a note can exploit this by calling the delta API after the share has been revoked. The attacker does not need elevated privileges beyond normal user access; they only need to sync or interact with the delta endpoint. The logic error means that the API returns a create event for the previously shared note with its full content, even though the attacker should no longer have access. No special network position or authentication bypass is required; the attacker uses their existing valid credentials [1][3].
Impact
Successful exploitation results in unauthorized disclosure of note content that the user no longer has permission to view. The attacker gains access to sensitive information contained in notes that were previously shared but later revoked. The impact is limited to information disclosure; no code execution or data modification is achieved [1].
Mitigation
The issue is fixed in Joplin Server version 3.5.3 [1]. Users should upgrade to v3.5.3 or later immediately. There is no known workaround, as the vulnerability is in the core sync/delta logic. No KEV listing has been published as of the advisory date [1][2].
- Logic error in Joplin Server allows share recipients to read notes they no longer have access to
- Server: Fixes #14110: Fix new clients on an existing account can download previously unshared items by personalizedrefrigerator · Pull Request #14289 · laurent22/joplin
- Sync fuzzer failure: New client on existing account: Can download previously-unshared items
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
147f15b6c3295Server: Fixes #14110: Fix new clients on an existing account can download previously unshared items (#14289)
2 files changed · +30 −12
packages/server/src/models/ChangeModel.test.ts+28 −10 modified@@ -51,16 +51,19 @@ describe('ChangeModel', () => { expect(allUncompressedChanges.length).toBe(8); { - // When we get all the changes, we only get CREATE 2 and CREATE 3. - // We don't get CREATE 1 because item 1 has been deleted. And we - // also don't get any UPDATE event since they've been compressed - // down to the CREATE events. + // When we get all the changes, we get DELETE 1, CREATE 2, and CREATE 3: + // - We don't get CREATE 1 since CREATE 1 -> DELETE 1 was compressed to + // DELETE 1. + // - We don't get any UPDATE event since they've been compressed + // down to the CREATE events. const changes = (await changeModel.delta(user.id)).items; - expect(changes.length).toBe(2); - expect(changes[0].item_id).toBe(item2.id); - expect(changes[0].type).toBe(ChangeType.Create); - expect(changes[1].item_id).toBe(item3.id); + expect(changes.length).toBe(3); + expect(changes[0].item_id).toBe(item1.id); + expect(changes[0].type).toBe(ChangeType.Delete); + expect(changes[1].item_id).toBe(item2.id); expect(changes[1].type).toBe(ChangeType.Create); + expect(changes[2].item_id).toBe(item3.id); + expect(changes[2].type).toBe(ChangeType.Create); } { @@ -355,12 +358,27 @@ describe('ChangeModel', () => { ], }, { - label: 'should remove create -> delete', + label: 'should replace create -> delete with delete', + // Create -> Delete can't be filtered out without un-deleting items. + // This is because compressChanges is often called on an individual **page** + // of results. For example, if the first page of results is, + // - Create: Item 1 + // - ... other changes not involving item 1... + // + // And the second page of results is, + // - Create: Item 1 + // - Delete Item 1 + // + // Then removing the Create -> Delete would result in Item 1 ultimately being + // created, rather than deleted, since there's still a "Create: Item 1" in the + // first page. changes: [ { type: ChangeType.Create }, { type: ChangeType.Delete }, ], - expected: [], + expected: [ + { type: ChangeType.Delete }, + ], }, { label: 'should replace update -> delete with delete',
packages/server/src/models/ChangeModel.ts+2 −2 modified@@ -384,7 +384,7 @@ export default class ChangeModel extends BaseModel<Change> { // know that the item has changed at least once. The reduction is basically: // // create - update => create - // create - delete => NOOP + // create - delete => delete // update - update => update // update - delete => delete // delete - create => create @@ -444,7 +444,7 @@ export default class ChangeModel extends BaseModel<Change> { } if (previous.type === ChangeType.Create && change.type === ChangeType.Delete) { - itemChanges.delete(itemId); + itemChanges.set(itemId, change); } if (previous.type === ChangeType.Update && change.type === ChangeType.Update) {
Vulnerability mechanics
Root cause
"The delta API's change compression logic incorrectly reduces a create→delete sequence to NOOP (removing both events) instead of preserving the delete, and the delta endpoint fails to re-verify item sharing permissions before attaching latest item content to delta output."
Attack vector
An attacker who is a share recipient can call the delta API (with DELTA_INCLUDES_ITEMS enabled, the default) to enumerate changes. The compression logic in `ChangeModel.ts` [patch_id=648828] previously collapsed a create→delete pair into nothing, but because compression is applied per page, a create on one page and a create→delete on another page would leave the create event visible while dropping the delete. Additionally, the delta API attaches the latest state of items to the output without checking whether those items are still shared with the requesting user. By crafting a sequence of share/unshare operations and paginated delta requests, the attacker can obtain a create event for a note that was deleted or unshared, with the full note content included in the response.
Affected code
The vulnerability is in `packages/server/src/models/ChangeModel.ts` [patch_id=648828], specifically in the `compressChanges` method where the create→delete compression rule was incorrectly set to NOOP (removing both events). The delta method also fails to re-verify item sharing permissions when attaching the latest item content to delta output. The corresponding test file `ChangeModel.test.ts` was updated to reflect the corrected behavior.
What the fix does
The patch [patch_id=648828] makes two changes in `ChangeModel.ts`. First, the compression rule for create→delete is changed from removing both entries (NOOP) to preserving the delete event, so that deletions are never silently dropped across page boundaries. Second, the test expectations are updated to reflect that a delete event is now emitted instead of suppressed. These changes ensure that when an item is deleted (or unshared), the delta API correctly reports the deletion to all recipients, preventing them from seeing a stale create event with the item's full content.
Preconditions
- authAttacker must be an authenticated user who has been granted share access to a note.
- networkAttacker must be able to send HTTP requests to the Joplin server's delta API endpoint.
- inputAttacker must craft delta API requests, potentially using pagination, to exploit the per-page compression flaw.
Generated on May 20, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3News mentions
0No linked articles in our index yet.