CVE-2022-2290: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Cross-site Scripting (XSS) - Reflected in GitHub repository zadam/trilium prior to 0.52.4, 0.53.1-beta.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 6.1
Published: 2022-07-03T06:15Z
Repository: https://github.com/zadam/trilium
Language: JavaScript
Files changed: package-lock.json, src/routes/api/export.js, src/routes/api/files.js, src/routes/api/image.js, src/routes/api/note_revisions.js, src/routes/custom.js, src/routes/routes.js, src/services/auth.js, src/share/routes.js
Commit date: 2022-07-01 00:01:29 +0200
Commit message: set correct content type for error messages

Diff: diff --git a/package-lock.json b/package-lock.json
index a873e3fa..132da101 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,14 +1,14 @@
 {
   "name": "trilium",
-  "version": "0.52.1-beta",
+  "version": "0.52.3",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "trilium",
-      "version": "0.52.1-beta",
+      "version": "0.52.3",
       "hasInstallScript": true,
       "license": "AGPL-3.0-only",
       "dependencies": {
         "@electron/remote": "2.0.8",
         "@excalidraw/excalidraw": "0.11.0",
diff --git a/src/routes/api/export.js b/src/routes/api/export.js
index fe772e49..6acb1995 100644
--- a/src/routes/api/export.js
+++ b/src/routes/api/export.js
@@ -13,11 +13,13 @@ function exportBranch(req, res) {
 
     if (!branch) {
         const message = `Cannot export branch ${branchId} since it does not exist.`;
         log.error(message);
 
-        res.status(500).send(message);
+        res.setHeader("Content-Type", "text/plain")
+            .status(500)
+            .send(message);
         return;
     }
 
     const taskContext = new TaskContext(taskId, 'export');
 
@@ -39,11 +41,13 @@ function exportBranch(req, res) {
         const message = "Export failed with following error: '" + e.message + "'. More details might be in the logs.";
         taskContext.reportError(message);
 
         log.error(message + e.stack);
 
-        res.status(500).send(message);
+        res.setHeader("Content-Type", "text/plain")
+            .status(500)
+            .send(message);
     }
 }
 
 module.exports = {
     exportBranch
diff --git a/src/routes/api/files.js b/src/routes/api/files.js
index 3aa2d51b..d28e8116 100644
--- a/src/routes/api/files.js
+++ b/src/routes/api/files.js
@@ -46,11 +46,13 @@ function getFilename(note) {
 
 function downloadNoteFile(noteId, res, contentDisposition = true) {
     const note = becca.getNote(noteId);
 
     if (!note) {
-        return res.status(404).send(`Note ${noteId} doesn't exist.`);
+        return res.setHeader("Content-Type", "text/plain")
+            .status(404)
+            .send(`Note ${noteId} doesn't exist.`);
     }
 
     if (note.isProtected && !protectedSessionService.isProtectedSessionAvailable()) {
         return res.status(401).send("Protected session not available");
     }
diff --git a/src/routes/api/image.js b/src/routes/api/image.js
index 7807f1fb..f5439569 100644
--- a/src/routes/api/image.js
+++ b/src/routes/api/image.js
@@ -18,24 +18,26 @@ function returnImage(req, res) {
         res.set('Content-Type', 'image/png');
         return res.send(fs.readFileSync(RESOURCE_DIR + '/db/image-deleted.png'));
     }
 
     /**
-     * special "image" type. the canvas is actually type application/json 
+     * special "image" type. the canvas is actually type application/json
      * to avoid bitrot and enable usage as referenced image the svg is included.
      */
     if (image.type === 'canvas') {
         const content = image.getContent();
         try {
             const data = JSON.parse(content);
-            
+
             const svg = data.svg || '<svg />'
             res.set('Content-Type', "image/svg+xml");
             res.set("Cache-Control", "no-cache, no-store, must-revalidate");
             res.send(svg);
         } catch(err) {
-            res.status(500).send("there was an error parsing excalidraw to svg");
+            res.setHeader("Content-Type", "text/plain")
+                .status(500)
+                .send("there was an error parsing excalidraw to svg");
         }
     } else {
         res.set('Content-Type', image.mime);
         res.set("Cache-Control", "no-cache, no-store, must-revalidate");
         res.send(image.getContent());
diff --git a/src/routes/api/note_revisions.js b/src/routes/api/note_revisions.js
index 6052e995..db1b6579 100644
--- a/src/routes/api/note_revisions.js
+++ b/src/routes/api/note_revisions.js
@@ -63,15 +63,19 @@ function getRevisionFilename(noteRevision) {
 
 function downloadNoteRevision(req, res) {
     const noteRevision = becca.getNoteRevision(req.params.noteRevisionId);
 
     if (noteRevision.noteId !== req.params.noteId) {
-        return res.status(400).send(`Note revision ${req.params.noteRevisionId} does not belong to note ${req.params.noteId}`);
+        return res.setHeader("Content-Type", "text/plain")
+            .status(400)
+            .send(`Note revision ${req.params.noteRevisionId} does not belong to note ${req.params.noteId}`);
     }
 
     if (noteRevision.isProtected && !protectedSessionService.isProtectedSessionAvailable()) {
-        return res.status(401).send("Protected session not available");
+        return res.setHeader("Content-Type", "text/plain")
+            .status(401)
+            .send("Protected session not available");
     }
 
     const filename = getRevisionFilename(noteRevision);
 
     res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
diff --git a/src/routes/custom.js b/src/routes/custom.js
index 9b190405..7a6ee9c4 100644
--- a/src/routes/custom.js
+++ b/src/routes/custom.js
@@ -47,11 +47,13 @@ function handleRequest(req, res) {
                 });
             }
             catch (e) {
                 log.error(`Custom handler ${note.noteId} failed with ${e.message}`);
 
-                res.status(500).send(e.message);
+                res.setHeader("Content-Type", "text/plain")
+                    .status(500)
+                    .send(e.message);
             }
         }
         else if (attr.name === 'customResourceProvider') {
             fileUploadService.downloadNoteFile(attr.noteId, res);
         }
@@ -63,11 +65,13 @@ function handleRequest(req, res) {
     }
 
     const message = `No handler matched for custom ${path} request.`;
 
     log.info(message);
-    res.status(404).send(message);
+    res.setHeader("Content-Type", "text/plain")
+        .status(404)
+        .send(message);
 }
 
 function register(router) {
     // explicitly no CSRF middleware since it's meant to allow integration from external services
 
diff --git a/src/routes/routes.js b/src/routes/routes.js
index b51f1a52..b10ca45e 100644
--- a/src/routes/routes.js
+++ b/src/routes/routes.js
@@ -118,10 +118,14 @@ function apiResultHandler(req, res, result) {
     }
 }
 
 function send(res, statusCode, response) {
     if (typeof response === 'string') {
+        if (statusCode >= 400) {
+            res.setHeader("Content-Type", "text/plain");
+        }
+
         res.status(statusCode).send(response);
 
         return response.length;
     }
     else {
@@ -165,11 +169,13 @@ function route(method, path, middleware, routeHandler, resultHandler, transactio
                             log.request(req, res, Date.now() - start, responseLength);
                         })
                         .catch(e => {
                             log.error(`${method} ${path} threw exception: ` + e.stack);
 
-                            res.status(500).send(e.message);
+                            res.setHeader("Content-Type", "text/plain")
+                                .status(500)
+                                .send(e.message);
                         });
                 }
                 else {
                     const responseLength = resultHandler(req, res, result);
 
@@ -178,11 +184,13 @@ function route(method, path, middleware, routeHandler, resultHandler, transactio
             }
         }
         catch (e) {
             log.error(`${method} ${path} threw exception: ` + e.stack);
 
-            res.status(500).send(e.message);
+            res.setHeader("Content-Type", "text/plain")
+                .status(500)
+                .send(e.message);
         }
     });
 }
 
 const GET = 'get', POST = 'post', PUT = 'put', PATCH = 'patch', DELETE = 'delete';
diff --git a/src/services/auth.js b/src/services/auth.js
index 3b36fa51..331f1d25 100644
--- a/src/services/auth.js
+++ b/src/services/auth.js
@@ -86,32 +86,40 @@ function checkEtapiToken(req, res, next) {
 }
 
 function reject(req, res, message) {
     log.info(`${req.method} ${req.path} rejected with 401 ${message}`);
 
-    res.status(401).send(message);
+    res.setHeader("Content-Type", "text/plain")
+        .status(401)
+        .send(message);
 }
 
 function checkCredentials(req, res, next) {
     if (!sqlInit.isDbInitialized()) {
-        res.status(400).send('Database is not initialized yet.');
+        res.setHeader("Content-Type", "text/plain")
+            .status(400)
+            .send('Database is not initialized yet.');
         return;
     }
 
     if (!passwordService.isPasswordSet()) {
-        res.status(400).send('Password has not been set yet. Please set a password and repeat the action');
+        res.setHeader("Content-Type", "text/plain")
+            .status(400)
+            .send('Password has not been set yet. Please set a password and repeat the action');
         return;
     }
 
     const header = req.headers['trilium-cred'] || '';
     const auth = new Buffer.from(header, 'base64').toString();
     const [username, password] = auth.split(/:/);
 
     // username is ignored
 
     if (!passwordEncryptionService.verifyPassword(password)) {
-        res.status(401).send('Incorrect password');
+        res.setHeader("Content-Type", "text/plain")
+            .status(401)
+            .send('Incorrect password');
     }
     else {
         next();
     }
 }
diff --git a/src/share/routes.js b/src/share/routes.js
index ce858ad6..74ff02e0 100644
--- a/src/share/routes.js
+++ b/src/share/routes.js
@@ -37,13 +37,13 @@ function register(router) {
         }
 
         addNoIndexHeader(note, res);
 
         if (note.hasLabel('shareRaw') || ['image', 'file'].includes(note.type)) {
-            res.setHeader('Content-Type', note.mime);
+            res.setHeader('Content-Type', note.mime)
+                .send(note.getContent());
 
-            res.send(note.getContent());
             return;
         }
 
         const {header, content, isEmpty} = contentRenderer.getContent(note);
 
@@ -81,11 +81,13 @@ function register(router) {
 
         const {noteId} = req.params;
         const note = shaca.getNote(noteId);
 
         if (!note) {
-            return res.status(404).send(`Note '${noteId}' not found`);
+            return res.setHeader("Content-Type", "text/plain")
+                .status(404)
+                .send(`Note '${noteId}' not found`);
         }
 
         addNoIndexHeader(note, res);
 
         res.json(note.getPojoWithAttributes());
@@ -96,11 +98,13 @@ function register(router) {
 
         const {noteId} = req.params;
         const note = shaca.getNote(noteId);
 
         if (!note) {
-            return res.status(404).send(`Note '${noteId}' not found`);
+            return res.setHeader("Content-Type", "text/plain")
+                .status(404)
+                .send(`Note '${noteId}' not found`);
         }
 
         addNoIndexHeader(note, res);
 
         const utils = require("../services/utils");
@@ -120,17 +124,21 @@ function register(router) {
         shacaLoader.ensureLoad();
 
         const image = shaca.getNote(req.params.noteId);
 
         if (!image) {
-            return res.status(404).send(`Note '${req.params.noteId}' not found`);
+            return res.setHeader('Content-Type', 'text/plain')
+                .status(404)
+                .send(`Note '${req.params.noteId}' not found`);
         }
         else if (!["image", "canvas"].includes(image.type)) {
-            return res.status(400).send("Requested note is not a shareable image");
+            return res.setHeader('Content-Type', 'text/plain')
+                .status(400)
+                .send("Requested note is not a shareable image");
         } else if (image.type === "canvas") {
             /**
-             * special "image" type. the canvas is actually type application/json 
+             * special "image" type. the canvas is actually type application/json
              * to avoid bitrot and enable usage as referenced image the svg is included.
              */
             const content = image.getContent();
             try {
                 const data = JSON.parse(content);
@@ -139,11 +147,13 @@ function register(router) {
                 addNoIndexHeader(image, res);
                 res.set('Content-Type', "image/svg+xml");
                 res.set("Cache-Control", "no-cache, no-store, must-revalidate");
                 res.send(svg);
             } catch(err) {
-                res.status(500).send("there was an error parsing excalidraw to svg");
+                res.setHeader('Content-Type', 'text/plain')
+                    .status(500)
+                    .send("there was an error parsing excalidraw to svg");
             }
         } else {
             // normal image
             res.set('Content-Type', image.mime);
             addNoIndexHeader(image, res);
@@ -157,11 +167,13 @@ function register(router) {
 
         const {noteId} = req.params;
         const note = shaca.getNote(noteId);
 
         if (!note) {
-            return res.status(404).send(`Note '${noteId}' not found`);
+            return res.setHeader('Content-Type', 'text/plain')
+                .status(404)
+                .send(`Note '${noteId}' not found`);
         }
 
         addNoIndexHeader(note, res);
 
         res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");

Vulnerable code:   "version": "0.52.1-beta",
      "version": "0.52.1-beta",
        res.status(500).send(message);
        res.status(500).send(message);
        return res.status(404).send(`Note ${noteId} doesn't exist.`);
     * special "image" type. the canvas is actually type application/json 
            
            res.status(500).send("there was an error parsing excalidraw to svg");
        return res.status(400).send(`Note revision ${req.params.noteRevisionId} does not belong to note ${req.params.noteId}`);
        return res.status(401).send("Protected session not available");
                res.status(500).send(e.message);
    res.status(404).send(message);
                            res.status(500).send(e.message);
            res.status(500).send(e.message);
    res.status(401).send(message);
        res.status(400).send('Database is not initialized yet.');
        res.status(400).send('Password has not been set yet. Please set a password and repeat the action');
        res.status(401).send('Incorrect password');
            res.setHeader('Content-Type', note.mime);
            res.send(note.getContent());
            return res.status(404).send(`Note '${noteId}' not found`);
            return res.status(404).send(`Note '${noteId}' not found`);
            return res.status(404).send(`Note '${req.params.noteId}' not found`);
            return res.status(400).send("Requested note is not a shareable image");
             * special "image" type. the canvas is actually type application/json 
                res.status(500).send("there was an error parsing excalidraw to svg");
            return res.status(404).send(`Note '${noteId}' not found`);
Fixed code:   "version": "0.52.3",
      "version": "0.52.3",
        res.setHeader("Content-Type", "text/plain")
            .status(500)
            .send(message);
        res.setHeader("Content-Type", "text/plain")
            .status(500)
            .send(message);
        return res.setHeader("Content-Type", "text/plain")
            .status(404)
            .send(`Note ${noteId} doesn't exist.`);
     * special "image" type. the canvas is actually type application/json

            res.setHeader("Content-Type", "text/plain")
                .status(500)
                .send("there was an error parsing excalidraw to svg");
        return res.setHeader("Content-Type", "text/plain")
            .status(400)
            .send(`Note revision ${req.params.noteRevisionId} does not belong to note ${req.params.noteId}`);
        return res.setHeader("Content-Type", "text/plain")
            .status(401)
            .send("Protected session not available");
                res.setHeader("Content-Type", "text/plain")
                    .status(500)
                    .send(e.message);
    res.setHeader("Content-Type", "text/plain")
        .status(404)
        .send(message);
        if (statusCode >= 400) {
            res.setHeader("Content-Type", "text/plain");
        }

                            res.setHeader("Content-Type", "text/plain")
                                .status(500)
                                .send(e.message);
            res.setHeader("Content-Type", "text/plain")
                .status(500)
                .send(e.message);
    res.setHeader("Content-Type", "text/plain")
        .status(401)
        .send(message);
        res.setHeader("Content-Type", "text/plain")
            .status(400)
            .send('Database is not initialized yet.');
        res.setHeader("Content-Type", "text/plain")
            .status(400)
            .send('Password has not been set yet. Please set a password and repeat the action');
        res.setHeader("Content-Type", "text/plain")
            .status(401)
            .send('Incorrect password');
            res.setHeader('Content-Type', note.mime)
                .send(note.getContent());
            return res.setHeader("Content-Type", "text/plain")
                .status(404)
                .send(`Note '${noteId}' not found`);
            return res.setHeader("Content-Type", "text/plain")
                .status(404)
                .send(`Note '${noteId}' not found`);
            return res.setHeader('Content-Type', 'text/plain')
                .status(404)
                .send(`Note '${req.params.noteId}' not found`);
            return res.setHeader('Content-Type', 'text/plain')
                .status(400)
                .send("Requested note is not a shareable image");
             * special "image" type. the canvas is actually type application/json
                res.setHeader('Content-Type', 'text/plain')
                    .status(500)
                    .send("there was an error parsing excalidraw to svg");
            return res.setHeader('Content-Type', 'text/plain')
                .status(404)
                .send(`Note '${noteId}' not found`);