Trilium: Arbitrary file disclosure and OOM crash via `file://` in a note image URL

An authenticated user without special rights or any holder of an ETAPI token stores an image URL with the file:// scheme in a text note; the server reads the target file unvalidated and stores its content as a downloadable attachment.

Advisory ID: TP-2026-047
Product: Trilium (open-source note-taking and knowledge base application with a server and a desktop component)
Vulnerability type: Arbitrary file disclosure and denial of service (CWE-73)
CVE: CVE-2026-53580
CVSS 3.1: 8.1 (High) · CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:H
Vendor advisory: GHSA-3gxr-j6g6-q75c
Affected versions: trilium < 0.104.0 (verified on v0.103.0, Docker)
Fixed in: 0.104.0
Reported: 23 May 2026

Summary

Trilium is an open-source note-taking and knowledge base application; the server downloads images referenced in note content automatically and stores them as an attachment. The image downloader accepts file:// URLs alongside http(s) and forwards the path part to fs.readFile() without any check. An authenticated user without special rights or any holder of an ETAPI token therefore stores <img src="file:///etc/passwd"> in a text note; the server reads the file, saves the bytes as an attachment, and delivers them through the download endpoint. This exposes session_secret.txt, the entire SQLite database document.db, and configuration files, which allows forging session cookies and decrypting protected notes offline; pointing the URL at /dev/zero makes the unbounded read exhaust memory and terminates the process (OOM). turingpoint verified the chain live against a running Trilium instance (byte-exact md5sum match) and reported it to the vendor, which fixed it in 0.104.0.

Root cause

The file:// branch of downloadImage() in apps/server/src/services/notes.ts:624-635 reads imageUrl.substring("file://".length) directly with fs.readFile(), with no canonicalization, allowlist, realpath check, or size limit. For the URL to survive to the sink at all, packages/commons/src/lib/shared_constants.ts:17 keeps the scheme 'file' in ALLOWED_PROTOCOLS, so sanitize-html passes the <img src="file://…"> tag through unchanged. The download fires on every authenticated save of a text note, because the option downloadImagesAutomatically in services/options_init.ts:161 defaults to true and downloadImages() (services/notes.ts:666-712) then fetches every <img> source. The fetched bytes are stored as an attachment via imageService.saveImageToAttachment() even when sharp does not recognize them as an image, and are afterwards retrievable raw at GET /api/attachments/:id/download (routes/api/files.ts). The path is reachable through PUT /api/notes/:noteId/data (session + CSRF), PUT /etapi/notes/:noteId/content (Bearer token, no CSRF), the web clipper, and the ZIP import, and requires only authentication without administrator rights.

Proof of Concept

# 1. Obtain an ETAPI token (session + CSRF assumed):
POST /api/etapi-tokens          {"tokenName":"poc"}

# 2. Write a file:// URL into the note content, the download fires automatically:
PUT /etapi/notes/<noteId>/content
Authorization: <etapi-token>

<p><img src="file:///home/node/trilium-data/session_secret.txt"></p>

# 3. Download the created attachment raw:
GET  /api/notes/<noteId>/attachments            -> attachmentId
GET  /api/attachments/<attachmentId>/download   -> file content

The server reads the path with fs.readFile(), stores the bytes as an image attachment (even when it is not an image), and returns them unchanged at the download endpoint. Pointing the src URL at file:///dev/zero instead of a file makes the read run unbounded and terminates the Node process through memory exhaustion.

Impact

  • Reading any file accessible to the Node process (uid 1000 in the default Docker image), including session_secret.txt, document.db, and configuration files.
  • Forging session cookies and CSRF tokens after exfiltrating session_secret.txt, which takes over an administrator session offline.
  • Offline decryption of protected notes and offline brute-force of the password using the hashes and keys obtained from document.db.
  • Denial of service through unbounded memory allocation via file:///dev/zero, which terminates the server process by OOM.

References

Is Something Like This in Your Software?

Our team found this vulnerability in the course of its work. Have your applications reviewed by the same specialists, with a penetration test from turingpoint.