diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 38d170903a..138fc39bc1 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 }} 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. diff --git a/apps/client/package.json b/apps/client/package.json index 4b48c04d14..aaad9a110c 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -1,6 +1,6 @@ { "name": "@triliumnext/client", - "version": "0.102.1", + "version": "0.102.2", "description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)", "private": true, "license": "AGPL-3.0-only", 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..bb3bb996c2 100644 --- a/apps/client/src/services/doc_renderer.ts +++ b/apps/client/src/services/doc_renderer.ts @@ -3,22 +3,39 @@ 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 { + // 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) { 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; } @@ -28,8 +45,6 @@ export default function renderDoc(note: FNote) { } else { resolve($content); } - - return $content; }); } @@ -39,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); @@ -48,10 +63,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 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/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}
} 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 69e765bd59..c7fadf8e3f 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@triliumnext/desktop", - "version": "0.102.1", + "version": "0.102.2", "description": "Build your personal knowledge base with Trilium Notes", "private": true, "main": "src/main.ts", @@ -28,15 +28,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": "14.0.0", - "electron": "41.1.0", "@electron-forge/cli": "7.11.1", "@electron-forge/maker-deb": "7.11.1", "@electron-forge/maker-dmg": "7.11.1", @@ -45,6 +40,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": "1.8.0", + "@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/apps/edit-docs/package.json b/apps/edit-docs/package.json index e2142bc35f..66ab77a1e1 100644 --- a/apps/edit-docs/package.json +++ b/apps/edit-docs/package.json @@ -1,6 +1,6 @@ { "name": "@triliumnext/edit-docs", - "version": "0.102.1", + "version": "0.102.2", "private": true, "description": "Desktop version of Trilium which imports the demo database (presented to new users at start-up) or the user guide and other documentation and saves the modifications for committing.", "dependencies": { diff --git a/apps/server/package.json b/apps/server/package.json index 67670ea0b9..ff8d9bd609 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,6 +1,6 @@ { "name": "@triliumnext/server", - "version": "0.102.1", + "version": "0.102.2", "description": "The server-side component of TriliumNext, which exposes the client via the web, allows for sync and provides a REST API for both internal and external use.", "private": true, "main": "./src/main.ts", diff --git a/apps/server/src/etapi/notes.ts b/apps/server/src/etapi/notes.ts index 867d325925..935868ef78 100644 --- a/apps/server/src/etapi/notes.ts +++ b/apps/server/src/etapi/notes.ts @@ -67,6 +67,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.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/'.`); + } + try { const resp = noteService.createNewNote(params); @@ -94,6 +99,14 @@ 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; + 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/'.`); + } + noteService.saveRevisionIfNeeded(note); eu.validateAndPatch(note, req.body, ALLOWED_PROPERTIES_FOR_PATCH); note.save(); diff --git a/apps/server/src/routes/api/files.ts b/apps/server/src/routes/api/files.ts index 5c47a22620..e7e580a631 100644 --- a/apps/server/src/routes/api/files.ts +++ b/apps/server/src/routes/api/files.ts @@ -230,6 +230,10 @@ function uploadModifiedFileToAttachment(req: Request<{ attachmentId: string }>) 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}'`); diff --git a/apps/server/src/routes/api/image.ts b/apps/server/src/routes/api/image.ts index 026e069c0d..746bad288f 100644 --- a/apps/server/src/routes/api/image.ts +++ b/apps/server/src/routes/api/image.ts @@ -6,6 +6,7 @@ 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<{ noteId: string }>, res: Response) { const image = becca.getNote(req.params.noteId); @@ -38,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); } export function renderPngAttachment(image: BNote | BRevision, res: Response, attachmentName: string) { @@ -88,7 +94,12 @@ function returnAttachedImage(req: Request<{ attachmentId: string }>, res: Respon 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<{ noteId: string }>) { @@ -129,3 +140,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/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; } diff --git a/apps/server/src/services/open_id.ts b/apps/server/src/services/open_id.ts index 2ae3bbe1ef..a6593e1bb2 100644 --- a/apps/server/src/services/open_id.ts +++ b/apps/server/src/services/open_id.ts @@ -1,14 +1,14 @@ 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 +27,7 @@ function isOpenIDEnabled() { function isUserSaved() { const data = sql.getValue("SELECT isSetup FROM user_data;"); - return data === "true" ? true : false; + return data === "true"; } function getUsername() { @@ -59,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; - } else { - return { - success: false, - message: "Token not set up", - user: userStatus, - }; + 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() { @@ -121,11 +118,10 @@ function generateOAuthConfig() { scope: "openid profile email", access_type: "offline", prompt: "consent", - state: "random_state_" + Math.random().toString(36).substring(2) }, routes: authRoutes, idpLogout: true, - logoutParams: logoutParams, + logoutParams, afterCallback: async (req: Request, res: Response, session: Session) => { if (!sqlInit.isDbInitialized()) return session; 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(''); + }); +}); diff --git a/apps/server/src/services/utils.ts b/apps/server/src/services/utils.ts index dc06f6206a..59771a1d37 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; @@ -560,6 +576,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 69489c5522..5b33103501 100644 --- a/apps/server/src/share/routes.ts +++ b/apps/server/src/share/routes.ts @@ -1,15 +1,14 @@ +import type { NextFunction, Request, Response, Router } from "express"; import safeCompare from "safe-compare"; -import type { NextFunction, Request, Response, Router } from "express"; - +import SearchContext from "../services/search/search_context.js"; +import searchService from "../services/search/services/search.js"; +import utils, { sanitizeSvg } from "../services/utils.js"; +import { getDefaultTemplatePath, renderNoteContent } from "./content_renderer.js"; +import type SAttachment from "./shaca/entities/sattachment.js"; +import type SNote from "./shaca/entities/snote.js"; import shaca from "./shaca/shaca.js"; import shacaLoader from "./shaca/shaca_loader.js"; -import searchService from "../services/search/services/search.js"; -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 { isShareDbReady } from "./sql.js"; function assertShareDbReady(_req: Request, res: Response, next: NextFunction) { @@ -104,17 +103,18 @@ function renderImageAttachment(image: SNote, res: Response, attachmentName: stri && possibleSvgContent !== null && "svg" in possibleSvgContent && typeof possibleSvgContent.svg === "string") - ? possibleSvgContent.svg - : null; + ? possibleSvgContent.svg + : null; if (contentSvg) { svgString = contentSvg; } } - 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); } @@ -320,7 +320,7 @@ function register(router: Router) { return; } - const searchContext = new SearchContext({ ancestorNoteId: ancestorNoteId }); + const searchContext = new SearchContext({ ancestorNoteId }); const searchResults = searchService.findResultsWithQuery(search, searchContext); const filteredResults = searchResults.map((sr) => { const fullNote = shaca.notes[sr.noteId]; 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 diff --git a/package.json b/package.json index e9897cca03..76c682174e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@triliumnext/source", - "version": "0.102.1", + "version": "0.102.2", "description": "Build your personal knowledge base with Trilium Notes", "directories": { "doc": "docs" 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 ); diff --git a/packages/commons/package.json b/packages/commons/package.json index 2fbf998a84..2ef9f70b94 100644 --- a/packages/commons/package.json +++ b/packages/commons/package.json @@ -1,6 +1,6 @@ { "name": "@triliumnext/commons", - "version": "0.102.1", + "version": "0.102.2", "description": "Shared library between the clients (e.g. browser, Electron) and the server, mostly for type definitions and utility methods.", "private": true, "type": "module", 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" }, diff --git a/packages/pdfjs-viewer/package.json b/packages/pdfjs-viewer/package.json index 34426dfaaf..457be5f64e 100644 --- a/packages/pdfjs-viewer/package.json +++ b/packages/pdfjs-viewer/package.json @@ -1,6 +1,6 @@ { "name": "@triliumnext/pdfjs-viewer", - "version": "0.102.1", + "version": "0.102.2", "private": true, "scripts": { "build": "tsx scripts/build.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e075d7fe45..305a045d8d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -448,7 +448,7 @@ importers: dependencies: '@electron/remote': specifier: 2.1.3 - version: 2.1.3(electron@41.1.0) + version: 2.1.3(electron@40.6.1) better-sqlite3: specifier: 12.8.0 version: 12.8.0 @@ -492,6 +492,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@1.8.0) + '@electron/fuses': + specifier: 1.8.0 + version: 1.8.0 '@triliumnext/commons': specifier: workspace:* version: link:../../packages/commons @@ -502,11 +508,11 @@ importers: specifier: 1.0.2 version: 1.0.2 copy-webpack-plugin: - specifier: 14.0.0 - version: 14.0.0(webpack@5.105.4(esbuild@0.27.5)) + specifier: 13.0.1 + version: 13.0.1(webpack@5.105.4(esbuild@0.27.5)) electron: - specifier: 41.1.0 - version: 41.1.0 + specifier: 40.6.1 + version: 40.6.1 prebuild-install: specifier: 7.1.3 version: 7.1.3 @@ -2278,6 +2284,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'} @@ -2315,6 +2327,10 @@ packages: engines: {node: '>=10.12.0'} hasBin: true + '@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==} engines: {node: '>=12'} @@ -7805,6 +7821,12 @@ packages: copy-anything@2.0.6: resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==} + copy-webpack-plugin@13.0.1: + resolution: {integrity: sha512-J+YV3WfhY6W/Xf9h+J1znYuqTye2xkBUIGyTPWuBAT27qajBa5mR4f8WBmfDY3YjRftT2kqZZiLi1qf0H+UOFw==} + engines: {node: '>= 18.12.0'} + peerDependencies: + webpack: '>=5.104.1' + copy-webpack-plugin@14.0.0: resolution: {integrity: sha512-3JLW90aBGeaTLpM7mYQKpnVdgsUZRExY55giiZgLuX/xTQRUs1dOCwbBnWnvY6Q6rfZoXMNwzOQJCSZPppfqXA==} engines: {node: '>= 20.9.0'} @@ -8490,6 +8512,11 @@ packages: resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} engines: {node: '>=8.0.0'} + electron@40.6.1: + resolution: {integrity: sha512-u9YfoixttdauciHV9Ut9Zf3YipJoU093kR1GSYTTXTAXqhiXI0G1A0NnL/f0O2m2UULCXaXMf2W71PloR6V9pQ==} + engines: {node: '>= 12.20.55'} + hasBin: true + electron@41.1.0: resolution: {integrity: sha512-0XRFyxRqetmqtkkBvV++wGbHYJ7bD++f6EgJW8y9kX4pPRagwlmKDtzqXZhKiu0DIQppm3sXxzHWK9GYP91OKQ==} engines: {node: '>= 12.20.55'} @@ -16391,6 +16418,15 @@ snapshots: - bluebird - supports-color + '@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.8.0 + transitivePeerDependencies: + - bluebird + - supports-color + '@electron-forge/publisher-base@7.11.1': dependencies: '@electron-forge/shared-types': 7.11.1 @@ -16473,6 +16509,12 @@ snapshots: glob: 7.2.3 minimatch: 3.1.5 + '@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: debug: 4.4.3 @@ -16598,6 +16640,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@electron/remote@2.1.3(electron@40.6.1)': + dependencies: + electron: 40.6.1 + '@electron/remote@2.1.3(electron@41.1.0)': dependencies: electron: 41.1.0 @@ -23286,6 +23332,15 @@ snapshots: is-what: 3.14.1 optional: true + copy-webpack-plugin@13.0.1(webpack@5.105.4(esbuild@0.27.5)): + dependencies: + glob-parent: 6.0.2 + normalize-path: 3.0.0 + schema-utils: 4.3.3 + serialize-javascript: 7.0.5 + tinyglobby: 0.2.15 + webpack: 5.105.4(esbuild@0.27.5) + copy-webpack-plugin@14.0.0(webpack@5.105.4(esbuild@0.27.5)): dependencies: glob-parent: 6.0.2 @@ -24075,6 +24130,14 @@ snapshots: - supports-color optional: true + electron@40.6.1: + dependencies: + '@electron/get': 2.0.3 + '@types/node': 24.12.0 + extract-zip: 2.0.1 + transitivePeerDependencies: + - supports-color + electron@41.1.0: dependencies: '@electron/get': 2.0.3