CVE: This vulnerability corresponds to CVE-2026-73041.
Summary
/api/asset/setFileAnnotation writes a client-supplied string straight to disk with no parsing or validation. showHighlight then interpolates five fields from that file into HTML attributes with no escaping and inserts the result with insertAdjacentHTML, for every annotation on every rendered PDF page.
The annotation sidecar travels with its PDF through export, import, synchronisation and rename, so a shared or imported document carries the payload with it.
Two lines below the affected template, the author writes one further field using setAttribute, which is injection-proof.
Details
The sink. app/src/asset/anno.ts:682 on dev, :683 on master:
let html = `<div class="pdf__rect popover__block" data-node-id="${selected.id}"
data-relations="${selected.ids || ""}" data-mode="${selected.mode}"
data-type="${selected.type}" style="--pdf-annotation-color: ${selected.color}">`;
...
rectsElement.insertAdjacentHTML("beforeend", html + "</div>");
Five interpolations, none escaped. Master carries the first three; data-type and the style colour are a more recent addition. Both refs are exploitable, because ids is the first field that breaks out.
This runs from showHighlight() for every annotation on every rendered page of a PDF.
One field in the same block is handled correctly. Two lines below:
rectsElement.lastElementChild.setAttribute("data-content", selected.content);
setAttribute cannot be broken out of. The author addressed exactly this concern for content and left the five fields in the template literal untreated.
The kernel never parses the file. /api/asset/setFileAnnotation reads arg["data"].(string) and calls filelock.WriteFile(writePath, []byte(data)). The only inspection in the handler is if "{}" == data { remove }. There is no JSON unmarshal, no schema, no length or character constraint. The file is an opaque client blob persisted verbatim.
Confirmed live. An annotation entry was written into an existing .sya fixture, preserving its structure, then read back through /api/asset/getFileAnnotation and verified on disk. All payload fields were byte-identical at every stage. Parsing the HTML that showHighlight emits from that file produces:
<div> { class: "pdf__rect popover__block", data-node-id: "…", data-relations: "a" }
<img> { src: "x", onerror: "alert('PDFIDS')" } ← from selected.ids
<img> { src: "x", onerror: "alert('PDFTYPE')" } ← from selected.type
Two genuine event-handler elements, from two independent fields.
Delivery. The .sya sidecar is not a local artefact. It is included in IncSync() on write, copied on export at kernel/model/export.go:2578-2582 and read on import at :4012, and renamed alongside its asset at kernel/model/assets.go:1506-1514. A shared notebook, an imported package or a synchronised workspace that carries a PDF carries its annotations too. The payload executes when the recipient opens the PDF.
Consequence in the desktop application. app/electron/main.js sets nodeIntegration: true, contextIsolation: false and webSecurity: false on every window (lines 913, 1019-1022, 1933-1936, 2157, 2197, 2248), so script executing in a renderer reaches Node built-ins including require('child_process').
Scope of observation. The write path, the round trip through the API, the on-disk contents and the parsed output above are confirmed as described. I have not observed handler invocation in a running client, so the step from the parsed attribute to execution is stated from the Electron configuration rather than from a captured runtime event.
Relationship to existing advisories. pdf__rect, showHighlight, anno.ts, pdf-annotation-color and selected.color each return no match across the published advisories for this project.
setFileAnnotation, getFileAnnotation and .sya match only GHSA-v7ph-r5r6-4jcj, which concerns publish-access filtering on the read endpoint. That advisory reports an information disclosure and does not touch anno.ts, the render path or escaping. It is relevant here only as established context: it confirms that /api/asset/getFileAnnotation carries CheckAuth alone and is reachable by publish readers.
Proof of Concept
Write an annotation whose ids field contains markup:
POST /api/asset/setFileAnnotation
{"path":"<asset path>.sya","data":"<annotation JSON with
ids = a\"><img src=x onerror=alert('PDFIDS')>"}
→ {"code":0}
Confirm the round trip:
POST /api/asset/getFileAnnotation → returned verbatim
on disk → byte-identical
Then open the PDF in the client. showHighlight renders every annotation on the page.
Impact
An attacker-supplied annotation file executes script when a user opens the associated PDF. Because the renderer runs with nodeIntegration: true and contextIsolation: false, that script reaches Node built-ins including require('child_process'), so execution is not confined to the page.
The delivery path is ordinary product behaviour rather than a contrived scenario: annotation sidecars are exported, imported, synchronised and renamed alongside their PDFs. Sharing a notebook containing an annotated PDF, or importing a package containing one, is sufficient. No interaction beyond opening the document is required, and every annotation on every page is rendered through the affected code.
Suggested fix
Build the element with setAttribute for every field, matching what the following line already does for content. That removes the injection surface entirely rather than escaping around it.
If the template literal is retained, apply escapeAttr to all five interpolations and constrain color to a palette index or hex pattern.
Server-side, setFileAnnotation should unmarshal the payload into a typed structure and re-serialise it, rather than persisting an opaque client blob. That would also give the field constraints a place to live.
CVE: This vulnerability corresponds to CVE-2026-73041.
Summary
/api/asset/setFileAnnotationwrites a client-supplied string straight to disk with no parsing or validation.showHighlightthen interpolates five fields from that file into HTML attributes with no escaping and inserts the result withinsertAdjacentHTML, for every annotation on every rendered PDF page.The annotation sidecar travels with its PDF through export, import, synchronisation and rename, so a shared or imported document carries the payload with it.
Two lines below the affected template, the author writes one further field using
setAttribute, which is injection-proof.Details
The sink.
app/src/asset/anno.ts:682on dev,:683on master:Five interpolations, none escaped. Master carries the first three;
data-typeand the style colour are a more recent addition. Both refs are exploitable, becauseidsis the first field that breaks out.This runs from
showHighlight()for every annotation on every rendered page of a PDF.One field in the same block is handled correctly. Two lines below:
setAttributecannot be broken out of. The author addressed exactly this concern forcontentand left the five fields in the template literal untreated.The kernel never parses the file.
/api/asset/setFileAnnotationreadsarg["data"].(string)and callsfilelock.WriteFile(writePath, []byte(data)). The only inspection in the handler isif "{}" == data { remove }. There is no JSON unmarshal, no schema, no length or character constraint. The file is an opaque client blob persisted verbatim.Confirmed live. An annotation entry was written into an existing
.syafixture, preserving its structure, then read back through/api/asset/getFileAnnotationand verified on disk. All payload fields were byte-identical at every stage. Parsing the HTML thatshowHighlightemits from that file produces:Two genuine event-handler elements, from two independent fields.
Delivery. The
.syasidecar is not a local artefact. It is included inIncSync()on write, copied on export atkernel/model/export.go:2578-2582and read on import at:4012, and renamed alongside its asset atkernel/model/assets.go:1506-1514. A shared notebook, an imported package or a synchronised workspace that carries a PDF carries its annotations too. The payload executes when the recipient opens the PDF.Consequence in the desktop application.
app/electron/main.jssetsnodeIntegration: true,contextIsolation: falseandwebSecurity: falseon every window (lines 913, 1019-1022, 1933-1936, 2157, 2197, 2248), so script executing in a renderer reaches Node built-ins includingrequire('child_process').Scope of observation. The write path, the round trip through the API, the on-disk contents and the parsed output above are confirmed as described. I have not observed handler invocation in a running client, so the step from the parsed attribute to execution is stated from the Electron configuration rather than from a captured runtime event.
Relationship to existing advisories.
pdf__rect,showHighlight,anno.ts,pdf-annotation-colorandselected.coloreach return no match across the published advisories for this project.setFileAnnotation,getFileAnnotationand.syamatch only GHSA-v7ph-r5r6-4jcj, which concerns publish-access filtering on the read endpoint. That advisory reports an information disclosure and does not touchanno.ts, the render path or escaping. It is relevant here only as established context: it confirms that/api/asset/getFileAnnotationcarriesCheckAuthalone and is reachable by publish readers.Proof of Concept
Write an annotation whose
idsfield contains markup:Confirm the round trip:
Then open the PDF in the client.
showHighlightrenders every annotation on the page.Impact
An attacker-supplied annotation file executes script when a user opens the associated PDF. Because the renderer runs with
nodeIntegration: trueandcontextIsolation: false, that script reaches Node built-ins includingrequire('child_process'), so execution is not confined to the page.The delivery path is ordinary product behaviour rather than a contrived scenario: annotation sidecars are exported, imported, synchronised and renamed alongside their PDFs. Sharing a notebook containing an annotated PDF, or importing a package containing one, is sufficient. No interaction beyond opening the document is required, and every annotation on every page is rendered through the affected code.
Suggested fix
Build the element with
setAttributefor every field, matching what the following line already does forcontent. That removes the injection surface entirely rather than escaping around it.If the template literal is retained, apply
escapeAttrto all five interpolations and constraincolorto a palette index or hex pattern.Server-side,
setFileAnnotationshould unmarshal the payload into a typed structure and re-serialise it, rather than persisting an opaque client blob. That would also give the field constraints a place to live.