VYPR
Critical severityNVD Advisory· Published May 29, 2026· Updated May 29, 2026

CVE-2026-45668

CVE-2026-45668

Description

Trilium Notes is a cross-platform, hierarchical note taking application focused on building large personal knowledge bases. Prior to 0.102.2, a malicious ZIP archive imported with safe import enabled achieves RCE via #docName path traversal and XSS by combining a payload note (type: code, mime: text/plain) containing raw HTML/JS and a trigger note (type: doc or type: launcher) with a #docName label that uses ../ path traversal to point at the payload note's API endpoint. The desktop client Electron renderer runs with nodeIntegration enabled, so an RCE is triggered once the payload is executed. This vulnerability is fixed in 0.102.2.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Trilium Notes prior to 0.102.2 allows RCE via safe import bypass using #docName path traversal and XSS in the desktop client.

Vulnerability

Trilium Notes before version 0.102.2 contains a vulnerability in the safe import feature. A malicious ZIP archive can achieve remote code execution (RCE) by combining a payload note (type: code, mime: text/plain) containing raw HTML/JS and a trigger note (type: doc or type: launcher) with a #docName label that uses ../ path traversal to point at the payload note's API endpoint. The desktop client Electron renderer runs with nodeIntegration enabled, allowing the payload to execute. [1]

Exploitation

An attacker must import a crafted ZIP archive with safe import enabled. The safe import does not sanitize non-HTML content types, so the raw HTML/JS payload is stored. The trigger note's #docName label uses path traversal (e.g., ../) to reference the payload note's API endpoint. When the trigger note is rendered, the URL is constructed without validation, leading to XSS. Because nodeIntegration is enabled, the XSS escalates to RCE. [1]

Impact

Successful exploitation grants the attacker remote code execution in the Electron renderer process. This allows full control over the desktop client, including access to the user's notes and system resources. [1]

Mitigation

The vulnerability is fixed in Trilium Notes version 0.102.2. Users should upgrade to this version immediately. No workarounds are available. The CVE is not listed on the Known Exploited Vulnerabilities (KEV) catalog. [1]

AI Insight generated on May 29, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2
  • Triliumnext/Triliuminferred2 versions
    <0.102.2+ 1 more
    • (no CPE)range: <0.102.2
    • (no CPE)range: <0.102.2

Patches

9
fc1be0d23db5

fix(ckeditor5-mermaid): use textContent for diagram source rendering

