
No. 19 · 2 November 2026 · Security · Encryption
You Should Probably Know: a no-execution audit of Joplin 3.7.22
We audited the Joplin desktop note app without running a line of it. Nothing covert turned up, and the encryption is real and well built. The thing to put on a roadmap is plugins: they run with full operating-system privileges, and nothing authenticates them at install time.
Abstract. This is a static, no-execution audit of Joplin desktop 3.7.22, the open-source encrypted note application, read from its source at the release tag and from its signed macOS binaries, with nothing run. We looked for backdoors, phone-home behavior, data and PII leakage, a gap between the encryption it promises and the encryption it ships, and legal traps. Nothing covert was found, and the encryption is real and well built. The news is a trust boundary: third-party plugins run with full operating-system privileges in an unhardened Electron context, and Joplin fetches them with no integrity check, so installing or updating a plugin is a trust decision the user cannot inspect. Note content, by contrast, is isolated and does not reach that context in one step. Hawthorne 4.7 on a scale where 0 is benign and 10 is dangerous; Species III; band, "You Should Probably Know."
TL;DR. The thing to put on a roadmap is plugins: they run as you, and nothing authenticates them at install time. The encryption holds, with two caveats worth knowing — the sync server still sees the shape and size of your notebook, and your notes sit in plaintext on your own disk.
Joplin is a mature encrypted note app: Electron on the desktop, React Native on mobile, a command-line client, a shared library, and a self-hostable server. We audited the desktop at version 3.7.22, read as source at the release tag and as signed macOS binaries treated as data. Nothing was executed. Every claim below is cited to a line in the source at the audited commit, and the acquisition and tool details are in a linked manifest 15.
This is the second audit in the series, after PhotoCraft, and the method hasn't changed: read everything, run nothing, cite every claim to a primary source, score against a published rubric. Three frontier models from three different labs then scored the evidence independently, two of them changed what this essay says, and one of us makes the final call. More on those changes as they come up, because the part of the method that corrects itself is the honest part.
Joplin's main window is created with Node integration on, context isolation off, and @electron/remote enabled 1. Context isolation is the setting that keeps a page's JavaScript in its own world, unable to touch the privileged objects the app runs with. Turn it off and there is no such wall. Node integration on means code in that window can reach the filesystem and spawn processes. @electron/remote is the userland successor to a built-in feature Electron removed in version 14; Joplin still carries it, and the flag is live. The plugin runner is built the same way 3. So the context is dangerous, and the useful question is what can reach it.
Note content can't, not in one step, and the isolation that stops it is the whole defense for note content. Joplin renders notes in an iframe loaded over a different protocol from the application itself, which hands the content to a separate operating-system process and blocks it from reaching the top-level window 2. That's on purpose: Joplin built it to reduce the impact of bugs in its HTML sanitizer, and there's a custom sanitizer and a content security policy in front of it besides 5. An earlier draft of our own stub said a sanitizer bypass was one step from code execution. It was wrong, and one of the reviewers caught it. A sanitizer bypass lands in an isolated process; reaching the privileged window from there would also mean defeating the browser's process isolation. We did not, however, exhaustively audit the inter-process message handlers or the custom joplin-content:// protocol's access control, so the honest statement is that the bar is substantially raised, not that it is proven impassable.
Plugins are the real one-step surface, and they bundle several risks worth keeping apart. A plugin you deliberately install runs in that unhardened context with full privileges. A plugin author you already trusted can ship a malicious update. And Joplin fetches plugins with no signature and no checksum; there is no integrity field in the plugin manifest at all 4, so a compromised repository account or a compromised mirror could serve a bad artifact that the client would accept. A network attacker, separately, is held off only by TLS. We did not build a proof-of-concept malicious plugin. For this architecture that's an afternoon's work, and we chose not to, because the method forbids running anything. That choice is exactly why we score the finding plausible rather than confirmed.
Two things soften it. Joplin maintains a vetted "recommended" category, and a Restricted install mode that refuses any plugin not on that list 16; those are real controls, though they are curation, not cryptographic provenance. And the fix isn't one fix. Moving the main window to context isolation with a preload bridge is worth doing, but it does not remove Node access from the plugin runner, which needs its own privilege boundary, and neither change touches artifact authenticity. Want an authentic artifact? Check a signature against a publisher identity held separately from the download channel. A digest fetched from the same channel as the artifact proves very little. We scored the plugin surface 7.0 of 10 on severity; it's the finding worth a roadmap slot.
Joplin's end-to-end encryption is modern, and whoever built it understood the problem. Content is encrypted with AES-256 in GCM mode, an authenticated cipher, with a fresh random nonce each time 7. The key that protects your master key is derived from your password with PBKDF2 over SHA-512 at 220,000 iterations, the figure OWASP recommends, set deliberately in 2024 with a comment saying so 6. Keys are exchanged between users for shared notebooks with RSA using OAEP padding at 2048 bits 9.
One number in that code would make a keyword scanner scream, and the scanner would be wrong. Each item's data key is derived with only 3 iterations 8. (Yes, three.) The input there is already a random 256-bit key, not a guessable password, so stretching it buys nothing; the derivation exists to give each item a unique key and prevent nonce reuse. The number that looks most like a bug is the one they got right, and telling the difference is the job.
Two caveats, stated plainly. Older, weaker encryption methods remain in the code, kept only to decrypt data written years ago: the SJCL family, whose original method was marked deprecated and no longer secure in 2020 and derives its key with PBKDF2 at only 1,000 iterations 18. And key storage, if every secure option fails, ends in plaintext in the local settings database. On a single-user desktop that's a local exposure, not a remote one, but it matters on headless setups and for anyone who backs up their profile directory.
Here is what a static read can and can't claim. The inspected code implements a credible end-to-end design, and with encryption enabled and the first sync completed it protects the content of supported items in transit and on the sync target. We did not run key-handling tests, cryptographic test vectors, or traffic capture, so this is a reading of the design, not a certificate that every operational path encrypts.
End-to-end encryption protects your note content. The metadata around it is another matter. With encryption fully on, the sync server still sees a cleartext envelope: item identifiers and types, the links between notebooks, notes, and tags, timestamps, and, because the ciphertext is about as long as the plaintext, the approximate size of every note and attachment 10. From that, a curious or compelled host can reconstruct the shape of your notebook, count your notes, and watch when you work, without decrypting a single word. It's inherent to the sync model and openly commented in the code, so it isn't a defect. It does refute the common belief that end-to-end encryption means the server knows nothing.
The second reviewer added the other half. Encryption is applied when an item is serialized for sync, not when it's stored 10. Your local database and your attachments sit in plaintext on your own disk, even with encryption fully enabled. That's a reasonable design, since your disk is your responsibility, but it also makes the plaintext-password fallback above close to moot for local secrecy: the notes themselves are already in the clear on that same disk.
There's also a newer path worth naming, though we did not audit it. Joplin 3.7.22 ships an MCP server and AI tools that can read and search notes on behalf of an external AI application 17. It's off unless the user turns it on, gated by a setting, but once on it is a deliberate egress of decrypted content to another program, and possibly to that program's model provider. Sound sync encryption does not constrain what the local, authorized client is told to hand out. We flag it as an opt-in trust boundary and a candidate for the next pass.
The fallback keychain module is keytar 7.9.0, the final release of an abandoned project that receives no security fixes 11. As maintained software, it has ceased to be. On the desktop it's only the second choice behind the operating system's own keystore, so most users never touch it; on the command-line client it's the only option. Retiring it is cheap and worth doing.
Joplin also hands plugins a bundled 7-Zip binary. On macOS it runs in place from the app bundle. On Linux, Joplin copies it into a cache directory under your profile, makes it executable, and runs it from there; it compares the copy with the bundled one by MD5 when it looks, so a swap made after that look would run 19. That takes local write access to your profile, which is why it sits low: severity 3.0, plausible.
The licensing nuance trips up companies, not users. The repository is AGPL by default, but a subdirectory can override that with its own license, and the server is under a personal-use license that forbids commercial use 12. Using the desktop app for your work notes is fine; operating or commercially offering the server is the part that needs a separate arrangement. The repository says so plainly. What gets a company into trouble is the "Joplin is open source" shorthand, a mumpsimus that tends to outlive the correction.
For completeness: there's no default telemetry, and crash reporting is off unless you turn it on 13. The shipped macOS binaries are signed, with no writable-and-executable memory, no packed or encrypted regions, and no embedded credentials; the highest section entropy we measured was 6.956, where packed or encrypted content would typically run well above 7.2 15. A present code signature proves provenance, not benignity. The server hashes passwords with bcrypt 14. The invisible-character sweep came back clean: every hit was translation data or a test fixture.
We ran nothing. We acquired only the macOS build, so the Windows and Linux native code is unexamined, and we did not unpack the plugin archives, exhaustively audit the IPC surface, or inspect the MCP path. Deep binary analysis wasn't available for this file format. We carry those gaps as explicit deductions instead of pretending they're clean. Three verdicts hide in that sentence, and they shouldn't blur: we found no covert behavior in what we inspected; we demonstrated no exploit of the boundaries we looked at; and we make no claim about what we did not execute or examine. The first two are defensible. None of them is a clean bill.
Hawthorne 4.7 of 10, Species III, in the band we call "You Should Probably Know." Under the rubric each finding deducts by severity and confidence, and the number is the sort key beneath that judgment. The biggest single deduction is the unhardened plugin context at severity 7.0, plausible; the rest are the keytar dependency, the plaintext-password fallback, the server license, the metadata disclosure, and smaller items, with explicit deductions for the surfaces we did not open. The itemized ledger is below. What would move it? A working proof-of-concept plugin exploit would make that finding confirmed, double its deduction to 1.96, and take the score to 5.7, still inside this band, which ends at 6.0. Crossing into "Scanners Out, Red Team Up" would take more than that one proof, for instance the integrity finding confirmed as well, which lands at 6.2. Shipping context isolation on the main window and a real plugin privilege boundary would remove the headline deduction and pull the score down to 3.7, "Mostly Harmless."
The ledger shows one thing this paragraph shouldn't hide. Rubric v0.3 weighs confirmed minor items heavily: the server license, a trap rather than a threat, deducts 0.64, the same as the plaintext-password fallback and two-thirds of the plugin headline. The rubric is fixed for the length of an audit, so it stands here; v0.4 will revisit it.
It's neither an alarm nor a free pass. It's a strong, privacy-respecting application with one real, bounded, fixable gap, worth reading before you lean on it. The two corrections above are in the essay on purpose, because a method that can't correct itself in public isn't worth trusting with the verdict. This one needed two. We kept both.
packages/app-desktop/ ElectronAppWrapper.ts
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/packages/app-desktop/ElectronAppWrapper.ts#L269-L272
readme/dev/ spec/note_viewer_isolation.md
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/readme/dev/spec/note_viewer_isolation.md
packages/app-desktop/services/plugins/PluginRunner.ts
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/packages/app-desktop/services/plugins/PluginRunner.ts#L122-L123
packages/lib/services/plugins/ RepositoryApi.ts
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/packages/lib/services/plugins/RepositoryApi.ts#L38 ; manifest type (no checksum field):
packages/lib/services/plugins/utils/types.ts
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/packages/lib/services/plugins/utils/types.ts#L42
packages/renderer/htmlUtils.ts
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/packages/renderer/htmlUtils.ts#L152 ;
packages/app-desktop/index.html
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/packages/app-desktop/index.html#L8
packages/lib/services/e2ee/ EncryptionService.ts
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/packages/lib/services/e2ee/EncryptionService.ts#L480
packages/lib/services/e2ee/crypto.ts
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/packages/lib/services/e2ee/crypto.ts#L20
packages/lib/services/ e2ee/EncryptionService.ts
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/packages/lib/services/e2ee/EncryptionService.ts#L479-L502
packages/lib/services/e2ee/ ppk/webCrypto/WebCryptoRsa.ts
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/packages/lib/services/e2ee/ppk/webCrypto/WebCryptoRsa.ts#L30
packages/lib/models/ BaseItem.ts
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/packages/lib/models/BaseItem.ts#L517-L569
packages/lib/models/Setting.ts
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/packages/lib/models/Setting.ts#L674 ;
packages/app-desktop/package.json
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/packages/app-desktop/package.json#L240
LICENSE
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/LICENSE ;
packages/server/LICENSE.md
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/packages/server/LICENSE.md
packages/lib/models/settings/builtInMetadata.ts
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/packages/lib/models/settings/builtInMetadata.ts#L1835
packages/server/src/utils/auth.ts
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/packages/server/src/utils/auth.ts#L4
binary-manifest.md
, this essay folder.
packages/lib/services/plugins/ RepositoryApi.ts
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/packages/lib/services/plugins/RepositoryApi.ts#L202-L203
packages/lib/services/rest/routes/ mcp.ts
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/packages/lib/services/rest/routes/mcp.ts#L23 ;
packages/lib/services/ai/tools/global/ readNotes.ts
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/packages/lib/services/ai/tools/global/readNotes.ts
packages/lib/services/e2ee/EncryptionService.ts
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/packages/lib/services/e2ee/EncryptionService.ts#L364-L370
packages/app-desktop/ utils/7zip/getPathToExecutable7Zip.ts
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/packages/app-desktop/utils/7zip/getPathToExecutable7Zip.ts#L16-L52 ; handed to plugins:
packages/app-desktop/services/ plugins/PluginRunner.ts
, https://github.com/laurent22/joplin/blob/95cab5a86d770c57f58fd43953f37a2cfe5a7016/packages/app-desktop/services/plugins/PluginRunner.ts#L131-L133
Rubric v0.3, adopted 2026-10-30, the same version that scored PhotoCraft's 3.8. Each finding deducts d = 4 × (severity/10)² × confidence; ambiguities add a fixed deduction (0.1 minor surface, 0.3 major); the total is the sort key, floored at 0.1 while anything is unresolved. Deductions below sum to 4.69, reported as 4.7. Confidence uses the claim register's ladder: source scores as confirmed (1.0); inference and untested chain score as plausible (0.5); open scores as an ambiguity. Bands: 2.1–4.0 Mostly Harmless; 4.1–6.0 You Should Probably Know; 6.1–8.0 Scanners Out, Red Team Up. Rows are sorted by deduction. Numbers are finding IDs in the audit database, in order of discovery; 12–18 are surfaces we cleared, which deduct nothing and are below the prose threshold where the essay doesn't name them.
| # | Finding | Severity | Confidence | Deduction |
|---|---|---|---|---|
| 1 | Unhardened Electron context (main window + plugin runner) | 7.0 | plausible | 0.98 |
| 3 | keytar 7.9.0 deprecated/ unmaintained | 4.0 | confirmed | 0.64 |
| 4 | Master password falls back to plaintext locally | 4.0 | confirmed | 0.64 |
| 5 | Server under a non-commercial Personal Use License | 4.0 | confirmed (legal) | 0.64 |
| 2 | No plugin integrity/signature check | 5.0 | plausible | 0.50 |
| 6 | E2EE metadata leakage (sync envelope) | 3.0 | confirmed | 0.36 |
| 9 | app.asar / large binaries / .jpl not unpacked | — | ambiguity | 0.30 |
| 7 | Legacy SJCL methods retain a weak KDF | 2.5 | confirmed | 0.25 |
| 8 | Bundled 7za handling (mac no- exec; linux cache) | 3.0 | plausible | 0.18 |
| 10 | Windows/Linux native code not examined | — | ambiguity | 0.10 |
| 11 | Deep binary analysis unavailable for Mach-O | — | ambiguity | 0.10 |
| 19 | Local notes plaintext at rest (disclosure) | info | contributing | 0.00 |
| 12 | No embedded malicious code or backdoor found | — | cleared | 0.00 |
| 13 | No telemetry or analytics; crash upload opt-in | — | cleared | 0.00 |
| 14 | E2EE primitives strong | — | cleared | 0.00 |
| 15 | No hidden instructions in the source | — | cleared | 0.00 |
| 16 | Dependency licenses clean | — | cleared | 0.00 |
| 17 | Web Clipper token-gated with explicit approval | — | cleared | 0.00 |
| 18 | Server password hashing sound | — | cleared | 0.00 |
Status legend: source = read directly from the pinned source; inference = a judgment from source we did not execute; untested chain = a plausible attack path with an unverified link; open = a surface we did not examine.
| Claim | Refs | Status |
|---|---|---|
| Main window: nodeIntegration on, contextIsolation off, @electron/remote on | [1] | source |
| Plugin runner uses the same unhardened flags | [3] | source |
| Note content is site-isolated (cross-protocol iframe, separate process) | [2] | source |
| No IPC / custom-protocol escape of that isolation was ruled out | [2] | open |
| Plugins fetched with no integrity field in the manifest | [4] | source |
| A malicious/updated/compromised plugin → full- privilege code execution | [3][4] | untested chain |
| Recommended-plugin vetting and Restricted install mode exist | [16] | source |
| AES-256-GCM, PBKDF2-SHA512 220k, RSA- OAEP-2048 | [6][7][9] | source |
| Per-item iterationCount 3 is correct, not a weakness | [8] | inference |
| Legacy SJCL methods kept to decrypt old data; the original uses PBKDF2 at 1,000 | [18] | source |
| Linux 7-Zip copy runs from the profile cache, MD5- compared when checked | [19] | source |
| E2EE whole-system correctness under every path | [6][7] | open |
| Sync envelope leaks graph/types/timestamps/sizes | [10] | source |
| Local database/attachments plaintext at rest | [10] | source |
| MCP/AI note-read egress path exists, setting-gated | [17] | source |
| MCP path's auth depth and defaults | [17] | open |
| macOS binaries signed, no W+X, max entropy 6.956 | [15] | source |
| Windows/Linux binaries benign | — | open |
References
- 01Main-window flags (nodeIntegration, contextIsolation, @electron/remote):
- 02Note-viewer isolation (cross-protocol iframe, separate process, no top-level access):
- 03Plugin runner flags:
- 04Plugin repository, mirrors, and no integrity field:
- 05HTML sanitizer and content security policy:
- 06Master-key KDF, PBKDF2-SHA512 at 220000:
- 07AES-256-GCM content cipher:
- 08Per-item data-key derivation and the reason for a low iteration count:
- 09RSA-OAEP public-key sharing (keys exchanged between users):
- 10Sync serialization, cleartext envelope, and encryption-at-serialization:
- 11keytar fallback and version:
- 12License structure and the server personal-use license: root
- 13Crash-report opt-in default:
- 14Server password hashing with bcrypt:
- 15Binary acquisition and static-triage manifest (release-archive SHA-256, members, tools, results):
- 16Recommended-plugins vetting and Restricted install mode:
- 17MCP server and AI note-read tools, setting-gated:
- 18Legacy SJCL encryption methods; the original SJCL method, deprecated 2020-01-23, uses PBKDF2 at 1000 iterations:
- 19Bundled 7-Zip path, Linux cache copy, MD5 comparison and chmod: