CVE-2022-45598: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Cross Site Scripting vulnerability in Joplin Desktop App before v2.9.17 allows attacker to execute arbitrary code via improper santization.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 6.1
Published: 2023-01-31T16:15Z
Repository: https://github.com/laurent22/joplin
Language: TypeScript
Files changed: packages/app-cli/tests/md_to_html/sanitize_12.html, packages/app-cli/tests/md_to_html/sanitize_12.md, packages/renderer/MdToHtml.ts
Commit date: 2022-11-14 17:16:59 +0000
Commit message: All: Security: Fix XSS when a specially crafted string is passed to the renderer

Diff: diff --git a/packages/app-cli/tests/md_to_html/sanitize_12.html b/packages/app-cli/tests/md_to_html/sanitize_12.html
new file mode 100644
index 000000000..c8a5e8c24
--- /dev/null
+++ b/packages/app-cli/tests/md_to_html/sanitize_12.html
@@ -0,0 +1 @@
+<div class="joplin-editable"><pre class="joplin-source" data-joplin-language="&quot;&gt;&lt;svg/onload=top.eval(atob(&quot;cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ29wZW4gLW4gL1N5c3RlbS9BcHBsaWNhdGlvbnMvQ2FsY3VsYXRvci5hcHAvQ29udGVudHMvTWFjT1MvQ2FsY3VsYXRvcicp&quot;))&gt;" data-joplin-source-open="```&quot;&gt;&lt;svg/onload=top.eval(atob(&quot;cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ29wZW4gLW4gL1N5c3RlbS9BcHBsaWNhdGlvbnMvQ2FsY3VsYXRvci5hcHAvQ29udGVudHMvTWFjT1MvQ2FsY3VsYXRvcicp&quot;))&gt;&#10;" data-joplin-source-close="&#10;```">ts</pre><pre class="hljs"><code><span class="hljs-attribute">ts</span></code></pre></div>
diff --git a/packages/app-cli/tests/md_to_html/sanitize_12.md b/packages/app-cli/tests/md_to_html/sanitize_12.md
new file mode 100644
index 000000000..22f71bb68
--- /dev/null
+++ b/packages/app-cli/tests/md_to_html/sanitize_12.md
@@ -0,0 +1,3 @@
+```"><svg/onload=top.eval(atob("cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ29wZW4gLW4gL1N5c3RlbS9BcHBsaWNhdGlvbnMvQ2FsY3VsYXRvci5hcHAvQ29udGVudHMvTWFjT1MvQ2FsY3VsYXRvcicp"))>
+ts
+```
diff --git a/packages/renderer/MdToHtml.ts b/packages/renderer/MdToHtml.ts
index 333c0e7cd..1e57d6993 100644
--- a/packages/renderer/MdToHtml.ts
+++ b/packages/renderer/MdToHtml.ts
@@ -6,10 +6,12 @@ import validateLinks from './MdToHtml/validateLinks';
 import { ItemIdToUrlHandler } from './utils';
 import { RenderResult, RenderResultPluginAsset } from './MarkupToHtml';
 import { Options as NoteStyleOptions } from './noteStyle';
 import hljs from './highlight';
 
+const Entities = require('html-entities').AllHtmlEntities;
+const htmlentities = new Entities().encode;
 const MarkdownIt = require('markdown-it');
 const md5 = require('md5');
 
 export interface RenderOptions {
 	contentMaxWidth?: number;
@@ -480,11 +482,11 @@ export default class MdToHtml {
 				let outputCodeHtml = '';
 
 				// The strings includes the last \n that is part of the fence,
 				// so we remove it because we need the exact code in the source block
 				const trimmedStr = this.removeLastNewLine(str);
-				const sourceBlockHtml = `<pre class="joplin-source" data-joplin-language="${lang}" data-joplin-source-open="\`\`\`${lang}&#10;" data-joplin-source-close="&#10;\`\`\`">${markdownIt.utils.escapeHtml(trimmedStr)}</pre>`;
+				const sourceBlockHtml = `<pre class="joplin-source" data-joplin-language="${htmlentities(lang)}" data-joplin-source-open="\`\`\`${htmlentities(lang)}&#10;" data-joplin-source-close="&#10;\`\`\`">${markdownIt.utils.escapeHtml(trimmedStr)}</pre>`;
 
 				if (this.shouldSkipHighlighting(trimmedStr, lang)) {
 					outputCodeHtml = markdownIt.utils.escapeHtml(trimmedStr);
 				} else {
 					try {

Vulnerable code: 				const sourceBlockHtml = `<pre class="joplin-source" data-joplin-language="${lang}" data-joplin-source-open="\`\`\`${lang}&#10;" data-joplin-source-close="&#10;\`\`\`">${markdownIt.utils.escapeHtml(trimmedStr)}</pre>`;
Fixed code: <div class="joplin-editable"><pre class="joplin-source" data-joplin-language="&quot;&gt;&lt;svg/onload=top.eval(atob(&quot;cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ29wZW4gLW4gL1N5c3RlbS9BcHBsaWNhdGlvbnMvQ2FsY3VsYXRvci5hcHAvQ29udGVudHMvTWFjT1MvQ2FsY3VsYXRvcicp&quot;))&gt;" data-joplin-source-open="```&quot;&gt;&lt;svg/onload=top.eval(atob(&quot;cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ29wZW4gLW4gL1N5c3RlbS9BcHBsaWNhdGlvbnMvQ2FsY3VsYXRvci5hcHAvQ29udGVudHMvTWFjT1MvQ2FsY3VsYXRvcicp&quot;))&gt;&#10;" data-joplin-source-close="&#10;```">ts</pre><pre class="hljs"><code><span class="hljs-attribute">ts</span></code></pre></div>
```"><svg/onload=top.eval(atob("cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ29wZW4gLW4gL1N5c3RlbS9BcHBsaWNhdGlvbnMvQ2FsY3VsYXRvci5hcHAvQ29udGVudHMvTWFjT1MvQ2FsY3VsYXRvcicp"))>
ts
```
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = new Entities().encode;
				const sourceBlockHtml = `<pre class="joplin-source" data-joplin-language="${htmlentities(lang)}" data-joplin-source-open="\`\`\`${htmlentities(lang)}&#10;" data-joplin-source-close="&#10;\`\`\`">${markdownIt.utils.escapeHtml(trimmedStr)}</pre>`;