Summary
A stored XSS in the Web Clipper rendering flow can be escalated to remote code execution in the desktop app.
The root cause is that the clipper preserves attacker-controlled attributes from the source page’s root element and stores them inside web-clip HTML. When the clip is later opened, Notesnook renders that HTML into a same-origin, unsandboxed iframe using contentDocument.write(...). Event-handler attributes such as onload, onclick, or onmouseover execute in the Notesnook origin. In the desktop app, this becomes RCE because Electron is configured with nodeIntegration: true and contextIsolation: false.
Details
The vulnerable attribute-copying logic is in the clipper:
packages/clipper/src/index.ts
for (const [name, value] of Object.entries(
toAttributes(document.documentElement)
)) {
body.setAttribute(name, value);
}
toAttributes() copies all attributes from the source page root without filtering:
function toAttributes(element: HTMLElement) {
const attributes: Record<string, string> = {};
for (const { name } of element.attributes) {
const value = element.getAttribute(name);
if (!value) continue;
attributes[name] = value;
}
return attributes;
}
This means attacker-controlled attributes such as:
- onload
- onclick
- onmouseover
are preserved from the attacker page and moved into the clipped output.
The clipper then stores complete clips as .clip attachments and embeds them as web-clip iframes in note content:
apps/web/src/utils/web-extension-server.ts
[new TextEncoder().encode(clip.data).buffer],
`${sanitizeFilename(clip.title)}.clip`,
{
type: "application/vnd.notesnook.web-clip"
}
);
The web-clip renderer later loads the attachment HTML and writes it directly into an iframe:
packages/editor/src/extensions/web-clip/component.tsx
iframe.contentDocument.open();
iframe.contentDocument.write(
typeof html !== "string" || !html ? FAILED_CONTENT : html
);
iframe.contentDocument.close();
iframe.contentDocument.head.innerHTML += `<base target="_blank">`;
The iframe is not sandboxed:
<iframe
ref={embedRef}
width="auto"
frameBorder={"0"}
scrolling={fullscreen ? "yes" : "no"}
/>
Because the iframe is same-origin and unsandboxed, event-handler attributes execute with access to the Notesnook renderer context.
On desktop, Electron is configured unsafely in:
nodeIntegration: true,
contextIsolation: false,
This turns the stored XSS into desktop RCE.
Important note: the clipped root attributes are currently applied to the generated clip body, not to documentElement. This does not prevent exploitation because handlers such as onload still execute when the clip is rendered.
PoC
- The attacker prepares and hosts a malicious HTML page under their control. For example:
<!doctype html>
<html onload="top.alert(1)">
<head>
<meta charset="utf-8">
<title>web clip poc</title>
</head>
<body>
<h1>clip test</h1>
</body>
</html>
- The attacker sends the URL of that page to the victim.
- The victim opens the page in a normal browser where the Notesnook Web Clipper extension is installed and logged into the victim’s Notesnook account.
- The victim opens the Web Clipper extension popup and selects:
- Area: Full page
- Mode: Complete with styles
- The victim saves the clip to his Notesnook account.
- During clipping, the clipper copies attacker-controlled attributes from the source page root element and preserves event-handler attributes such as onload.
- Notesnook stores the clipped page as a note containing a web-clip attachment.
- The victim later opens the same Notesnook account in the web, mobile, desktop app, and the malicious note/attachment is synchronized there. -> stored XSS
- Because the Electron desktop app runs with nodeIntegration: true and contextIsolation: false, this XSS can be escalated to RCE.
Impact
This issue affects the Web Clipper complete clip flow.
- Stored XSS in all platform
- Remote Code Execution in the desktop app
Summary
A stored XSS in the Web Clipper rendering flow can be escalated to remote code execution in the desktop app.
The root cause is that the clipper preserves attacker-controlled attributes from the source page’s root element and stores them inside web-clip HTML. When the clip is later opened, Notesnook renders that HTML into a same-origin, unsandboxed iframe using
contentDocument.write(...). Event-handler attributes such asonload,onclick, oronmouseoverexecute in the Notesnook origin. In the desktop app, this becomes RCE because Electron is configured withnodeIntegration: trueandcontextIsolation: false.Details
The vulnerable attribute-copying logic is in the clipper:
packages/clipper/src/index.tstoAttributes() copies all attributes from the source page root without filtering:
This means attacker-controlled attributes such as:
are preserved from the attacker page and moved into the clipped output.
The clipper then stores complete clips as .clip attachments and embeds them as web-clip iframes in note content:
apps/web/src/utils/web-extension-server.tsThe web-clip renderer later loads the attachment HTML and writes it directly into an iframe:
packages/editor/src/extensions/web-clip/component.tsxThe iframe is not sandboxed:
Because the iframe is same-origin and unsandboxed, event-handler attributes execute with access to the Notesnook renderer context.
On desktop, Electron is configured unsafely in:
This turns the stored XSS into desktop RCE.
Important note: the clipped root attributes are currently applied to the generated clip body, not to documentElement. This does not prevent exploitation because handlers such as onload still execute when the clip is rendered.
PoC
Impact
This issue affects the Web Clipper complete clip flow.