Compare commits

...

24 Commits

Author SHA1 Message Date
Elian Doran
bf23439792 chore(release): prepare for v0.102.2 2026-04-05 19:30:04 +03:00
Elian Doran
b7a0bc08be Various bugfixes (#9274) 2026-04-05 19:28:59 +03:00
Elian Doran
9d6a26dda9 docs(security): add more details & change reporting mechanism 2026-04-05 19:28:30 +03:00
Elian Doran
a01ce2c3fc docs(release): release notes for v0.102.2 2026-04-05 19:28:03 +03:00
Elian Doran
13b1e0afbb fix(desktop): make failing due to wrong version of fuses 2026-04-05 12:46:39 +03:00
Elian Doran
4a48796142 chore(ci): trigger dev on release branches as well 2026-04-05 12:37:33 +03:00
Elian Doran
9a4fef80b9 chore(deps): fix pnpm lock 2026-04-05 12:15:07 +03:00
Elian Doran
79dc4b39f1 chore(client): address requested changes 2026-04-05 12:11:05 +03:00
Elian Doran
9bc18b774e test(server): add unit tests for sanitizeSvg 2026-04-05 12:11:05 +03:00
Elian Doran
465c36407c Update apps/server/src/etapi/notes.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-05 12:10:52 +03:00
Elian Doran
b99486259e Update apps/server/src/etapi/notes.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-05 12:10:44 +03:00
Elian Doran
ecf5475966 Update apps/desktop/package.json
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-05 12:10:29 +03:00
Elian Doran
90822cc8a3 chore: address requested changes 2026-04-05 11:59:45 +03:00
Elian Doran
5c46209ddc feat(server): improve request handling for SVGs 2026-04-05 11:28:28 +03:00
Elian Doran
176de87b6b feat(desktop): add Electron fuses 2026-04-05 11:01:22 +03:00
Elian Doran
7f199c527b feat(share): improve request handling for SVGs 2026-04-05 10:52:36 +03:00
Elian Doran
2432e230c5 chore(etapi): enforce MIME for image upload 2026-04-05 10:44:47 +03:00
Elian Doran
fc1be0d23d fix(ckeditor5-mermaid): use textContent for diagram source rendering 2026-04-05 10:17:16 +03:00
Elian Doran
626aca5181 fix(client): toasts could render HTML content 2026-04-04 22:21:25 +03:00
Elian Doran
8204322b46 fix(openid): use more secure RNG 2026-04-04 22:02:33 +03:00
Elian Doran
ed3b86cd49 fix(import): no longer preserve named note IDs 2026-04-04 21:27:37 +03:00
Elian Doran
b371675494 chore(commons): mark docName as a dangerous attribute 2026-04-04 21:25:05 +03:00
Elian Doran
ff06c8e7bd fix(client): validate docName attribute in doc renderer 2026-04-04 21:21:50 +03:00
Elian Doran
8ff41d8fa9 fix(server): align attachment upload validation with note upload 2026-04-04 20:46:03 +03:00
27 changed files with 548 additions and 177 deletions

View File

@@ -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 }}

View File

@@ -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.

View File

@@ -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",

View File

@@ -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);
});
});

View File

