CVE-2026-73041: How a PDF Annotation in SiYuan Turns into Remote Code Execution (RCE)

Executables and scripts are the obvious suspects when we think about dangerous file types. Modern application architectures blur that line. CVE-2026-73041, disclosed against the SiYuan note-taking application and rated Critical at 9.4 on the CVSS v4.0 scale, illustrates exactly how a PDF annotation, something most users would never think twice about opening, can be weaponised into stored script execution that reaches far past the browser sandbox.

Annotation metadata occupies an odd blind spot in most security reviews. Teams scrutinise the content a user types and the files they upload, but the small fields riding alongside that content, colors, coordinates, labels, identifiers, are often treated as inert because they never look like user input in the traditional sense. SiYuan’s PDF annotation handling shows why that assumption breaks down and how quickly a single unescaped field can turn into a foothold on the underlying operating system.

CVE-2026-73041: Vulnerability Overview

SiYuan is a privacy-first note-taking application built around markdown and PDF annotation support. CVE-2026-73041 is found by Shirshak Roy, a Security Engineer at SECNORA. The master and dev branches of SiYuan’s kernel did not validate or escape annotation fields written to disk through the setFileAnnotation endpoint. 

Affected Product and Component

CVE-2026-73041 is scoped to the Go module siyuan-note/siyuan/kernel, distributed via the “gomod” ecosystem. This is the backend kernel package, not the Electron front end itself, which matters for the delivery chain, the vulnerable write and read handlers live in the kernel API, while the vulnerable render call lives in the desktop client’s TypeScript layer. The two have to work together for the chain to complete.

Affected Versions and Patch

The “Affected versions” field lists only master and dev, the project’s pre-release branches, rather than naming a range of numbered stable releases. NVD’s structured version data translates this into a CPE-friendly range for tooling purposes, any version less than 3.7.4 is marked affected and 3.7.4 itself is marked as the first unaffected release. Both framings point to the same practical instruction for CVE-2026-73041, anyone running a build earlier than v3.7.4, whether a tagged release or a development build, should update.

Technical Analysis

CVE-2026-73041 involves a vulnerability chain that begins with how SiYuan stores PDF annotation data and ends with how that data is rendered in the desktop client.

Root Cause: Unvalidated Writes to Disk

The write handler behind CVE-2026-73041, “/api/asset/setFileAnnotation” takes a client-supplied string and writes it straight to disk as a .sya sidecar file using “filelock.WriteFile()”. The handler’s only inspection of the incoming data is a check for whether it equals the literal string “{}”, in which case the file is removed instead of written. Beyond that single check, there is no JSON unmarshaling, no schema validation and no character escaping anywhere in the write path. The file is persisted to disk exactly as the client sent it, functioning as an opaque blob rather than structured, validated data. It’s a pattern that shows up elsewhere in the same API surface too, including in how SiYuan’s search endpoints handle unsanitised query input.

Sink: Unescaped DOM Interpolation

When a user opens a PDF, CVE-2026-73041 reaches its client-side rendering stage through showHighlight() in “app/src/asset/anno.ts”, which builds the annotation markup using a JavaScript template literal and inserts the result into the page with insertAdjacentHTML. Five fields read back from the .sya file are interpolated into that template with no escaping applied to any of them:

  • data-node-id
  • data-relations, populated from selected.ids
  • data-mode
  • data-type
  • the style attribute, populated from selected.color

This function runs for every annotation on every rendered page of a PDF, which means the exposure scales with however many annotations a document carries, not just the first one encountered. The master branch carries only the first three of these fields, “data-node-id”, “data-relations” and “data-mode”, “data-type” and the style attribute are a more recent addition present on dev. Both branches are exploitable regardless, since data-relations is the field that breaks out on either one.

Secure Pattern Used Elsewhere in the Same Function

Two lines below the vulnerable template, the same code block handles a sixth field, the annotation’s actual text content, differently: rectsElement.lastElementChild.setAttribute(“data-content”, selected.content). Because setAttribute assigns a value rather than parsing markup, it cannot be broken out of the way a template-literal interpolation can. That safer pattern already existed in the same function, just two lines away from where the five metadata fields needed it.

Proof of Concept

The verification process was concrete rather than theoretical. An annotation entry was written into an existing .sya fixture, preserving the file’s structure, using a crafted ids field that broke out of the attribute with an img tag carrying an onerror handler. The write returned success and the entry was then read back through /api/asset/getFileAnnotation and separately checked directly on disk. All payload fields were confirmed byte-identical at every stage as sent, as returned by the read endpoint and as stored on the filesystem. Parsing the HTML that showHighlight() produces from that stored data yielded two genuine event-handler elements, one sourced from the ids field and one from the type field, each carrying a working onerror attribute.

That is the specific, demonstrated extent of the proof of concept: breakout was shown for ids and type. The other three fields (data-node-id, data-mode and the style/color attribute) are unescaped by the same code, but there’s no separate confirmed breakout demonstrated for each of them individually and the style attribute in particular sits in a different attribute context than a bare HTML attribute, which could affect how it would need to be broken out in practice.

Exploitation Chain: From Stored XSS to RCE

This distinction matters enough to state on its own. The scope of observation is clear – the write path, the round trip through the read API, the on-disk contents, and the parsed DOM output are all confirmed through direct testing. The step from a script executing inside the renderer to full command execution on the host operating system was not captured as a live exploit run against a running client. It is instead stated based on the application’s Electron configuration.

That configuration disables the standard renderer sandboxing protections across the renderer windows, in app/electron/main.js at several locations (lines 913, 1019–1022, 1933–1936, 2157, 2197, 2248). Node integration is enabled, context isolation is turned off and web security is disabled for these windows.

