CVE-2022-23340
Description
Joplin 2.6.10 allows remote attackers to execute system commands through malicious code in user search results.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Joplin 2.6.10 fails to sanitize user content in search results, allowing XSS that can lead to remote code execution via the Electron shell.
Vulnerability
Joplin version 2.6.10 contains a cross-site scripting (XSS) vulnerability in the "Goto Anything" search feature (triggered by Ctrl+P). When a user searches for a term, the application renders note content in the search results without proper HTML sanitization. An attacker can embed a malicious HTML payload (e.g., an ` tag with an onerror` event) inside a note that is indexed by the search engine. If the victim's search matches the surrounding text, the payload is rendered and executed in the Electron context. References [1], [2], and [4] confirm the issue and the fix in commit 810018b.
Exploitation
An attacker must have the ability to create or modify a note in Joplin that will be indexed by the local search engine. No network position or authentication is required beyond normal note access. The attacker inserts a payload like <img/src="1"/onerror=eval("require('child_process').exec('calc.exe');");> within the note content near searchable text. When the victim presses Ctrl+P and types the search term, Joplin displays the matching results and executes the embedded JavaScript in the Electron process. The attack requires no user interaction beyond the normal search action.
Impact
Successful exploitation allows an attacker to execute arbitrary JavaScript code in the context of the Electron application, which can leverage Node.js APIs (e.g., require('child_process').exec) to run system commands. This leads to full remote code execution (RCE) on the victim's machine, with the same privileges as the Joplin process. The impact includes complete compromise of confidentiality, integrity, and availability of the user's data and system [1][4].
Mitigation
The vulnerability was fixed in commit 810018b, which introduced proper sanitization (using MarkupToHtml from the renderer package) for the search result display. Users should upgrade to Joplin version 2.6.11 or later. No official workaround is available for the vulnerable version (2.6.10), but users can avoid using the Ctrl+P search with untrusted notes until patched. The issue is tracked on GitHub as #6004 and has a related CVE (CVE-2022-23340) [2][3].
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 packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
joplinnpm | < 2.7.1 | 2.7.1 |
Affected products
2- Joplin/Joplindescription
Patches
1810018b41f4dDesktop: Security: Fixes #6004: Prevent XSS in Goto Anything
6 files changed · +49 −39
.eslintignore+3 −0 modified@@ -1927,6 +1927,9 @@ packages/renderer/headerAnchor.js.map packages/renderer/htmlUtils.d.ts packages/renderer/htmlUtils.js packages/renderer/htmlUtils.js.map +packages/renderer/htmlUtils.test.d.ts +packages/renderer/htmlUtils.test.js +packages/renderer/htmlUtils.test.js.map packages/renderer/index.d.ts packages/renderer/index.js packages/renderer/index.js.map
.gitignore+3 −0 modified@@ -1917,6 +1917,9 @@ packages/renderer/headerAnchor.js.map packages/renderer/htmlUtils.d.ts packages/renderer/htmlUtils.js packages/renderer/htmlUtils.js.map +packages/renderer/htmlUtils.test.d.ts +packages/renderer/htmlUtils.test.js +packages/renderer/htmlUtils.test.js.map packages/renderer/index.d.ts packages/renderer/index.js packages/renderer/index.js.map
packages/app-desktop/plugins/GotoAnything.tsx+2 −1 modified@@ -19,6 +19,7 @@ const { mergeOverlappingIntervals } = require('@joplin/lib/ArrayUtils.js'); import markupLanguageUtils from '../utils/markupLanguageUtils'; import focusEditorIfEditorCommand from '@joplin/lib/services/commands/focusEditorIfEditorCommand'; import Logger from '@joplin/lib/Logger'; +import { MarkupToHtml } from '@joplin/renderer'; const logger = Logger.create('GotoAnything'); @@ -81,7 +82,7 @@ class Dialog extends React.PureComponent<Props, State> { private inputRef: any; private itemListRef: any; private listUpdateIID_: any; - private markupToHtml_: any; + private markupToHtml_: MarkupToHtml; private userCallback_: any = null; constructor(props: Props) {
packages/lib/htmlUtils.ts+0 −35 modified@@ -1,7 +1,6 @@ const urlUtils = require('./urlUtils.js'); const Entities = require('html-entities').AllHtmlEntities; const htmlentities = new Entities().encode; -const htmlparser2 = require('@joplin/fork-htmlparser2'); const { escapeHtml } = require('./string-utils.js'); // [\s\S] instead of . for multiline matching @@ -138,40 +137,6 @@ class HtmlUtils { return output.join(' '); } - public stripHtml(html: string) { - const output: string[] = []; - - const tagStack: any[] = []; - - const currentTag = () => { - if (!tagStack.length) return ''; - return tagStack[tagStack.length - 1]; - }; - - const disallowedTags = ['script', 'style', 'head', 'iframe', 'frameset', 'frame', 'object', 'base']; - - const parser = new htmlparser2.Parser({ - - onopentag: (name: string) => { - tagStack.push(name.toLowerCase()); - }, - - ontext: (decodedText: string) => { - if (disallowedTags.includes(currentTag())) return; - output.push(decodedText); - }, - - onclosetag: (name: string) => { - if (currentTag() === name.toLowerCase()) tagStack.pop(); - }, - - }, { decodeEntities: true }); - - parser.write(html); - parser.end(); - - return output.join('').replace(/\s+/g, ' '); - } } export default new HtmlUtils();
packages/renderer/htmlUtils.test.ts+32 −0 added@@ -0,0 +1,32 @@ +import htmlUtils from './htmlUtils'; + +describe('htmlUtils', () => { + + test('should strip off HTML', () => { + const testCases = [ + [ + '', + '', + ], + [ + '<b>test</b>', + 'test', + ], + [ + 'Joplin®', + 'Joplin®', + ], + [ + '<b>test</b>', + '<b>test</b>', + ], + ]; + + for (const t of testCases) { + const [input, expected] = t; + const actual = htmlUtils.stripHtml(input); + expect(actual).toBe(expected); + } + }); + +});
packages/renderer/htmlUtils.ts+9 −3 modified@@ -97,8 +97,7 @@ class HtmlUtils { return selfClosingElements.includes(tagName.toLowerCase()); } - // TODO: copied from @joplin/lib - stripHtml(html: string) { + public stripHtml(html: string) { const output: string[] = []; const tagStack: string[] = []; @@ -130,7 +129,14 @@ class HtmlUtils { parser.write(html); parser.end(); - return output.join('').replace(/\s+/g, ' '); + // In general, we want to get back plain text from this function, so all + // HTML entities are decoded. Howver, to prevent XSS attacks, we + // re-encode all the "<" characters, which should break any attempt to + // inject HTML tags. + + return output.join('') + .replace(/\s+/g, ' ') + .replace(/</g, '<'); } public sanitizeHtml(html: string, options: any = null) {
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-8478-53pv-jxvmghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-23340ghsaADVISORY
- github.com/laurent22/joplin/commit/810018b41f4d9f0c2fd9af4b8fd332fa4a0210f0ghsaWEB
- github.com/laurent22/joplin/issues/6004ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.