https://github.com/triliumnext/triliumElian DoranApr 5, 2026Fixed in 0.102.2via llm-release-walk
1 file changed · +2 2
  • packages/ckeditor5-mermaid/src/mermaidediting.ts+2 2 modified
    @@ -183,7 +183,7 @@ export default class MermaidEditing extends Plugin {
     			const mermaidSource = data.item.getAttribute( 'source' ) as string;
     			const domElement = this.toDomElement( domDocument );
     
    -			domElement.innerHTML = mermaidSource;
    +			domElement.textContent = mermaidSource;
     
     			window.setTimeout( () => {
     				// @todo: by the looks of it the domElement needs to be hooked to tree in order to allow for rendering.
    @@ -219,7 +219,7 @@ export default class MermaidEditing extends Plugin {
     					const domPreviewWrapper = domConverter.viewToDom(child);
     
     					if ( domPreviewWrapper ) {
    -						domPreviewWrapper.innerHTML = newSource;
    +						domPreviewWrapper.textContent = newSource;
     						domPreviewWrapper.removeAttribute( 'data-processed' );
     
     						this._renderMermaid( domPreviewWrapper );
    
626aca518188

fix(client): toasts could render HTML content

https://github.com/triliumnext/triliumElian DoranApr 4, 2026Fixed in 0.102.2via llm-release-walk
1 file changed · +1 2
  • apps/client/src/widgets/Toast.tsx+1 2 modified
    @@ -5,7 +5,6 @@ import { useEffect } from "preact/hooks";
     
     import { removeToastFromStore, ToastOptionsWithRequiredId, toasts } from "../services/toast";
     import Icon from "./react/Icon";
    -import { RawHtmlBlock } from "./react/RawHtml";
     import Button from "./react/Button";
     
     export default function ToastContainer() {
    @@ -54,7 +53,7 @@ function Toast({ id, title, timeout, progress, message, icon, buttons }: ToastOp
                     <div class="toast-icon">{toastIcon}</div>
                 )}
     
    -            <RawHtmlBlock className="toast-body" html={message} />
    +            <div className="toast-body">{message}</div>
     
                 {!title && <div class="toast-header">{closeButton}</div>}
     
    
ed3b86cd49f6

fix(import): no longer preserve named note IDs

https://github.com/triliumnext/triliumElian DoranApr 4, 2026Fixed in 0.102.2via llm-release-walk
1 file changed · +1 2
  • apps/server/src/services/import/zip.ts+1 2 modified
    @@ -51,8 +51,7 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
                 return "empty_note_id";
             }
     
    -        if (origNoteId === "root" || origNoteId.startsWith("_") || opts?.preserveIds) {
    -            // these "named" noteIds don't differ between Trilium instances
    +        if (origNoteId === "root" || opts?.preserveIds) {
                 return origNoteId;
             }
     
    
b3716754947b

chore(commons): mark docName as a dangerous attribute

https://github.com/triliumnext/triliumElian DoranApr 4, 2026Fixed in 0.102.2via llm-release-walk
1 file changed · +1 0
  • packages/commons/src/lib/builtin_attributes.ts+1 0 modified
    @@ -81,6 +81,7 @@ export default [
         { type: "label", name: "webViewSrc", isDangerous: true },
         { type: "label", name: "hideHighlightWidget" },
         { type: "label", name: "iconPack", isDangerous: true },
    +    { type: "label", name: "docName", isDangerous: true },
     
         { type: "label", name: "printLandscape" },
         { type: "label", name: "printPageSize" },
    
ff06c8e7bd42

fix(client): validate docName attribute in doc renderer

https://github.com/triliumnext/triliumElian DoranApr 4, 2026Fixed in 0.102.2via llm-release-walk
2 files changed · +51 0
  • apps/client/src/services/doc_renderer.spec.ts+30 0 added
    @@ -0,0 +1,30 @@
    +import { describe, expect, it } from "vitest";
    +
    +import { isValidDocName } from "./doc_renderer.js";
    +
    +describe("isValidDocName", () => {
    +    it("accepts valid docNames", () => {
    +        expect(isValidDocName("launchbar_intro")).toBe(true);
    +        expect(isValidDocName("User Guide/Quick Start")).toBe(true);
    +        expect(isValidDocName("User Guide/User Guide/Quick Start")).toBe(true);
    +        expect(isValidDocName("Quick Start Guide")).toBe(true);
    +        expect(isValidDocName("quick_start_guide")).toBe(true);
    +        expect(isValidDocName("quick-start-guide")).toBe(true);
    +    });
    +
    +    it("rejects path traversal attacks", () => {
    +        expect(isValidDocName("..")).toBe(false);
    +        expect(isValidDocName("../etc/passwd")).toBe(false);
    +        expect(isValidDocName("foo/../bar")).toBe(false);
    +        expect(isValidDocName("../../../../api/notes/_malicious/open")).toBe(false);
    +        expect(isValidDocName("..\\etc\\passwd")).toBe(false);
    +        expect(isValidDocName("foo\\bar")).toBe(false);
    +    });
    +
    +    it("rejects URL manipulation attacks", () => {
    +        expect(isValidDocName("../../../../api/notes/_malicious/open?x=")).toBe(false);
    +        expect(isValidDocName("foo#bar")).toBe(false);
    +        expect(isValidDocName("%2e%2e")).toBe(false);
    +        expect(isValidDocName("%2e%2e%2f%2e%2e%2fapi")).toBe(false);
    +    });
    +});
    
  • apps/client/src/services/doc_renderer.ts+21 0 modified
    @@ -3,6 +3,22 @@ import { applyReferenceLinks } from "../widgets/type_widgets/text/read_only_help
     import { getCurrentLanguage } from "./i18n.js";
     import { formatCodeBlocks } from "./syntax_highlight.js";
     
    +/**
    + * Validates a docName to prevent path traversal attacks.
    + * Allows forward slashes for subdirectories (e.g., "User Guide/Quick Start")
    + * but blocks traversal sequences and URL manipulation characters.
    + */
    +export function isValidDocName(docName: string): boolean {
    +    if (docName.includes("..") ||
    +        docName.includes("\\") ||
    +        docName.includes("?") ||
    +        docName.includes("#") ||
    +        docName.includes("%")) {
    +        return false;
    +    }
    +    return true;
    +}
    +
     export default function renderDoc(note: FNote) {
         return new Promise<JQuery<HTMLElement>>((resolve) => {
             let docName = note.getLabelValue("docName");
    @@ -49,6 +65,11 @@ async function processContent(url: string, $content: JQuery<HTMLElement>) {
     }
     
     function getUrl(docNameValue: string, language: string) {
    +    if (!isValidDocName(docNameValue)) {
    +        console.error(`Invalid docName: ${docNameValue}`);
    +        return "";
    +    }
    +
         // Cannot have spaces in the URL due to how JQuery.load works.
         docNameValue = docNameValue.replaceAll(" ", "%20");
     
    
5c46209ddca3

feat(server): improve request handling for SVGs

https://github.com/triliumnext/triliumElian DoranApr 5, 2026Fixed in 0.102.2via llm-release-walk
3 files changed · +46 27
  • apps/server/src/routes/api/image.ts+28 10 modified
    @@ -1,12 +1,14 @@
    -"use strict";
     
    -import imageService from "../../services/image.js";
    -import becca from "../../becca/becca.js";
    -import fs from "fs";
    +
     import type { Request, Response } from "express";
    +import fs from "fs";
    +
    +import becca from "../../becca/becca.js";
     import type BNote from "../../becca/entities/bnote.js";
     import type BRevision from "../../becca/entities/brevision.js";
    +import imageService from "../../services/image.js";
     import { RESOURCE_DIR } from "../../services/resource_dir.js";
    +import { sanitizeSvg } from "../../services/utils.js";
     
     function returnImageFromNote(req: Request, res: Response) {
         const image = becca.getNote(req.params.noteId);
    @@ -37,28 +39,33 @@ function returnImageInt(image: BNote | BRevision | null, res: Response) {
         } else {
             res.set("Content-Type", image.mime);
             res.set("Cache-Control", "no-cache, no-store, must-revalidate");
    -        res.send(image.getContent());
    +
    +        if (image.mime === "image/svg+xml") {
    +            sendSanitizedSvg(res, image.getContent());
    +        } else {
    +            res.send(image.getContent());
    +        }
         }
     }
     
     export function renderSvgAttachment(image: BNote | BRevision, res: Response, attachmentName: string) {
    -    let svg: string | Buffer = `<svg xmlns="http://www.w3.org/2000/svg"></svg>`;
    +    let svgContent: string | Buffer = `<svg xmlns="http://www.w3.org/2000/svg"></svg>`;
         const attachment = image.getAttachmentByTitle(attachmentName);
     
         if (attachment) {
    -        svg = attachment.getContent();
    +        svgContent = attachment.getContent();
         } else {
             // backwards compatibility, before attachments, the SVG was stored in the main note content as a separate key
             const contentSvg = image.getJsonContentSafely()?.svg;
     
             if (contentSvg) {
    -            svg = contentSvg;
    +            svgContent = contentSvg;
             }
         }
     
         res.set("Content-Type", "image/svg+xml");
         res.set("Cache-Control", "no-cache, no-store, must-revalidate");
    -    res.send(svg);
    +    sendSanitizedSvg(res, svgContent);
     }
     
     function returnAttachedImage(req: Request, res: Response) {
    @@ -75,7 +82,12 @@ function returnAttachedImage(req: Request, res: Response) {
     
         res.set("Content-Type", attachment.mime);
         res.set("Cache-Control", "no-cache, no-store, must-revalidate");
    -    res.send(attachment.getContent());
    +
    +    if (attachment.mime === "image/svg+xml") {
    +        sendSanitizedSvg(res, attachment.getContent());
    +    } else {
    +        res.send(attachment.getContent());
    +    }
     }
     
     function updateImage(req: Request) {
    @@ -116,3 +128,9 @@ export default {
         returnAttachedImage,
         updateImage
     };
    +
    +function sendSanitizedSvg(res: Response, content: string | Buffer) {
    +    const svgString = typeof content === "string" ? content : content.toString("utf-8");
    +    res.set("Content-Security-Policy", "script-src 'none'");
    +    res.send(sanitizeSvg(svgString));
    +}
    
  • apps/server/src/services/utils.ts+17 0 modified
    @@ -119,6 +119,22 @@ export function sanitizeSqlIdentifier(str: string) {
         return str.replace(/[^A-Za-z0-9_]/g, "");
     }
     
    +/**
    + * Sanitize SVG to remove potentially dangerous elements and attributes.
    + * This prevents XSS via script injection in SVG content.
    + */
    +export function sanitizeSvg(svg: string): string {
    +    return svg
    +        // Remove script elements
    +        .replace(/<script[\s\S]*?<\/script>/gi, '')
    +        // Remove on* event handlers (onclick, onload, onerror, etc.)
    +        .replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '')
    +        .replace(/\s+on\w+\s*=\s*[^\s>]+/gi, '')
    +        // Remove javascript: URLs
    +        .replace(/href\s*=\s*["']javascript:[^"']*["']/gi, 'href="#"')
    +        .replace(/xlink:href\s*=\s*["']javascript:[^"']*["']/gi, 'xlink:href="#"');
    +}
    +
     export const escapeHtml = escape;
     
     export const unescapeHtml = unescape;
    @@ -556,6 +572,7 @@ export default {
         replaceAll,
         safeExtractMessageAndStackFromError,
         sanitizeSqlIdentifier,
    +    sanitizeSvg,
         stripTags,
         slugify,
         timeLimit,
    
  • apps/server/src/share/routes.ts+1 17 modified
    @@ -9,30 +9,14 @@ import SearchContext from "../services/search/search_context.js";
     import type SNote from "./shaca/entities/snote.js";
     import type SAttachment from "./shaca/entities/sattachment.js";
     import { getDefaultTemplatePath, renderNoteContent } from "./content_renderer.js";
    -import utils from "../services/utils.js";
    +import utils, { sanitizeSvg } from "../services/utils.js";
     
     function addNoIndexHeader(note: SNote, res: Response) {
         if (note.isLabelTruthy("shareDisallowRobotIndexing")) {
             res.setHeader("X-Robots-Tag", "noindex");
         }
     }
     
    -/**
    - * Sanitize SVG to remove potentially dangerous elements and attributes.
    - * This prevents XSS via script injection in SVG exports.
    - */
    -function sanitizeSvg(svg: string): string {
    -    return svg
    -        // Remove script elements
    -        .replace(/<script[\s\S]*?<\/script>/gi, '')
    -        // Remove on* event handlers (onclick, onload, onerror, etc.)
    -        .replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '')
    -        .replace(/\s+on\w+\s*=\s*[^\s>]+/gi, '')
    -        // Remove javascript: URLs
    -        .replace(/href\s*=\s*["']javascript:[^"']*["']/gi, 'href="#"')
    -        .replace(/xlink:href\s*=\s*["']javascript:[^"']*["']/gi, 'xlink:href="#"');
    -}
    -
     function requestCredentials(res: Response) {
         res.setHeader("WWW-Authenticate", 'Basic realm="User Visible Realm", charset="UTF-8"').sendStatus(401);
     }
    
7f199c527ba8

feat(share): improve request handling for SVGs

https://github.com/triliumnext/triliumElian DoranApr 5, 2026Fixed in 0.102.2via llm-release-walk
1 file changed · +18 1
  • apps/server/src/share/routes.ts+18 1 modified
    @@ -17,6 +17,22 @@ function addNoIndexHeader(note: SNote, res: Response) {
         }
     }
     
    +/**
    + * Sanitize SVG to remove potentially dangerous elements and attributes.
    + * This prevents XSS via script injection in SVG exports.
    + */
    +function sanitizeSvg(svg: string): string {
    +    return svg
    +        // Remove script elements
    +        .replace(/<script[\s\S]*?<\/script>/gi, '')
    +        // Remove on* event handlers (onclick, onload, onerror, etc.)
    +        .replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '')
    +        .replace(/\s+on\w+\s*=\s*[^\s>]+/gi, '')
    +        // Remove javascript: URLs
    +        .replace(/href\s*=\s*["']javascript:[^"']*["']/gi, 'href="#"')
    +        .replace(/xlink:href\s*=\s*["']javascript:[^"']*["']/gi, 'xlink:href="#"');
    +}
    +
     function requestCredentials(res: Response) {
         res.setHeader("WWW-Authenticate", 'Basic realm="User Visible Realm", charset="UTF-8"').sendStatus(401);
     }
    @@ -102,9 +118,10 @@ function renderImageAttachment(image: SNote, res: Response, attachmentName: stri
             }
         }
     
    -    const svg = svgString;
    +    const svg = sanitizeSvg(svgString);
         res.set("Content-Type", "image/svg+xml");
         res.set("Cache-Control", "no-cache, no-store, must-revalidate");
    +    res.set("Content-Security-Policy", "script-src 'none'");
         res.send(svg);
     }
     
    
2432e230c5d4

chore(etapi): enforce MIME for image upload

https://github.com/triliumnext/triliumElian DoranApr 5, 2026Fixed in 0.102.2via llm-release-walk
1 file changed · +12 0
  • apps/server/src/etapi/notes.ts+12 0 modified
    @@ -66,6 +66,11 @@ function register(router: Router) {
             eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_NOTE);
             const params = _params as NoteParams;
     
    +        // Validate MIME type for image notes
    +        if (params.type === "image" && params.mime && !params.mime.startsWith("image/")) {
    +            throw new eu.EtapiError(400, "INVALID_MIME_FOR_IMAGE", `MIME type '${params.mime}' is not allowed for image notes. MIME must start with 'image/'.`);
    +        }
    +
             try {
                 const resp = noteService.createNewNote(params);
     
    @@ -93,6 +98,13 @@ function register(router: Router) {
                 throw new eu.EtapiError(400, "NOTE_IS_PROTECTED", `Note '${req.params.noteId}' is protected and cannot be modified through ETAPI.`);
             }
     
    +        // Validate MIME type for image notes (check both current and new type/mime)
    +        const effectiveType = req.body.type || note.type;
    +        const effectiveMime = req.body.mime || note.mime;
    +        if (effectiveType === "image" && effectiveMime && !effectiveMime.startsWith("image/")) {
    +            throw new eu.EtapiError(400, "INVALID_MIME_FOR_IMAGE", `MIME type '${effectiveMime}' is not allowed for image notes. MIME must start with 'image/'.`);
    +        }
    +
             noteService.saveRevisionIfNeeded(note);
             eu.validateAndPatch(note, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
             note.save();
    
8ff41d8fa924

fix(server): align attachment upload validation with note upload

https://github.com/triliumnext/triliumElian DoranApr 4, 2026Fixed in 0.102.2via llm-release-walk
1 file changed · +4 0
  • apps/server/src/routes/api/files.ts+4 0 modified
    @@ -232,6 +232,10 @@ function uploadModifiedFileToAttachment(req: Request) {
         const { attachmentId } = req.params;
         const { filePath } = req.body;
     
    +    if (!createdTemporaryFiles.has(filePath)) {
    +        throw new ValidationError(`File '${filePath}' is not a temporary file.`);
    +    }
    +
         const attachment = becca.getAttachmentOrThrow(attachmentId);
     
         log.info(`Updating attachment '${attachmentId}' with content from '${filePath}'`);
    

Vulnerability mechanics

Root cause

"Missing input validation on the docName label value allows path traversal out of the static doc_notes directory, combined with jQuery's .load() parsing arbitrary responses as HTML, leading to XSS and RCE in the Electron renderer."

Attack vector

An attacker crafts a malicious ZIP archive containing two notes: a payload note (type: code, mime: text/plain) with raw HTML/JS, and a trigger note (type: doc or launcher) with a `#docName` label containing `../` path traversal sequences pointing to the payload note's API endpoint. When a victim imports the archive with safe import enabled, the doc renderer constructs a URL like `../../../../api/notes/_payload/open?x=` which traverses out of the static doc_notes directory and hits the `/api/notes/:noteId/open` route. jQuery's `.load()` fetches the response and parses it as HTML regardless of Content-Type, inserting the payload into the DOM. Because the Electron renderer has `nodeIntegration` enabled, the injected HTML/JS achieves remote code execution [ref_id=1].

Affected code

The vulnerability resides in `apps/client/src/services/doc_renderer.ts` where the `getUrl()` function concatenates the `docName` label value into a URL without validation, and in `apps/server/src/services/builtin_attributes.ts` where `docName` was not marked as a dangerous attribute. The fix adds `isValidDocName()` validation in the doc renderer and marks `docName` as dangerous in the builtin attributes list [patch_id=3104636][patch_id=3104637].

What the fix does

The fix adds an `isValidDocName()` function in `apps/client/src/services/doc_renderer.ts` that rejects docName values containing `..`, backslashes, `?`, `#`, or `%` characters, preventing path traversal and URL manipulation [patch_id=3104637]. Additionally, `docName` is marked as `isDangerous: true` in the builtin attributes list, so safe import will disable the attribute on imported notes [patch_id=3104636]. A separate commit removes the `RawHtmlBlock` component from toast rendering to prevent HTML injection in toast messages [patch_id=3104635].

Preconditions

  • inputVictim must import a malicious ZIP archive into Trilium Notes
  • configSafe import must be enabled (the vulnerability bypasses its sanitization)
  • configDesktop client Electron renderer must have nodeIntegration enabled (default)

Generated on May 29, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

1

News mentions

0

No linked articles in our index yet.