With those settings in place, JavaScript running in a renderer process gains direct access to Node’s built-in modules including the module used to spawn OS-level processes which is the well-established mechanism by which this class of permissive Electron configuration converts a DOM-level script injection into host-level code execution. The stored XSS is proven end-to-end by the proof of concept. The bridge from that XSS to arbitrary command execution is a documented, plausible consequence of the app’s own configuration rather than something separately demonstrated against a live instance.

Attack Vector and Delivery

Propagation Through Sync, Export and Import

In CVE-2026-73041, the payload does not travel as a standalone script or a macro-laden document. It lives inside the .sya sidecar file that accompanies its parent PDF through the application’s normal file-handling behaviour. This propagation runs through several separate code paths: the sidecar is included in the sync process through IncSync() when a workspace is written, it is copied during export at kernel/model/export.go lines 2578 through 2582, it is read back in during import at line 4012 of the same file and it is renamed alongside its parent asset at kernel/model/assets.go lines 1506 through 1514. Four distinct operations, all part of ordinary product behaviour, all carrying the sidecar and whatever it contains.

User Interaction Required

Because sharing, exporting, importing and renaming all preserve the sidecar automatically, a notebook shared with a colleague, a package imported from an external source or a workspace synchronised across devices carries the annotation and any payload inside it, without any extra step on the part of whoever created it. On the receiving end, the only action required is opening the PDF normally. showHighlight() then renders every annotation on the page as part of standard rendering, with no separate prompt, download or confirmation step for the annotation data itself.

Severity Assessment

The severity of CVE-2026-73041 is reflected in its CVSS and SSVC assessments:

  • CVE ID: CVE-2026-73041
  • CWE-79, Improper Neutralization of Input During Web Page Generation (Cross-Site Scripting)
  • CVSS v4.0 Base Score: 9.4 (Critical)
  • CVSS v4.0 Vector: CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
  • CVSS v3.1 Base Score: 9.0 (Critical)
  • CVSS v3.1 Vector: AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H

The CVSS v4.0 vector for CVE-2026-73041 rates the attack as Network-based, Low complexity, with no attack requirements, Low privileges required and Passive user interaction, meaning the victim only needs to open the affected PDF. Confidentiality, integrity and availability impacts are all rated High for both the vulnerable and subsequent systems, reflecting the potential path from stored XSS to host-level code execution.

The SSVC assessment for CVE-2026-73041 records exploitation status as “poc,” automatability as “no,” and technical impact as “total,” indicating a demonstrated proof of concept with significant potential impact.

Related Vulnerabilities

The annotation subsystem has a documented history beyond this one finding. An earlier, separately tracked issue examined /api/asset/getFileAnnotation, the same read endpoint used to verify the round trip in this finding’s proof of concept and found that it enforced only CheckAuth, making it reachable by publish readers and resulting in an information-disclosure issue. That earlier finding, a gap in how SiYuan’s publish-password authentication holds up, does not touch anno.ts, the render path or any escaping question, so it is a separate class of problem. The only overlap is structural, both findings touch the same annotation endpoints, which is useful context for anyone auditing that part of the codebase as a whole.

Mitigation and Recommendations

CVE-2026-73041 is patched in SiYuan v3.7.4. Anyone running master, dev or an earlier release should update immediately.

For End Users

The practical guidance is simple, update to v3.7.4 or later before opening notebooks, workspaces or shared PDFs from sources you don’t fully control. Because the payload rides along with completely ordinary file operations, there is no separate “suspicious attachment” behaviour to watch for. Version currency is the mitigation.

For Developers

The suggested fix, along with the broader pattern here, offers lessons that generalise well past this one application:

  • Replace the template literal with setAttribute calls for all five fields, matching what the same function already does correctly for the content field two lines away. This closes the injection surface rather than working around it with escaping.
  • If the template literal must be kept, escape every interpolation and constrain the color field, ideally to a palette index or a strict hex-color pattern, so that even an unescaped value can’t carry markup.
  • Validate on the server side. setFileAnnotation should unmarshal its payload into a typed structure and re-serialise it before writing to disk, rather than persisting an opaque client blob. That would also give field-level constraints, like the color pattern above, a natural place to live.
  • Harden Electron configurations. nodeIntegration, true and contextIsolation – false convert any renderer-side script injection into a plausible path to host-level code execution. Modern Electron apps should sandbox renderer processes and use contextBridge for any communication that genuinely needs to reach the main process.
  • State what was confirmed and what was inferred, separately. This finding models that well, it draws a clear line between what the proof of concept directly demonstrated (the write, the read-back, the parsed DOM output) and what follows from the application’s own configuration but wasn’t independently captured (the jump to command execution).

Conclusion

CVE-2026-73041 represents a familiar chain, unvalidated input persisted to disk, rendered unsafely on the client and given a plausible path to full compromise by a permissive application architecture. The confirmed part of the chain, stored XSS through at least two of five unescaped annotation fields, is serious enough on its own and it was verified end to end from the initial write through the parsed DOM output.

Combined with an Electron configuration that removes the usual sandbox boundary and a delivery mechanism that piggybacks on entirely ordinary sync, export, import and rename behavior, it’s a strong argument for auditing how “just metadata” fields get treated on the way from disk to DOM and for being precise, in any writeup, about exactly which parts of a vulnerability chain were demonstrated versus inferred.

SECNORA helps organisations strengthen application security through expert-led penetration testing that identifies high-impact vulnerabilities before they reach production. Talk to our team.

References

  • NVD: CVE-2026-73041
  • GHSA-fqpw-c3pj-w8g9: PDF annotation fields are written to disk unparsed and rendered into five raw attributes, turning a shared PDF into Remote Code Execution on the desktop client