From 8ff41d8fa924686f039f8ab7bcf3b2cc5deb5b8e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 4 Apr 2026 20:46:03 +0300 Subject: [PATCH 01/22] fix(server): align attachment upload validation with note upload --- apps/server/src/routes/api/files.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/server/src/routes/api/files.ts b/apps/server/src/routes/api/files.ts index 4a6e17382b..078feed170 100644 --- a/apps/server/src/routes/api/files.ts +++ b/apps/server/src/routes/api/files.ts @@ -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}'`); From ff06c8e7bd428840a0b6c02adc54ec56c64b9810 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 4 Apr 2026 21:21:50 +0300 Subject: [PATCH 02/22] fix(client): validate docName attribute in doc renderer --- apps/client/src/services/doc_renderer.spec.ts | 30 +++++++++++++++++++ apps/client/src/services/doc_renderer.ts | 21 +++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 apps/client/src/services/doc_renderer.spec.ts diff --git a/apps/client/src/services/doc_renderer.spec.ts b/apps/client/src/services/doc_renderer.spec.ts new file mode 100644 index 0000000000..c36e3e80b9 --- /dev/null +++ b/apps/client/src/services/doc_renderer.spec.ts @@ -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); + }); +}); diff --git a/apps/client/src/services/doc_renderer.ts b/apps/client/src/services/doc_renderer.ts index 1ae60fb9ca..d2b2140847 100644 --- a/apps/client/src/services/doc_renderer.ts +++ b/apps/client/src/services/doc_renderer.ts @@ -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>((resolve) => { let docName = note.getLabelValue("docName"); @@ -49,6 +65,11 @@ async function processContent(url: string, $content: JQuery) { } 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"); From b3716754947bc26d9fbf5a5fee2b218501db9f5f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 4 Apr 2026 21:25:05 +0300 Subject: [PATCH 03/22] chore(commons): mark docName as a dangerous attribute --- packages/commons/src/lib/builtin_attributes.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/commons/src/lib/builtin_attributes.ts b/packages/commons/src/lib/builtin_attributes.ts index 0f509c0d25..2172c6fa50 100644 --- a/packages/commons/src/lib/builtin_attributes.ts +++ b/packages/commons/src/lib/builtin_attributes.ts @@ -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" }, From ed3b86cd49f6722c92eef802ded07e993bf34882 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 4 Apr 2026 21:27:37 +0300 Subject: [PATCH 04/22] fix(import): no longer preserve named note IDs --- apps/server/src/services/import/zip.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/server/src/services/import/zip.ts b/apps/server/src/services/import/zip.ts index 2d2251cb31..9c7cd8aefc 100644 --- a/apps/server/src/services/import/zip.ts +++ b/apps/server/src/services/import/zip.ts @@ -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; } From 8204322b465c7e1a0673e8baaa4eea4541b50b2b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 4 Apr 2026 22:02:33 +0300 Subject: [PATCH 05/22] fix(openid): use more secure RNG --- apps/server/src/services/open_id.ts | 33 +++++++++++++++-------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/apps/server/src/services/open_id.ts b/apps/server/src/services/open_id.ts index 2ae3bbe1ef..1f2de28ba0 100644 --- a/apps/server/src/services/open_id.ts +++ b/apps/server/src/services/open_id.ts @@ -1,14 +1,15 @@ +import crypto from "crypto"; import type { NextFunction, Request, Response } from "express"; -import openIDEncryption from "./encryption/open_id_encryption.js"; -import sqlInit from "./sql_init.js"; -import options from "./options.js"; import type { Session } from "express-openid-connect"; -import sql from "./sql.js"; -import config from "./config.js"; +import config from "./config.js"; +import openIDEncryption from "./encryption/open_id_encryption.js"; +import options from "./options.js"; +import sql from "./sql.js"; +import sqlInit from "./sql_init.js"; function checkOpenIDConfig() { - const missingVars: string[] = [] + const missingVars: string[] = []; if (config.MultiFactorAuthentication.oauthBaseUrl === "") { missingVars.push("oauthBaseUrl"); } @@ -27,7 +28,7 @@ function isOpenIDEnabled() { function isUserSaved() { const data = sql.getValue("SELECT isSetup FROM user_data;"); - return data === "true" ? true : false; + return data === "true"; } function getUsername() { @@ -80,13 +81,13 @@ function isTokenValid(req: Request, res: Response, next: NextFunction) { }; }); return result; - } else { - return { - success: false, - message: "Token not set up", - user: userStatus, - }; - } + } + return { + success: false, + message: "Token not set up", + user: userStatus, + }; + } function getSSOIssuerName() { @@ -121,11 +122,11 @@ function generateOAuthConfig() { scope: "openid profile email", access_type: "offline", prompt: "consent", - state: "random_state_" + Math.random().toString(36).substring(2) + state: crypto.randomUUID() }, routes: authRoutes, idpLogout: true, - logoutParams: logoutParams, + logoutParams, afterCallback: async (req: Request, res: Response, session: Session) => { if (!sqlInit.isDbInitialized()) return session; From 626aca518188cfa182470f1a6b96c522c88edf21 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 4 Apr 2026 22:21:25 +0300 Subject: [PATCH 06/22] fix(client): toasts could render HTML content --- apps/client/src/widgets/Toast.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/client/src/widgets/Toast.tsx b/apps/client/src/widgets/Toast.tsx index 9630836e1b..f0321345a2 100644 --- a/apps/client/src/widgets/Toast.tsx +++ b/apps/client/src/widgets/Toast.tsx @@ -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
{toastIcon}
)} - +
{message}
{!title &&
{closeButton}
} From fc1be0d23db5ecd9b6669f22184bf1d900274640 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 5 Apr 2026 10:17:16 +0300 Subject: [PATCH 07/22] fix(ckeditor5-mermaid): use textContent for diagram source rendering --- packages/ckeditor5-mermaid/src/mermaidediting.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ckeditor5-mermaid/src/mermaidediting.ts b/packages/ckeditor5-mermaid/src/mermaidediting.ts index 05d7af480b..1c8470c0f4 100644 --- a/packages/ckeditor5-mermaid/src/mermaidediting.ts +++ b/packages/ckeditor5-mermaid/src/mermaidediting.ts @@ -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 ); From 2432e230c5d4904c8d867d696d57366ff9e71cc5 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 5 Apr 2026 10:44:47 +0300 Subject: [PATCH 08/22] chore(etapi): enforce MIME for image upload --- apps/server/src/etapi/notes.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/server/src/etapi/notes.ts b/apps/server/src/etapi/notes.ts index 9fae830704..d85f128240 100644 --- a/apps/server/src/etapi/notes.ts +++ b/apps/server/src/etapi/notes.ts @@ -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(); From 7f199c527ba8c374cd5c5939c978a9d547222ef0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 5 Apr 2026 10:52:36 +0300 Subject: [PATCH 09/22] feat(share): improve request handling for SVGs --- apps/server/src/share/routes.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/server/src/share/routes.ts b/apps/server/src/share/routes.ts index 80544fd998..f164b2bf4b 100644 --- a/apps/server/src/share/routes.ts +++ b/apps/server/src/share/routes.ts @@ -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(//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); } From 176de87b6b451835c5105c701f228405dad5fc5c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 5 Apr 2026 11:01:22 +0300 Subject: [PATCH 10/22] feat(desktop): add Electron fuses --- apps/desktop/electron-forge/forge.config.ts | 12 ++++ apps/desktop/package.json | 16 ++--- pnpm-lock.yaml | 68 +++++++++++++-------- 3 files changed, 63 insertions(+), 33 deletions(-) diff --git a/apps/desktop/electron-forge/forge.config.ts b/apps/desktop/electron-forge/forge.config.ts index 03d5a4e857..491a349dd0 100644 --- a/apps/desktop/electron-forge/forge.config.ts +++ b/apps/desktop/electron-forge/forge.config.ts @@ -1,4 +1,5 @@ import type { ForgeConfig } from "@electron-forge/shared-types"; +import { FuseV1Options, FuseVersion } from "@electron/fuses"; import { LOCALES } from "@triliumnext/commons"; import { existsSync } from "fs"; import fs from "fs-extra"; @@ -166,6 +167,17 @@ const config: ForgeConfig = { { name: "@electron-forge/plugin-auto-unpack-natives", config: {} + }, + { + name: "@electron-forge/plugin-fuses", + config: { + version: FuseVersion.V1, + [FuseV1Options.RunAsNode]: false, + [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false, + [FuseV1Options.EnableNodeCliInspectArguments]: false, + [FuseV1Options.EnableCookieEncryption]: true, + [FuseV1Options.OnlyLoadAppFromAsar]: true + } } ], hooks: { diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 5d6d2b9f46..a31b742df5 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -27,15 +27,10 @@ "electron-debug": "4.1.0", "electron-dl": "4.0.0", "electron-squirrel-startup": "1.0.1", - "jquery.fancytree": "2.38.5", - "jquery-hotkeys": "0.2.2" + "jquery-hotkeys": "0.2.2", + "jquery.fancytree": "2.38.5" }, "devDependencies": { - "@types/electron-squirrel-startup": "1.0.2", - "@triliumnext/commons": "workspace:*", - "@triliumnext/server": "workspace:*", - "copy-webpack-plugin": "13.0.1", - "electron": "40.6.1", "@electron-forge/cli": "7.11.1", "@electron-forge/maker-deb": "7.11.1", "@electron-forge/maker-dmg": "7.11.1", @@ -44,6 +39,13 @@ "@electron-forge/maker-squirrel": "7.11.1", "@electron-forge/maker-zip": "7.11.1", "@electron-forge/plugin-auto-unpack-natives": "7.11.1", + "@electron-forge/plugin-fuses": "7.11.1", + "@electron/fuses": "2.1.1", + "@triliumnext/commons": "workspace:*", + "@triliumnext/server": "workspace:*", + "@types/electron-squirrel-startup": "1.0.2", + "copy-webpack-plugin": "13.0.1", + "electron": "40.6.1", "prebuild-install": "7.1.3" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c5059d249..94ff3064a2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -445,6 +445,12 @@ importers: '@electron-forge/plugin-auto-unpack-natives': specifier: 7.11.1 version: 7.11.1 + '@electron-forge/plugin-fuses': + specifier: 7.11.1 + version: 7.11.1(@electron/fuses@2.1.1) + '@electron/fuses': + specifier: 2.1.1 + version: 2.1.1 '@triliumnext/commons': specifier: workspace:* version: link:../../packages/commons @@ -2345,6 +2351,12 @@ packages: resolution: {integrity: sha512-lKpSOV1GA3FoYiD9k05i6v4KaQVmojnRgCr7d6VL1bFp13QOtXSaAWhFI9mtSY7rGElOacX6Zt7P7rPoB8T9eQ==} engines: {node: '>= 16.4.0'} + '@electron-forge/plugin-fuses@7.11.1': + resolution: {integrity: sha512-Td517mHf+RjQAayFDM2kKb7NaGdRXrZfPbc7KOHlGbXthp5YTkFu2cCZGWokiqt1y1wsFaAodULhqBIg7vbbbw==} + engines: {node: '>= 16.4.0'} + peerDependencies: + '@electron/fuses': ^1.0.0 + '@electron-forge/publisher-base@7.11.1': resolution: {integrity: sha512-rXE9oMFGMtdQrixnumWYH5TTGsp99iPHZb3jI74YWq518ctCh6DlIgWlhf6ok2X0+lhWovcIb45KJucUFAQ13w==} engines: {node: '>= 16.4.0'} @@ -2382,6 +2394,11 @@ packages: engines: {node: '>=10.12.0'} hasBin: true + '@electron/fuses@2.1.1': + resolution: {integrity: sha512-38ho27/mtUV/LpsZ1LCDJUomKBBSUZDk/qBH4FNNtoN5fmnkmWDcIp5pm1Kv3InqhRjKZKs7Jzx+wWZNMArHrA==} + engines: {node: '>=22.12.0'} + hasBin: true + '@electron/get@2.0.3': resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} engines: {node: '>=12'} @@ -16058,6 +16075,8 @@ snapshots: '@ckeditor/ckeditor5-core': 47.4.0 '@ckeditor/ckeditor5-upload': 47.4.0 ckeditor5: 47.4.0 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-ai@47.4.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)': dependencies: @@ -16198,12 +16217,16 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.4.0 '@ckeditor/ckeditor5-widget': 47.4.0 es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-cloud-services@47.4.0': dependencies: '@ckeditor/ckeditor5-core': 47.4.0 '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-code-block@47.4.0(patch_hash=2361d8caad7d6b5bddacc3a3b4aa37dbfba260b1c1b22a450413a79c1bb1ce95)': dependencies: @@ -16396,6 +16419,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-editor-classic@47.4.0': dependencies: @@ -16405,6 +16430,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-editor-decoupled@47.4.0': dependencies: @@ -16414,6 +16441,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-editor-inline@47.4.0': dependencies: @@ -16447,8 +16476,6 @@ snapshots: '@ckeditor/ckeditor5-table': 47.4.0 '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-emoji@47.4.0': dependencies: @@ -16505,8 +16532,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.4.0 '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-export-word@47.4.0': dependencies: @@ -16531,6 +16556,8 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 es-toolkit: 1.39.5 + transitivePeerDependencies: + - supports-color '@ckeditor/ckeditor5-font@47.4.0': dependencies: @@ -16666,8 +16693,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.4.0 '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-indent@47.4.0': dependencies: @@ -16791,8 +16816,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-merge-fields@47.4.0': dependencies: @@ -16805,8 +16828,6 @@ snapshots: '@ckeditor/ckeditor5-widget': 47.4.0 ckeditor5: 47.4.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-minimap@47.4.0': dependencies: @@ -16815,8 +16836,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.4.0 '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-operations-compressor@47.4.0': dependencies: @@ -16871,8 +16890,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.4.0 '@ckeditor/ckeditor5-widget': 47.4.0 ckeditor5: 47.4.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-pagination@47.4.0': dependencies: @@ -16992,8 +17009,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.4.0 '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-source-editing-enhanced@47.4.0': dependencies: @@ -17041,8 +17056,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-table@47.4.0': dependencies: @@ -17055,8 +17068,6 @@ snapshots: '@ckeditor/ckeditor5-widget': 47.4.0 ckeditor5: 47.4.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-template@47.4.0': dependencies: @@ -17131,8 +17142,6 @@ snapshots: '@ckeditor/ckeditor5-icons': 47.4.0 '@ckeditor/ckeditor5-ui': 47.4.0 '@ckeditor/ckeditor5-utils': 47.4.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-upload@47.4.0': dependencies: @@ -17169,8 +17178,6 @@ snapshots: '@ckeditor/ckeditor5-engine': 47.4.0 '@ckeditor/ckeditor5-utils': 47.4.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-widget@47.4.0': dependencies: @@ -17190,8 +17197,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@codemirror/autocomplete@6.18.6': dependencies: @@ -17615,6 +17620,15 @@ snapshots: - bluebird - supports-color + '@electron-forge/plugin-fuses@7.11.1(@electron/fuses@2.1.1)': + dependencies: + '@electron-forge/plugin-base': 7.11.1 + '@electron-forge/shared-types': 7.11.1 + '@electron/fuses': 2.1.1 + transitivePeerDependencies: + - bluebird + - supports-color + '@electron-forge/publisher-base@7.11.1': dependencies: '@electron-forge/shared-types': 7.11.1 @@ -17697,6 +17711,8 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 + '@electron/fuses@2.1.1': {} + '@electron/get@2.0.3': dependencies: debug: 4.4.3(supports-color@8.1.1) From 5c46209ddca3d1b986df4eebc69f4c209271a9b4 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 5 Apr 2026 11:28:28 +0300 Subject: [PATCH 11/22] feat(server): improve request handling for SVGs --- apps/server/src/routes/api/image.ts | 38 +++++++++++++++++++++-------- apps/server/src/services/utils.ts | 17 +++++++++++++ apps/server/src/share/routes.ts | 18 +------------- 3 files changed, 46 insertions(+), 27 deletions(-) diff --git a/apps/server/src/routes/api/image.ts b/apps/server/src/routes/api/image.ts index aa24e7aae9..988921bdfd 100644 --- a/apps/server/src/routes/api/image.ts +++ b/apps/server/src/routes/api/image.ts @@ -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 = ``; + let svgContent: string | Buffer = ``; 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)); +} diff --git a/apps/server/src/services/utils.ts b/apps/server/src/services/utils.ts index a2b707edf9..4acea02bdd 100644 --- a/apps/server/src/services/utils.ts +++ b/apps/server/src/services/utils.ts @@ -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(//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, diff --git a/apps/server/src/share/routes.ts b/apps/server/src/share/routes.ts index f164b2bf4b..41e57120c0 100644 --- a/apps/server/src/share/routes.ts +++ b/apps/server/src/share/routes.ts @@ -9,7 +9,7 @@ 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")) { @@ -17,22 +17,6 @@ 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(//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); } From 90822cc8a372d4a9e363a7bc44c2ff8247ea457b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 5 Apr 2026 11:59:45 +0300 Subject: [PATCH 12/22] chore: address requested changes --- apps/client/src/services/doc_renderer.ts | 32 +++++++++++-------- apps/server/src/services/open_id.ts | 39 +++++++++++------------- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/apps/client/src/services/doc_renderer.ts b/apps/client/src/services/doc_renderer.ts index d2b2140847..74ccfa8447 100644 --- a/apps/client/src/services/doc_renderer.ts +++ b/apps/client/src/services/doc_renderer.ts @@ -21,20 +21,26 @@ export function isValidDocName(docName: string): boolean { export default function renderDoc(note: FNote) { return new Promise>((resolve) => { - let docName = note.getLabelValue("docName"); + const docName = note.getLabelValue("docName"); const $content = $("
"); - if (docName) { - // find doc based on language - const url = getUrl(docName, getCurrentLanguage()); + // find doc based on language + const url = getUrl(docName, getCurrentLanguage()); + + if (url) { $content.load(url, async (response, status) => { // fallback to english doc if no translation available if (status === "error") { const fallbackUrl = getUrl(docName, "en"); - $content.load(fallbackUrl, async () => { - await processContent(fallbackUrl, $content) + + if (fallbackUrl) { + $content.load(fallbackUrl, async () => { + await processContent(fallbackUrl, $content); + resolve($content); + }); + } else { resolve($content); - }); + } return; } @@ -44,8 +50,6 @@ export default function renderDoc(note: FNote) { } else { resolve($content); } - - return $content; }); } @@ -55,7 +59,7 @@ async function processContent(url: string, $content: JQuery) { // Images are relative to the docnote but that will not work when rendered in the application since the path breaks. $content.find("img").each((i, el) => { const $img = $(el); - $img.attr("src", dir + "/" + $img.attr("src")); + $img.attr("src", `${dir }/${ $img.attr("src")}`); }); formatCodeBlocks($content); @@ -64,15 +68,17 @@ async function processContent(url: string, $content: JQuery) { await applyReferenceLinks($content[0]); } -function getUrl(docNameValue: string, language: string) { +function getUrl(docNameValue: string | null, language: string) { + if (!docNameValue) return; + if (!isValidDocName(docNameValue)) { console.error(`Invalid docName: ${docNameValue}`); - return ""; + return null; } // Cannot have spaces in the URL due to how JQuery.load works. docNameValue = docNameValue.replaceAll(" ", "%20"); - const basePath = window.glob.isDev ? window.glob.assetPath + "/.." : window.glob.assetPath; + const basePath = window.glob.isDev ? `${window.glob.assetPath }/..` : window.glob.assetPath; return `${basePath}/doc_notes/${language}/${docNameValue}.html`; } diff --git a/apps/server/src/services/open_id.ts b/apps/server/src/services/open_id.ts index 1f2de28ba0..a6593e1bb2 100644 --- a/apps/server/src/services/open_id.ts +++ b/apps/server/src/services/open_id.ts @@ -1,4 +1,3 @@ -import crypto from "crypto"; import type { NextFunction, Request, Response } from "express"; import type { Session } from "express-openid-connect"; @@ -60,34 +59,31 @@ function getOAuthStatus() { }; } -function isTokenValid(req: Request, res: Response, next: NextFunction) { +async function isTokenValid(req: Request, res: Response, next: NextFunction) { const userStatus = openIDEncryption.isSubjectIdentifierSaved(); if (req.oidc !== undefined) { - const result = req.oidc - .fetchUserInfo() - .then((result) => { - return { - success: true, - message: "Token is valid", - user: userStatus, - }; - }) - .catch((result) => { - return { - success: false, - message: "Token is not valid", - user: userStatus, - }; - }); - return result; - } + try { + await req.oidc.fetchUserInfo(); + return { + success: true, + message: "Token is valid", + user: userStatus, + }; + } catch { + return { + success: false, + message: "Token is not valid", + user: userStatus, + }; + } + } + return { success: false, message: "Token not set up", user: userStatus, }; - } function getSSOIssuerName() { @@ -122,7 +118,6 @@ function generateOAuthConfig() { scope: "openid profile email", access_type: "offline", prompt: "consent", - state: crypto.randomUUID() }, routes: authRoutes, idpLogout: true, From ecf54759660007e828ca7967f68c03f39c379b66 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 5 Apr 2026 12:10:29 +0300 Subject: [PATCH 13/22] Update apps/desktop/package.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- apps/desktop/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index a31b742df5..0c30a11f93 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -40,7 +40,7 @@ "@electron-forge/maker-zip": "7.11.1", "@electron-forge/plugin-auto-unpack-natives": "7.11.1", "@electron-forge/plugin-fuses": "7.11.1", - "@electron/fuses": "2.1.1", + "@electron/fuses": "1.0.0", "@triliumnext/commons": "workspace:*", "@triliumnext/server": "workspace:*", "@types/electron-squirrel-startup": "1.0.2", From b99486259ecb821f54b3bbf6eb33c72782c21372 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 5 Apr 2026 12:10:44 +0300 Subject: [PATCH 14/22] Update apps/server/src/etapi/notes.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- apps/server/src/etapi/notes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/etapi/notes.ts b/apps/server/src/etapi/notes.ts index d85f128240..544efd9dc6 100644 --- a/apps/server/src/etapi/notes.ts +++ b/apps/server/src/etapi/notes.ts @@ -67,7 +67,7 @@ function register(router: Router) { const params = _params as NoteParams; // Validate MIME type for image notes - if (params.type === "image" && params.mime && !params.mime.startsWith("image/")) { + if (params.type === "image" && params.mime && !params.mime.toLowerCase().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/'.`); } From 465c36407cb3e6e898275f4de4b22a98c0418e2f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 5 Apr 2026 12:10:52 +0300 Subject: [PATCH 15/22] Update apps/server/src/etapi/notes.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- apps/server/src/etapi/notes.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/server/src/etapi/notes.ts b/apps/server/src/etapi/notes.ts index 544efd9dc6..f7fb5c9c64 100644 --- a/apps/server/src/etapi/notes.ts +++ b/apps/server/src/etapi/notes.ts @@ -99,9 +99,10 @@ function register(router: Router) { } // 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/")) { + const effectiveType = req.body.type ?? note.type; + const effectiveMime = req.body.mime ?? note.mime; + const normalizedEffectiveMime = typeof effectiveMime === "string" ? effectiveMime.toLowerCase() : effectiveMime; + if (effectiveType === "image" && normalizedEffectiveMime && !normalizedEffectiveMime.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/'.`); } From 9bc18b774e39a53fc83c8d6a07871256b14e6fb1 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 5 Apr 2026 12:07:55 +0300 Subject: [PATCH 16/22] test(server): add unit tests for sanitizeSvg --- apps/server/src/services/utils.spec.ts | 107 +++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/apps/server/src/services/utils.spec.ts b/apps/server/src/services/utils.spec.ts index 1a69d7dd17..bf7fda5570 100644 --- a/apps/server/src/services/utils.spec.ts +++ b/apps/server/src/services/utils.spec.ts @@ -705,3 +705,110 @@ describe("#slugify", () => { expect(result).toBe(expectedSlug); }); }); + +describe("#sanitizeSvg", () => { + it("should remove script elements", () => { + const maliciousSvg = ''; + const result = utils.sanitizeSvg(maliciousSvg); + expect(result).toBe(''); + }); + + it("should remove script elements with attributes", () => { + const maliciousSvg = ''; + const result = utils.sanitizeSvg(maliciousSvg); + expect(result).toBe(''); + }); + + it("should remove multiline script elements", () => { + const maliciousSvg = ``; + const result = utils.sanitizeSvg(maliciousSvg); + expect(result).toBe(''); + }); + + it("should remove onclick event handlers with double quotes", () => { + const maliciousSvg = ''; + const result = utils.sanitizeSvg(maliciousSvg); + expect(result).toBe(''); + }); + + it("should remove onclick event handlers with single quotes", () => { + const maliciousSvg = ""; + const result = utils.sanitizeSvg(maliciousSvg); + expect(result).toBe(''); + }); + + it("should remove onload event handlers", () => { + const maliciousSvg = ''; + const result = utils.sanitizeSvg(maliciousSvg); + expect(result).toBe(''); + }); + + it("should remove onerror event handlers", () => { + const maliciousSvg = ''; + const result = utils.sanitizeSvg(maliciousSvg); + expect(result).toBe(''); + }); + + it("should remove onmouseover event handlers", () => { + const maliciousSvg = ''; + const result = utils.sanitizeSvg(maliciousSvg); + expect(result).toBe(''); + }); + + it("should remove event handlers without quotes", () => { + const maliciousSvg = ''; + const result = utils.sanitizeSvg(maliciousSvg); + expect(result).toBe(''); + }); + + it("should replace javascript: URLs in href with #", () => { + const maliciousSvg = 'Click me'; + const result = utils.sanitizeSvg(maliciousSvg); + expect(result).toBe('Click me'); + }); + + it("should replace javascript: URLs in xlink:href with #", () => { + const maliciousSvg = 'Click me'; + const result = utils.sanitizeSvg(maliciousSvg); + expect(result).toBe('Click me'); + }); + + it("should preserve valid SVG content", () => { + const validSvg = ''; + const result = utils.sanitizeSvg(validSvg); + expect(result).toBe(validSvg); + }); + + it("should preserve valid href URLs", () => { + const validSvg = 'Link'; + const result = utils.sanitizeSvg(validSvg); + expect(result).toBe(validSvg); + }); + + it("should handle multiple malicious elements", () => { + const maliciousSvg = 'link'; + const result = utils.sanitizeSvg(maliciousSvg); + expect(result).toBe('link'); + }); + + it("should handle empty SVG", () => { + const emptySvg = ''; + const result = utils.sanitizeSvg(emptySvg); + expect(result).toBe(''); + }); + + it("should be case insensitive for script tags", () => { + const maliciousSvg = ''; + const result = utils.sanitizeSvg(maliciousSvg); + expect(result).toBe(''); + }); + + it("should be case insensitive for event handlers", () => { + const maliciousSvg = ''; + const result = utils.sanitizeSvg(maliciousSvg); + expect(result).toBe(''); + }); +}); From 79dc4b39f18a49a51e3509d044829705e567d717 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 5 Apr 2026 12:11:00 +0300 Subject: [PATCH 17/22] chore(client): address requested changes --- apps/client/src/services/doc_renderer.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/apps/client/src/services/doc_renderer.ts b/apps/client/src/services/doc_renderer.ts index 74ccfa8447..bb3bb996c2 100644 --- a/apps/client/src/services/doc_renderer.ts +++ b/apps/client/src/services/doc_renderer.ts @@ -9,14 +9,9 @@ import { formatCodeBlocks } from "./syntax_highlight.js"; * 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; + // Allow alphanumeric characters, spaces, underscores, hyphens, and forward slashes. + const validDocNameRegex = /^[a-zA-Z0-9_/\- ]+$/; + return validDocNameRegex.test(docName); } export default function renderDoc(note: FNote) { @@ -59,7 +54,7 @@ async function processContent(url: string, $content: JQuery) { // Images are relative to the docnote but that will not work when rendered in the application since the path breaks. $content.find("img").each((i, el) => { const $img = $(el); - $img.attr("src", `${dir }/${ $img.attr("src")}`); + $img.attr("src", `${dir}/${$img.attr("src")}`); }); formatCodeBlocks($content); From 9a4fef80b954165ac3437f6646a934cd6c6f0df5 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 5 Apr 2026 12:15:07 +0300 Subject: [PATCH 18/22] chore(deps): fix pnpm lock --- pnpm-lock.yaml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94ff3064a2..f1c9787efd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -447,10 +447,10 @@ importers: version: 7.11.1 '@electron-forge/plugin-fuses': specifier: 7.11.1 - version: 7.11.1(@electron/fuses@2.1.1) + version: 7.11.1(@electron/fuses@1.0.0) '@electron/fuses': - specifier: 2.1.1 - version: 2.1.1 + specifier: 1.0.0 + version: 1.0.0 '@triliumnext/commons': specifier: workspace:* version: link:../../packages/commons @@ -2394,10 +2394,8 @@ packages: engines: {node: '>=10.12.0'} hasBin: true - '@electron/fuses@2.1.1': - resolution: {integrity: sha512-38ho27/mtUV/LpsZ1LCDJUomKBBSUZDk/qBH4FNNtoN5fmnkmWDcIp5pm1Kv3InqhRjKZKs7Jzx+wWZNMArHrA==} - engines: {node: '>=22.12.0'} - hasBin: true + '@electron/fuses@1.0.0': + resolution: {integrity: sha512-VjWIlZHEB7a93tXl+6tX2YzN+s1/mS0RM8WX4GZlMOqAzlmRfTMP6pp0MM0LtkzWZB+KQOv+zJt5Dlgdik+DUQ==} '@electron/get@2.0.3': resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} @@ -17620,11 +17618,11 @@ snapshots: - bluebird - supports-color - '@electron-forge/plugin-fuses@7.11.1(@electron/fuses@2.1.1)': + '@electron-forge/plugin-fuses@7.11.1(@electron/fuses@1.0.0)': dependencies: '@electron-forge/plugin-base': 7.11.1 '@electron-forge/shared-types': 7.11.1 - '@electron/fuses': 2.1.1 + '@electron/fuses': 1.0.0 transitivePeerDependencies: - bluebird - supports-color @@ -17711,7 +17709,9 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 - '@electron/fuses@2.1.1': {} + '@electron/fuses@1.0.0': + dependencies: + fs-extra: 9.1.0 '@electron/get@2.0.3': dependencies: From 4a48796142e453a5c4425dae2eb44298d0464ea1 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 5 Apr 2026 12:37:33 +0300 Subject: [PATCH 19/22] chore(ci): trigger dev on release branches as well --- .github/workflows/dev.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 041a31ea4a..977d4269bf 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -1,9 +1,13 @@ name: Dev on: push: - branches: [ main ] + branches: + - main + - "release/*" pull_request: - branches: [ main ] + branches: + - main + - "release/*" concurrency: group: ${{ github.workflow }}-${{ github.ref }} From 13b1e0afbbd90fac7699a737b71e166cf0d61af7 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 5 Apr 2026 12:46:39 +0300 Subject: [PATCH 20/22] fix(desktop): make failing due to wrong version of fuses --- apps/desktop/package.json | 2 +- pnpm-lock.yaml | 33 +++++++++++---------------------- 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 0c30a11f93..0ce2d34b6c 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -40,7 +40,7 @@ "@electron-forge/maker-zip": "7.11.1", "@electron-forge/plugin-auto-unpack-natives": "7.11.1", "@electron-forge/plugin-fuses": "7.11.1", - "@electron/fuses": "1.0.0", + "@electron/fuses": "1.8.0", "@triliumnext/commons": "workspace:*", "@triliumnext/server": "workspace:*", "@types/electron-squirrel-startup": "1.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1c9787efd..87504dcfdf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -447,10 +447,10 @@ importers: version: 7.11.1 '@electron-forge/plugin-fuses': specifier: 7.11.1 - version: 7.11.1(@electron/fuses@1.0.0) + version: 7.11.1(@electron/fuses@1.8.0) '@electron/fuses': - specifier: 1.0.0 - version: 1.0.0 + specifier: 1.8.0 + version: 1.8.0 '@triliumnext/commons': specifier: workspace:* version: link:../../packages/commons @@ -2394,8 +2394,9 @@ packages: engines: {node: '>=10.12.0'} hasBin: true - '@electron/fuses@1.0.0': - resolution: {integrity: sha512-VjWIlZHEB7a93tXl+6tX2YzN+s1/mS0RM8WX4GZlMOqAzlmRfTMP6pp0MM0LtkzWZB+KQOv+zJt5Dlgdik+DUQ==} + '@electron/fuses@1.8.0': + resolution: {integrity: sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==} + hasBin: true '@electron/get@2.0.3': resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} @@ -16658,8 +16659,6 @@ snapshots: '@ckeditor/ckeditor5-widget': 47.4.0 ckeditor5: 47.4.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-icons@47.4.0': {} @@ -16702,8 +16701,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.4.0 '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-inspector@5.0.0': {} @@ -16713,8 +16710,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.4.0 '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-line-height@47.4.0': dependencies: @@ -16739,8 +16734,6 @@ snapshots: '@ckeditor/ckeditor5-widget': 47.4.0 ckeditor5: 47.4.0 es-toolkit: 1.39.5 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-list-multi-level@47.4.0': dependencies: @@ -16764,8 +16757,6 @@ snapshots: '@ckeditor/ckeditor5-ui': 47.4.0 '@ckeditor/ckeditor5-utils': 47.4.0 ckeditor5: 47.4.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-markdown-gfm@47.4.0': dependencies: @@ -16803,8 +16794,6 @@ snapshots: '@ckeditor/ckeditor5-utils': 47.4.0 '@ckeditor/ckeditor5-widget': 47.4.0 ckeditor5: 47.4.0 - transitivePeerDependencies: - - supports-color '@ckeditor/ckeditor5-mention@47.4.0(patch_hash=5981fb59ba35829e4dff1d39cf771000f8a8fdfa7a34b51d8af9549541f2d62d)': dependencies: @@ -17618,11 +17607,11 @@ snapshots: - bluebird - supports-color - '@electron-forge/plugin-fuses@7.11.1(@electron/fuses@1.0.0)': + '@electron-forge/plugin-fuses@7.11.1(@electron/fuses@1.8.0)': dependencies: '@electron-forge/plugin-base': 7.11.1 '@electron-forge/shared-types': 7.11.1 - '@electron/fuses': 1.0.0 + '@electron/fuses': 1.8.0 transitivePeerDependencies: - bluebird - supports-color @@ -17709,9 +17698,11 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 - '@electron/fuses@1.0.0': + '@electron/fuses@1.8.0': dependencies: + chalk: 4.1.2 fs-extra: 9.1.0 + minimist: 1.2.8 '@electron/get@2.0.3': dependencies: @@ -23164,8 +23155,6 @@ snapshots: ckeditor5-collaboration@47.4.0: dependencies: '@ckeditor/ckeditor5-collaboration-core': 47.4.0 - transitivePeerDependencies: - - supports-color ckeditor5-premium-features@47.4.0(bufferutil@4.0.9)(ckeditor5@47.4.0)(utf-8-validate@6.0.5): dependencies: From a01ce2c3fc1ec88a8892eeec99fdb4caa8cbcd1e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 5 Apr 2026 19:28:03 +0300 Subject: [PATCH 21/22] docs(release): release notes for v0.102.2 --- docs/Release Notes/!!!meta.json | 132 +++++++++++------- .../Release Notes/Release Template.md | 4 + docs/Release Notes/Release Notes/v0.102.2.md | 37 +++++ 3 files changed, 120 insertions(+), 53 deletions(-) create mode 100644 docs/Release Notes/Release Notes/v0.102.2.md diff --git a/docs/Release Notes/!!!meta.json b/docs/Release Notes/!!!meta.json index 60744e69d0..a8e7fa7ce4 100644 --- a/docs/Release Notes/!!!meta.json +++ b/docs/Release Notes/!!!meta.json @@ -1,6 +1,6 @@ { "formatVersion": 2, - "appVersion": "0.102.0", + "appVersion": "0.102.1", "files": [ { "isClone": false, @@ -61,6 +61,32 @@ "attachments": [], "dirFileName": "Release Notes", "children": [ + { + "isClone": false, + "noteId": "ZdWJsMQvY1fo", + "notePath": [ + "hD3V4hiu2VW4", + "ZdWJsMQvY1fo" + ], + "title": "v0.102.2", + "notePosition": 10, + "prefix": null, + "isExpanded": false, + "type": "text", + "mime": "text/html", + "attributes": [ + { + "type": "relation", + "name": "template", + "value": "wyurrlcDl416", + "isInheritable": false, + "position": 60 + } + ], + "format": "markdown", + "dataFileName": "v0.102.2.md", + "attachments": [] + }, { "isClone": false, "noteId": "4FTGCuCiG7s7", @@ -69,7 +95,7 @@ "4FTGCuCiG7s7" ], "title": "v0.102.1", - "notePosition": 10, + "notePosition": 20, "prefix": null, "isExpanded": false, "type": "text", @@ -95,7 +121,7 @@ "d582eD4RY4OM" ], "title": "v0.102.0", - "notePosition": 20, + "notePosition": 30, "prefix": null, "isExpanded": false, "type": "text", @@ -121,7 +147,7 @@ "IlBzLeN3MJhw" ], "title": "v0.101.3", - "notePosition": 30, + "notePosition": 40, "prefix": null, "isExpanded": false, "type": "text", @@ -147,7 +173,7 @@ "vcBthaXcwAm6" ], "title": "v0.101.2", - "notePosition": 40, + "notePosition": 50, "prefix": null, "isExpanded": false, "type": "text", @@ -173,7 +199,7 @@ "AgUcrU9nFXuW" ], "title": "v0.101.1", - "notePosition": 50, + "notePosition": 60, "prefix": null, "isExpanded": false, "type": "text", @@ -199,7 +225,7 @@ "uYwlZ594eyJu" ], "title": "v0.101.0", - "notePosition": 60, + "notePosition": 70, "prefix": null, "isExpanded": false, "type": "text", @@ -225,7 +251,7 @@ "iPGKEk7pwJXK" ], "title": "v0.100.0", - "notePosition": 70, + "notePosition": 80, "prefix": null, "isExpanded": false, "type": "text", @@ -251,7 +277,7 @@ "7HKMTjmopLcM" ], "title": "v0.99.5", - "notePosition": 80, + "notePosition": 90, "prefix": null, "isExpanded": false, "type": "text", @@ -277,7 +303,7 @@ "RMBaNYPsRpIr" ], "title": "v0.99.4", - "notePosition": 90, + "notePosition": 100, "prefix": null, "isExpanded": false, "type": "text", @@ -303,7 +329,7 @@ "yuroLztFfpu5" ], "title": "v0.99.3", - "notePosition": 100, + "notePosition": 110, "prefix": null, "isExpanded": false, "type": "text", @@ -329,7 +355,7 @@ "z207sehwMJ6C" ], "title": "v0.99.2", - "notePosition": 110, + "notePosition": 120, "prefix": null, "isExpanded": false, "type": "text", @@ -355,7 +381,7 @@ "WGQsXq2jNyTi" ], "title": "v0.99.1", - "notePosition": 120, + "notePosition": 130, "prefix": null, "isExpanded": false, "type": "text", @@ -381,7 +407,7 @@ "cyw2Yue9vXf3" ], "title": "v0.99.0", - "notePosition": 130, + "notePosition": 140, "prefix": null, "isExpanded": false, "type": "text", @@ -407,7 +433,7 @@ "QOJwjruOUr4k" ], "title": "v0.98.1", - "notePosition": 140, + "notePosition": 150, "prefix": null, "isExpanded": false, "type": "text", @@ -433,7 +459,7 @@ "PLUoryywi0BC" ], "title": "v0.98.0", - "notePosition": 150, + "notePosition": 160, "prefix": null, "isExpanded": false, "type": "text", @@ -459,7 +485,7 @@ "lvOuiWsLDv8F" ], "title": "v0.97.2", - "notePosition": 160, + "notePosition": 170, "prefix": null, "isExpanded": false, "type": "text", @@ -485,7 +511,7 @@ "OtFZ6Nd9vM3n" ], "title": "v0.97.1", - "notePosition": 170, + "notePosition": 180, "prefix": null, "isExpanded": false, "type": "text", @@ -511,7 +537,7 @@ "SJZ5PwfzHSQ1" ], "title": "v0.97.0", - "notePosition": 180, + "notePosition": 190, "prefix": null, "isExpanded": false, "type": "text", @@ -537,7 +563,7 @@ "mYXFde3LuNR7" ], "title": "v0.96.0", - "notePosition": 190, + "notePosition": 200, "prefix": null, "isExpanded": false, "type": "text", @@ -563,7 +589,7 @@ "jthwbL0FdaeU" ], "title": "v0.95.0", - "notePosition": 200, + "notePosition": 210, "prefix": null, "isExpanded": false, "type": "text", @@ -589,7 +615,7 @@ "7HGYsJbLuhnv" ], "title": "v0.94.1", - "notePosition": 210, + "notePosition": 220, "prefix": null, "isExpanded": false, "type": "text", @@ -615,7 +641,7 @@ "Neq53ujRGBqv" ], "title": "v0.94.0", - "notePosition": 220, + "notePosition": 230, "prefix": null, "isExpanded": false, "type": "text", @@ -641,7 +667,7 @@ "VN3xnce1vLkX" ], "title": "v0.93.0", - "notePosition": 230, + "notePosition": 240, "prefix": null, "isExpanded": false, "type": "text", @@ -659,7 +685,7 @@ "WRaBfQqPr6qo" ], "title": "v0.92.7", - "notePosition": 240, + "notePosition": 250, "prefix": null, "isExpanded": false, "type": "text", @@ -685,7 +711,7 @@ "a2rwfKNmUFU1" ], "title": "v0.92.6", - "notePosition": 250, + "notePosition": 260, "prefix": null, "isExpanded": false, "type": "text", @@ -703,7 +729,7 @@ "fEJ8qErr0BKL" ], "title": "v0.92.5-beta", - "notePosition": 260, + "notePosition": 270, "prefix": null, "isExpanded": false, "type": "text", @@ -721,7 +747,7 @@ "kkkZQQGSXjwy" ], "title": "v0.92.4", - "notePosition": 270, + "notePosition": 280, "prefix": null, "isExpanded": false, "type": "text", @@ -739,7 +765,7 @@ "vAroNixiezaH" ], "title": "v0.92.3-beta", - "notePosition": 280, + "notePosition": 290, "prefix": null, "isExpanded": false, "type": "text", @@ -757,7 +783,7 @@ "mHEq1wxAKNZd" ], "title": "v0.92.2-beta", - "notePosition": 290, + "notePosition": 300, "prefix": null, "isExpanded": false, "type": "text", @@ -775,7 +801,7 @@ "IykjoAmBpc61" ], "title": "v0.92.1-beta", - "notePosition": 300, + "notePosition": 310, "prefix": null, "isExpanded": false, "type": "text", @@ -793,7 +819,7 @@ "dq2AJ9vSBX4Y" ], "title": "v0.92.0-beta", - "notePosition": 310, + "notePosition": 320, "prefix": null, "isExpanded": false, "type": "text", @@ -811,7 +837,7 @@ "3a8aMe4jz4yM" ], "title": "v0.91.6", - "notePosition": 320, + "notePosition": 330, "prefix": null, "isExpanded": false, "type": "text", @@ -829,7 +855,7 @@ "8djQjkiDGESe" ], "title": "v0.91.5", - "notePosition": 330, + "notePosition": 340, "prefix": null, "isExpanded": false, "type": "text", @@ -847,7 +873,7 @@ "OylxVoVJqNmr" ], "title": "v0.91.4-beta", - "notePosition": 340, + "notePosition": 350, "prefix": null, "isExpanded": false, "type": "text", @@ -865,7 +891,7 @@ "tANGQDvnyhrj" ], "title": "v0.91.3-beta", - "notePosition": 350, + "notePosition": 360, "prefix": null, "isExpanded": false, "type": "text", @@ -883,7 +909,7 @@ "hMoBfwSoj1SC" ], "title": "v0.91.2-beta", - "notePosition": 360, + "notePosition": 370, "prefix": null, "isExpanded": false, "type": "text", @@ -901,7 +927,7 @@ "a2XMSKROCl9z" ], "title": "v0.91.1-beta", - "notePosition": 370, + "notePosition": 380, "prefix": null, "isExpanded": false, "type": "text", @@ -919,7 +945,7 @@ "yqXFvWbLkuMD" ], "title": "v0.90.12", - "notePosition": 380, + "notePosition": 390, "prefix": null, "isExpanded": false, "type": "text", @@ -937,7 +963,7 @@ "veS7pg311yJP" ], "title": "v0.90.11-beta", - "notePosition": 390, + "notePosition": 400, "prefix": null, "isExpanded": false, "type": "text", @@ -955,7 +981,7 @@ "sq5W9TQxRqMq" ], "title": "v0.90.10-beta", - "notePosition": 400, + "notePosition": 410, "prefix": null, "isExpanded": false, "type": "text", @@ -973,7 +999,7 @@ "yFEGVCUM9tPx" ], "title": "v0.90.9-beta", - "notePosition": 410, + "notePosition": 420, "prefix": null, "isExpanded": false, "type": "text", @@ -991,7 +1017,7 @@ "o4wAGqOQuJtV" ], "title": "v0.90.8", - "notePosition": 420, + "notePosition": 430, "prefix": null, "isExpanded": false, "type": "text", @@ -1024,7 +1050,7 @@ "i4A5g9iOg9I0" ], "title": "v0.90.7-beta", - "notePosition": 430, + "notePosition": 440, "prefix": null, "isExpanded": false, "type": "text", @@ -1042,7 +1068,7 @@ "ThNf2GaKgXUs" ], "title": "v0.90.6-beta", - "notePosition": 440, + "notePosition": 450, "prefix": null, "isExpanded": false, "type": "text", @@ -1060,7 +1086,7 @@ "G4PAi554kQUr" ], "title": "v0.90.5-beta", - "notePosition": 450, + "notePosition": 460, "prefix": null, "isExpanded": false, "type": "text", @@ -1087,7 +1113,7 @@ "zATRobGRCmBn" ], "title": "v0.90.4", - "notePosition": 460, + "notePosition": 470, "prefix": null, "isExpanded": false, "type": "text", @@ -1105,7 +1131,7 @@ "sCDLf8IKn3Iz" ], "title": "v0.90.3", - "notePosition": 470, + "notePosition": 480, "prefix": null, "isExpanded": false, "type": "text", @@ -1123,7 +1149,7 @@ "VqqyBu4AuTjC" ], "title": "v0.90.2-beta", - "notePosition": 480, + "notePosition": 490, "prefix": null, "isExpanded": false, "type": "text", @@ -1141,7 +1167,7 @@ "RX3Nl7wInLsA" ], "title": "v0.90.1-beta", - "notePosition": 490, + "notePosition": 500, "prefix": null, "isExpanded": false, "type": "text", @@ -1159,7 +1185,7 @@ "GyueACukPWjk" ], "title": "v0.90.0-beta", - "notePosition": 500, + "notePosition": 510, "prefix": null, "isExpanded": false, "type": "text", @@ -1177,7 +1203,7 @@ "kzjHexDTTeVB" ], "title": "v0.48", - "notePosition": 510, + "notePosition": 520, "prefix": null, "isExpanded": false, "type": "text", @@ -1244,7 +1270,7 @@ "wyurrlcDl416" ], "title": "Release Template", - "notePosition": 520, + "notePosition": 530, "prefix": null, "isExpanded": false, "type": "text", diff --git a/docs/Release Notes/Release Notes/Release Template.md b/docs/Release Notes/Release Notes/Release Template.md index ff160444ca..d14bba34cf 100644 --- a/docs/Release Notes/Release Notes/Release Template.md +++ b/docs/Release Notes/Release Notes/Release Template.md @@ -32,4 +32,8 @@ ## ๐Ÿ› ๏ธ Technical updates +* \[โ€ฆ\] + +## ๐Ÿ”’๏ธ Security improvements + * \[โ€ฆ\] \ No newline at end of file diff --git a/docs/Release Notes/Release Notes/v0.102.2.md b/docs/Release Notes/Release Notes/v0.102.2.md new file mode 100644 index 0000000000..94a3b5e1fc --- /dev/null +++ b/docs/Release Notes/Release Notes/v0.102.2.md @@ -0,0 +1,37 @@ +# v0.102.2 +> [!IMPORTANT] +> **This release contains important security fixes. All users are strongly encouraged to update immediately.** +> +> Several vulnerabilities affecting content handling and the desktop application have been addressed. We recommend upgrading before the next scheduled release to ensure your installation is protected. + +> [!NOTE] +> If you enjoyed this release, consider showing a token of appreciation by: +> +> * Pressing the โ€œStarโ€ button on [GitHub](https://github.com/TriliumNext/Trilium) (top-right). +> * Considering a one-time or recurrent donation to the [lead developer](https://github.com/eliandoran) via [GitHub Sponsors](https://github.com/sponsors/eliandoran) or [PayPal](https://paypal.me/eliandoran). +> * If you are interested in an [official mobile application](https://oss.issuehunt.io/r/TriliumNext/Trilium/issues/7447) ย ([#7447](https://github.com/TriliumNext/Trilium/issues/7447)) or [multi-user support](https://oss.issuehunt.io/r/TriliumNext/Trilium/issues/4956) ([#4956](https://github.com/TriliumNext/Trilium/issues/4956)), consider offering financial support via IssueHunt (see links). + +## ๐Ÿ”’๏ธ Security improvements + +* Content Handling + + * Improved request handling for SVG content in share routes + * Improved request handling for SVG content in the main API + * Enhanced content rendering in the Mermaid diagram editor + * Fixed toast notifications to properly escape content + * Added validation for the `docName` attribute in the document renderer + * Marked `docName` as a sensitive attribute in the commons module +* Desktop Application (Electron) + + * Added Electron fuses to harden the desktop application against external abuse + * Improved application integrity checks +* API & Import + + * Added MIME type validation for image uploads via ETAPI + * Aligned attachment upload validation with note upload validation + * Import no longer preserves named note IDs to prevent potential conflicts +* Authentication + + * OpenID Connect now uses a more secure random number generator + +We've also updated our SECURITY.MD file to detail our security practices and how to report vulnerabilities. \ No newline at end of file From 9d6a26dda974cba014638bc36f600bc33ccd2afc Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 5 Apr 2026 19:28:30 +0300 Subject: [PATCH 22/22] docs(security): add more details & change reporting mechanism --- SECURITY.md | 84 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 5 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 20c58fca16..befd061a74 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,13 +2,87 @@ ## Supported Versions -In the (still active) 0.X phase of the project only the latest stable minor release is getting bugfixes (including security ones). +Only the latest stable minor release receives security fixes. -So e.g. if the latest stable version is 0.42.3 and the latest beta version is 0.43.0-beta, then 0.42 line will still get security fixes but older versions (like 0.41.X) won't get any fixes. +For example, if the latest stable version is 0.92.3 and the latest beta is 0.93.0-beta, then only the 0.92.x line will receive security patches. Older versions (like 0.91.x) will not receive fixes. -Description above is a general rule and may be altered on case by case basis. +This policy may be altered on a case-by-case basis for critical vulnerabilities. ## Reporting a Vulnerability -* For low severity vulnerabilities, they can be reported as GitHub issues. -* For severe vulnerabilities, please report it using [GitHub Security Advisories](https://github.com/TriliumNext/Trilium/security/advisories). +**Please report all security vulnerabilities through [GitHub Security Advisories](https://github.com/TriliumNext/Notes/security/advisories/new).** + +We do not accept security reports via email, public issues, or other channels. GitHub Security Advisories allows us to: +- Discuss and triage vulnerabilities privately +- Coordinate fixes before public disclosure +- Credit reporters appropriately +- Publish advisories with CVE identifiers + +### What to Include + +When reporting, please provide: +- A clear description of the vulnerability +- Steps to reproduce or proof-of-concept +- Affected versions (if known) +- Potential impact assessment +- Any suggested mitigations or fixes + +### Response Timeline + +- **Initial response**: Within 7 days +- **Triage decision**: Within 14 days +- **Fix timeline**: Depends on severity and complexity + +## Scope + +### In Scope + +- Remote code execution +- Authentication/authorization bypass +- Cross-site scripting (XSS) that affects other users +- SQL injection +- Path traversal +- Sensitive data exposure +- Privilege escalation + +### Out of Scope (Won't Fix) + +The following are considered out of scope or accepted risks: + +#### Self-XSS / Self-Injection +Trilium is a personal knowledge base where users have full control over their own data. Users can intentionally create notes containing scripts, HTML, or other executable content. This is by design - Trilium's scripting system allows users to extend functionality with custom JavaScript. + +Vulnerabilities that require a user to inject malicious content into their own notes and then view it themselves are not considered security issues. + +#### Electron Architecture (nodeIntegration) +Trilium's desktop application runs with `nodeIntegration: true` to enable its powerful scripting features. This is an intentional design decision, similar to VS Code extensions having full system access. We mitigate risks by: +- Sanitizing content at input boundaries +- Fixing specific XSS vectors as they're discovered +- Using Electron fuses to prevent external abuse + +#### Authenticated User Actions +Actions that require valid authentication and only affect the authenticated user's own data are generally not vulnerabilities. + +#### Denial of Service via Resource Exhaustion +Creating extremely large notes or performing many operations is expected user behavior in a note-taking application. + +#### Missing Security Headers on Non-Sensitive Endpoints +We implement security headers where they provide meaningful protection, but may omit them on endpoints where they provide no practical benefit. + +## Coordinated Disclosure + +We follow a coordinated disclosure process: + +1. **Report received** - We acknowledge receipt and begin triage +2. **Fix developed** - We develop and test a fix privately +3. **Release prepared** - Security release is prepared with vague changelog +4. **Users notified** - Release is published, users encouraged to upgrade +5. **Advisory published** - After reasonable upgrade window (typically 2-4 weeks), full advisory is published + +We appreciate reporters allowing us time to fix issues before public disclosure. We aim to credit all reporters in published advisories unless they prefer to remain anonymous. + +## Security Updates + +Security fixes are released as patch versions (e.g., 0.92.1 โ†’ 0.92.2) to minimize upgrade friction. We recommend all users keep their installations up to date. + +Subscribe to GitHub releases or watch the repository to receive notifications of new releases.