@@ -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<JQuery<HTMLElement>>((resolve) => {
let docName = note.getLabelValue("docName");
const docName = note.getLabelValue("docName");
const $content = $("<div>");
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<HTMLElement>) {
// 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<HTMLElement>) {
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`;
}

View File

@@ -5,7 +5,6 @@ import { useEffect } from "preact/hooks";
import { removeToastFromStore, ToastOptionsWithRequiredId, toasts } from "../services/toast";
import Icon from "./react/Icon";
import { RawHtmlBlock } from "./react/RawHtml";
import Button from "./react/Button";
export default function ToastContainer() {
@@ -54,7 +53,7 @@ function Toast({ id, title, timeout, progress, message, icon, buttons }: ToastOp
<div class="toast-icon">{toastIcon}</div>
)}
<RawHtmlBlock className="toast-body" html={message} />
<div className="toast-body">{message}</div>
{!title && <div class="toast-header">{closeButton}</div>}

View File

@@ -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: {

View File

@@ -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",
@@ -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": "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"
}
}

View File

@@ -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": {

View File

@@ -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",

View File

@@ -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.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);
@@ -93,6 +98,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();

View File

@@ -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}'`);

View File

@@ -1,12 +1,14 @@
"use strict";
import imageService from "../../services/image.js";
import becca from "../../becca/becca.js";
import fs from "fs";
import type { Request, Response } from "express";
import fs from "fs";
import becca from "../../becca/becca.js";
import type BNote from "../../becca/entities/bnote.js";
import type BRevision from "../../becca/entities/brevision.js";
import imageService from "../../services/image.js";
import { RESOURCE_DIR } from "../../services/resource_dir.js";
import { sanitizeSvg } from "../../services/utils.js";
function returnImageFromNote(req: Request, res: Response) {
const image = becca.getNote(req.params.noteId);
@@ -37,28 +39,33 @@ function returnImageInt(image: BNote | BRevision | null, res: Response) {
} else {
res.set("Content-Type", image.mime);
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
res.send(image.getContent());
if (image.mime === "image/svg+xml") {
sendSanitizedSvg(res, image.getContent());
} else {
res.send(image.getContent());
}
}
}
export function renderSvgAttachment(image: BNote | BRevision, res: Response, attachmentName: string) {
let svg: string | Buffer = `<svg xmlns="http://www.w3.org/2000/svg"></svg>`;
let svgContent: string | Buffer = `<svg xmlns="http://www.w3.org/2000/svg"></svg>`;
const attachment = image.getAttachmentByTitle(attachmentName);
if (attachment) {
svg = attachment.getContent();
svgContent = attachment.getContent();
} else {
// backwards compatibility, before attachments, the SVG was stored in the main note content as a separate key
const contentSvg = image.getJsonContentSafely()?.svg;
if (contentSvg) {
svg = contentSvg;
svgContent = contentSvg;
}
}
res.set("Content-Type", "image/svg+xml");
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
res.send(svg);
sendSanitizedSvg(res, svgContent);
}
function returnAttachedImage(req: Request, res: Response) {
@@ -75,7 +82,12 @@ function returnAttachedImage(req: Request, res: Response) {
res.set("Content-Type", attachment.mime);
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
res.send(attachment.getContent());
if (attachment.mime === "image/svg+xml") {
sendSanitizedSvg(res, attachment.getContent());
} else {
res.send(attachment.getContent());
}
}
function updateImage(req: Request) {
@@ -116,3 +128,9 @@ export default {
returnAttachedImage,
updateImage
};
function sendSanitizedSvg(res: Response, content: string | Buffer) {
const svgString = typeof content === "string" ? content : content.toString("utf-8");
res.set("Content-Security-Policy", "script-src 'none'");
res.send(sanitizeSvg(svgString));
}

View File

@@ -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;
}

View File

@@ -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<string>("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;

View File

@@ -705,3 +705,110 @@ describe("#slugify", () => {
expect(result).toBe(expectedSlug);
});
});
describe("#sanitizeSvg", () => {
it("should remove script elements", () => {
const maliciousSvg = '<svg><script>alert("XSS")</script><rect width="100" height="100"/></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100" height="100"/></svg>');
});
it("should remove script elements with attributes", () => {
const maliciousSvg = '<svg><script type="text/javascript">alert("XSS")</script></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg></svg>');
});
it("should remove multiline script elements", () => {
const maliciousSvg = `<svg><script>
var x = 1;
alert(x);
</script></svg>`;
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg></svg>');
});
it("should remove onclick event handlers with double quotes", () => {
const maliciousSvg = '<svg><rect onclick="doEvil()" width="100"/></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100"/></svg>');
});
it("should remove onclick event handlers with single quotes", () => {
const maliciousSvg = "<svg><rect onclick='doEvil()' width=\"100\"/></svg>";
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100"/></svg>');
});
it("should remove onload event handlers", () => {
const maliciousSvg = '<svg onload="doEvil()"><rect width="100"/></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100"/></svg>');
});
it("should remove onerror event handlers", () => {
const maliciousSvg = '<svg><image onerror="alert(1)" href="invalid.jpg"/></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><image href="invalid.jpg"/></svg>');
});
it("should remove onmouseover event handlers", () => {
const maliciousSvg = '<svg><rect onmouseover="alert(1)" width="100"/></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100"/></svg>');
});
it("should remove event handlers without quotes", () => {
const maliciousSvg = '<svg><rect onclick=alert(1) width="100"/></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100"/></svg>');
});
it("should replace javascript: URLs in href with #", () => {
const maliciousSvg = '<svg><a href="javascript:alert(1)"><text>Click me</text></a></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><a href="#"><text>Click me</text></a></svg>');
});
it("should replace javascript: URLs in xlink:href with #", () => {
const maliciousSvg = '<svg><a xlink:href="javascript:alert(1)"><text>Click me</text></a></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><a xlink:href="#"><text>Click me</text></a></svg>');
});
it("should preserve valid SVG content", () => {
const validSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="10" y="10" width="80" height="80" fill="blue"/><circle cx="50" cy="50" r="30" fill="red"/></svg>';
const result = utils.sanitizeSvg(validSvg);
expect(result).toBe(validSvg);
});
it("should preserve valid href URLs", () => {
const validSvg = '<svg><a href="https://example.com"><text>Link</text></a></svg>';
const result = utils.sanitizeSvg(validSvg);
expect(result).toBe(validSvg);
});
it("should handle multiple malicious elements", () => {
const maliciousSvg = '<svg onload="evil()"><script>evil()</script><rect onclick="bad()" width="100"/><a href="javascript:attack()">link</a></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100"/><a href="#">link</a></svg>');
});
it("should handle empty SVG", () => {
const emptySvg = '<svg></svg>';
const result = utils.sanitizeSvg(emptySvg);
expect(result).toBe('<svg></svg>');
});
it("should be case insensitive for script tags", () => {
const maliciousSvg = '<svg><SCRIPT>alert(1)</SCRIPT><Script>alert(2)</Script></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg></svg>');
});
it("should be case insensitive for event handlers", () => {
const maliciousSvg = '<svg><rect ONCLICK="alert(1)" width="100"/></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100"/></svg>');
});
});

