CVE-2023-46119: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: Parse Server is an open source backend that can be deployed to any infrastructure that can run Node.js. Parse Server crashes when uploading a file without extension. This vulnerability has been patched in versions 5.5.6 and 6.3.1.


CWE description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
CVSS v3 base score: 7.5
Published: 2023-10-25T18:17Z
Repository: https://github.com/parse-community/parse-server
Language: JavaScript
Files changed: spec/ParseFile.spec.js, src/Routers/FilesRouter.js
Commit date: 2023-10-21 01:01:35 +0200
Commit message: fix: Server crash when uploading file without extension; fixes security vulnerability [GHSA-792q-q67h-w579](https://github.com/parse-community/parse-server/security/advisories/GHSA-792q-q67h-w579) (#8781)


Diff: diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js
index eeab537008..b4fc72bdbd 100644
--- a/spec/ParseFile.spec.js
+++ b/spec/ParseFile.spec.js
@@ -1362,10 +1362,38 @@ describe('Parse.File testing', () => {
       ).toBeRejectedWith(
         new Parse.Error(Parse.Error.FILE_SAVE_ERROR, `File upload of extension html is disabled.`)
       );
     });
 
+    it('allows file without extension', async () => {
+      await reconfigureServer({
+        fileUpload: {
+          enableForPublic: true,
+          fileExtensions: ['^[^hH][^tT][^mM][^lL]?$'],
+        },
+      });
+      const headers = {
+        'X-Parse-Application-Id': 'test',
+        'X-Parse-REST-API-Key': 'rest',
+      };
+
+      const values = ['filenamewithoutextension'];
+
+      for (const value of values) {
+        await expectAsync(
+          request({
+            method: 'POST',
+            headers: headers,
+            url: `http://localhost:8378/1/files/${value}`,
+            body: '<html></html>\n',
+          }).catch(e => {
+            throw new Error(e.data.error);
+          })
+        ).toBeResolved();
+      }
+    });
+
     it('works with array', async () => {
       await reconfigureServer({
         fileUpload: {
           enableForPublic: true,
           fileExtensions: ['jpg'],
diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js
index a5322b4c60..408dc81d8f 100644
--- a/src/Routers/FilesRouter.js
+++ b/src/Routers/FilesRouter.js
@@ -157,13 +157,13 @@ export class FilesRouter {
       if (filename && filename.includes('.')) {
         extension = filename.split('.')[1];
       } else if (contentType && contentType.includes('/')) {
         extension = contentType.split('/')[1];
       }
-      extension = extension.split(' ').join('');
+      extension = extension?.split(' ')?.join('');
 
-      if (!isValidExtension(extension)) {
+      if (extension && !isValidExtension(extension)) {
         next(
           new Parse.Error(
             Parse.Error.FILE_SAVE_ERROR,
             `File upload of extension ${extension} is disabled.`
           )

Vulnerable code:       extension = extension.split(' ').join('');
      if (!isValidExtension(extension)) {
Fixed code:     it('allows file without extension', async () => {
      await reconfigureServer({
        fileUpload: {
          enableForPublic: true,
          fileExtensions: ['^[^hH][^tT][^mM][^lL]?$'],
        },
      });
      const headers = {
        'X-Parse-Application-Id': 'test',
        'X-Parse-REST-API-Key': 'rest',
      };

      const values = ['filenamewithoutextension'];

      for (const value of values) {
        await expectAsync(
          request({
            method: 'POST',
            headers: headers,
            url: `http://localhost:8378/1/files/${value}`,
            body: '<html></html>\n',
          }).catch(e => {
            throw new Error(e.data.error);
          })
        ).toBeResolved();
      }
    });

      extension = extension?.split(' ')?.join('');
      if (extension && !isValidExtension(extension)) {