Skip to content

RCE via stored XSS in note export rendering

Critical
thecodrr published GHSA-fjm8-jg78-89h4 Apr 25, 2026

Package

Notesnook Web/Desktop (Notesnook)

Affected versions

< 3.3.15

Patched versions

>= 3.3.15
Notesnook iOS/Android (Notesnook)
< 3.3.20
>= 3.3.20

Description

Summary

A stored XSS in the note export flow can be escalated to remote code execution in the desktop app.

The root cause is that exported note fields such as title, headline, and content are inserted into the generated HTML template without HTML escaping. When the note is later exported to PDF, Notesnook renders that HTML into a same-origin, unsandboxed iframe using iframe.srcdoc = .... Injected script executes in the Notesnook origin. In the desktop app, this becomes RCE because Electron is configured with nodeIntegration: true and contextIsolation: false.

Details

The export flow embeds note-controlled data into an HTML template without escaping:

  • packages/core/src/collections/notes.ts:416
    • notes.export() passes note data into the HTML template builder.
  • packages/core/src/utils/templates/html/template.ts:31
    • headline is interpolated into a meta tag.
  • packages/core/src/utils/templates/html/template.ts:33
    • title is interpolated into <title>.
  • packages/core/src/utils/templates/html/template.ts:210
    • title is interpolated into <h1>.
  • packages/core/src/utils/templates/html/template.ts:211
    • content is interpolated directly into the HTML body.

The exported HTML is then rendered for PDF generation through an unsandboxed iframe.srcdoc:

  • apps/web/src/common/export.ts:40
    • exportToPDF() creates an iframe and sets iframe.srcdoc = content.
    • No sandbox attribute is applied.

The data path from note content into exported HTML is reachable through the normal export flow:

  • packages/common/src/utils/export-notes.ts:219
    • exported note content is prepared and passed into database.notes.export().

On desktop, this becomes code execution because the main renderer is intentionally Node-enabled:

  • apps/desktop/src/main.ts:126
    • nodeIntegration: true
    • contextIsolation: false
    • nodeIntegrationInWorker: true

Because srcdoc inherits the parent origin, JavaScript running inside the exported iframe can call:

parent.require("fs")
and use Node primitives such as fs, child_process, etc.

This means the vulnerability is not just stored XSS in exported HTML; in Notesnook Desktop it becomes arbitrary code execution.

### Details
_Give all details on the vulnerability. Pointing to the incriminated source code is very helpful for the maintainer._

### PoC
1. The attacker prepares a malicious note title containing HTML/JavaScript. For example:

   ```html
   </h1><script>const fs=parent.require("fs");const os=parent.require("os");const path=parent.require("path");fs.writeFileSync(path.join(os.tmpdir(),"notesnook-xss-proof.txt"),"xss via exported note title");</script><h1>
The attacker gets this note into the victim’s Notesnook account. This can happen through any workflow that causes attacker-controlled note data to be stored or imported by the victim, such as a malicious backup/import dataset or other untrusted note content source.

The victim opens the malicious note in Notesnook.

The victim exports the note as PDF.

During export, Notesnook converts the note into HTML and inserts attacker-controlled fields such as title, headline, and content into the export template without HTML escaping.

Notesnook then renders the generated export HTML into a same-origin iframe using iframe.srcdoc.

Because the iframe is not sandboxed, the injected script executes in the Notesnook renderer origin. -> stored XSS in the export/render flow

In the desktop app, Electron is configured with nodeIntegration: true and contextIsolation: false, so JavaScript executing inside the injected export frame can access parent.require(...).

This allows the attacker to execute arbitrary local code in the victim’s desktop app context. In the PoC above, the payload creates the file %TEMP%\\notesnook-xss-proof.txt, which confirms code execution.
Expected result:
<img width="1919" height="1079" alt="image" src="https://github.com/user-attachments/assets/bfc1e221-9283-492f-9649-9211c4296a9c" />
xss via exported note title
This confirms that JavaScript embedded in the note title executed during export and accessed Node APIs from the desktop renderer.

A minimal alert-based payload for confirming JavaScript execution is:

</h1><script>parent.alert(1)</script><h1>
<img width="1919" height="1026" alt="image" src="https://github.com/user-attachments/assets/f12dd55d-c485-4bf3-accf-e51991e73390" />

However, the file-write PoC is more reliable evidence because it proves code execution rather than only a visual popup.
case rce :
Create a new note.
Set the note title to:
<script>
const { exec } = parent.require('child_process');
exec('calc.exe', (error) => {
  if (error) console.log('Error:', error);
});
document.body.insertAdjacentHTML('beforeend', '<p id="executed" style="color:red;font-size:20px;">✅ Payload executed - Calculator should open now!</p>');
</script>
Set the body to any benign text, for example: test.
Export the note as PDF.
Check whether the following calc open 
<img width="1919" height="1035" alt="image" src="https://github.com/user-attachments/assets/85744b9e-f003-46bf-9ac5-a15b3c4be787" />

### Impact
This is a stored client-side injection that becomes arbitrary code execution in the desktop application.

Who is impacted:

Notesnook Desktop users who export an attacker-controlled note.
Any workflow that imports or opens untrusted note data before export is in scope.
Security impact:

Arbitrary local code execution in the victim’s Notesnook Desktop context
Access to Node/Electron APIs from attacker-controlled JavaScript
Potential file read/write and command execution on the victim machine
Full compromise of the desktop app session and any data accessible to that process

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H

CVE ID

CVE-2026-42090

Weaknesses

Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

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. Learn more on MITRE.

Improper Control of Generation of Code ('Code Injection')

The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment. Learn more on MITRE.

Credits