View File

@@ -119,6 +119,22 @@ export function sanitizeSqlIdentifier(str: string) {
return str.replace(/[^A-Za-z0-9_]/g, "");
}
/**
* Sanitize SVG to remove potentially dangerous elements and attributes.
* This prevents XSS via script injection in SVG content.
*/
export function sanitizeSvg(svg: string): string {
return svg
// Remove script elements
.replace(/<script[\s\S]*?<\/script>/gi, '')
// Remove on* event handlers (onclick, onload, onerror, etc.)
.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '')
.replace(/\s+on\w+\s*=\s*[^\s>]+/gi, '')
// Remove javascript: URLs
.replace(/href\s*=\s*["']javascript:[^"']*["']/gi, 'href="#"')
.replace(/xlink:href\s*=\s*["']javascript:[^"']*["']/gi, 'xlink:href="#"');
}
export const escapeHtml = escape;
export const unescapeHtml = unescape;
@@ -556,6 +572,7 @@ export default {
replaceAll,
safeExtractMessageAndStackFromError,
sanitizeSqlIdentifier,
sanitizeSvg,
stripTags,
slugify,
timeLimit,

View File

@@ -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")) {
@@ -102,9 +102,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);
}

View File

@@ -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",

View File

@@ -32,4 +32,8 @@
## 🛠️ Technical updates
* \[…\]
## 🔒️ Security improvements
* \[…\]

View File

@@ -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.

View File

@@ -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"

View File

@@ -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 );

View File

@@ -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",

View File

@@ -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" },

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/pdfjs-viewer",
"version": "0.102.1",
"version": "0.102.2",
"private": true,
"scripts": {
"build": "tsx scripts/build.ts",

85
pnpm-lock.yaml generated
View File

@@ -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@1.8.0)
'@electron/fuses':
specifier: 1.8.0
version: 1.8.0
'@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,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'}
@@ -16058,6 +16074,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 +16216,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 +16418,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 +16429,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 +16440,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 +16475,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 +16531,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 +16555,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:
@@ -16633,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': {}
@@ -16666,8 +16690,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:
@@ -16679,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': {}
@@ -16690,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:
@@ -16716,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:
@@ -16741,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:
@@ -16780,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:
@@ -16791,8 +16803,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 +16815,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 +16823,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 +16877,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 +16996,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 +17043,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 +17055,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 +17129,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 +17165,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 +17184,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 +17607,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
@@ -17697,6 +17698,12 @@ snapshots:
glob: 7.2.3
minimatch: 3.1.2
'@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(supports-color@8.1.1)
@@ -23148,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: