VYPR
High severityNVD Advisory· Published Jun 10, 2025· Updated Jun 17, 2025

CVE-2024-57186

CVE-2024-57186

Description

In Erxes <1.6.2, an unauthenticated attacker can read arbitrary files from the system using a Path Traversal vulnerability in the /read-file endpoint handler.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

CVE-2024-57186: Unauthenticated path traversal in Erxes <1.6.2 allows arbitrary file read via /read-file endpoint.

Vulnerability

Description

CVE-2024-57186 describes an unauthenticated path traversal vulnerability in Erxes versions prior to 1.6.2. The flaw exists in the /read-file endpoint handler, where user-controlled input is used to construct a filesystem path without proper sanitization. This allows an attacker to read arbitrary files on the system by manipulating path segments with sequences like ../ [1][4].

Exploitation

The endpoint is accessible without authentication, meaning any remote attacker can send crafted HTTP requests to traverse directories. No privileges or previous access are required. The vulnerability was discovered by SonarSource researchers during a code audit of the Erxes microservices platform, which is a source-available experience management solution [1]. The commit that introduced the fix (d626070) added a sanitizeFilename function to validate file names before constructing paths, addressing the root cause [3].

Impact

A successful exploit enables an attacker to read any file on the server that the application process can access, including configuration files, environment variables, and potentially authentication secrets. As noted in SonarSource's analysis, chaining this path traversal with other vulnerabilities like Redis SSRF could allow an attacker to take full control of an Erxes instance [1].

Mitigation

The vulnerability has been fixed in Erxes version 1.6.3. Users should upgrade immediately. No workarounds are documented; upgrading to the patched version is the recommended mitigation. The fix is included in commit d626070, which sanitizes filenames before use in file operations [2][3].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
erxesnpm
< 1.6.21.6.2

Affected products

2
  • Erxes/Erxesdescription
  • Erxes/Erxesllm-create
    Range: <1.6.2

Patches

1
d626070a0fcd

upload and read file name update to sanitized

https://github.com/erxes/erxesgerelsukhFeb 20, 2024via ghsa
3 files changed · +64 35
  • packages/core/src/data/utils.ts+33 19 modified
    @@ -22,6 +22,7 @@ import {
     import { graphqlPubsub } from '../pubsub';
     import { getService, getServices } from '@erxes/api-utils/src/serviceDiscovery';
     import redis from '@erxes/api-utils/src/redis';
    +import sanitizeFilename from '@erxes/api-utils/src/sanitize-filename';
     
     export interface IEmailParams {
       toEmails?: string[];
    @@ -478,6 +479,8 @@ const uploadToCFImages = async (
       forcePrivate?: boolean,
       models?: IModels,
     ) => {
    +  const sanitizedFilename = sanitizeFilename(file.name);
    +
       const CLOUDFLARE_ACCOUNT_ID = await getConfig(
         'CLOUDFLARE_ACCOUNT_ID',
         '',
    @@ -505,7 +508,7 @@ const uploadToCFImages = async (
         Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}`,
       };
     
    -  let fileName = `${Math.random()}${file.name.replace(/ /g, '')}`;
    +  let fileName = `${Math.random()}${sanitizedFilename}`;
       const extension = fileName.split('.').pop();
     
       if (extension && ['JPEG', 'JPG', 'PNG'].includes(extension)) {
    @@ -542,6 +545,8 @@ const uploadToCFImages = async (
     
     // upload file to Cloudflare stream
     const uploadToCFStream = async (file: any, models?: IModels) => {
    +  const sanitizedFilename = sanitizeFilename(file.name);
    +
       const CLOUDFLARE_ACCOUNT_ID = await getConfig(
         'CLOUDFLARE_ACCOUNT_ID',
         '',
    @@ -559,7 +564,7 @@ const uploadToCFStream = async (file: any, models?: IModels) => {
         Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}`,
       };
     
    -  const fileName = `${Math.random()}${file.name.replace(/ /g, '')}`;
    +  const fileName = `${Math.random()}${sanitizedFilename}`;
     
       const formData = new FormData();
       formData.append('file', fs.createReadStream(file.path));
    @@ -589,6 +594,8 @@ export const uploadFileCloudflare = async (
       forcePrivate: boolean = false,
       models?: IModels,
     ): Promise<string> => {
    +  const sanitizedFilename = sanitizeFilename(file.name);
    +
       const CLOUDFLARE_BUCKET = await getConfig(
         'CLOUDFLARE_BUCKET_NAME',
         '',
    @@ -625,7 +632,7 @@ export const uploadFileCloudflare = async (
         : await getConfig('FILE_SYSTEM_PUBLIC', 'true', models);
     
       // generate unique name
    -  const fileName = `${Math.random()}${file.name.replace(/ /g, '')}`;
    +  const fileName = `${Math.random()}${sanitizedFilename.replace(/ /g, '')}`;
     
       // read file
       const buffer = await fs.readFileSync(file.path);
    @@ -663,6 +670,8 @@ export const uploadFileAWS = async (
       forcePrivate: boolean = false,
       models?: IModels,
     ): Promise<string> => {
    +  const sanitizedFilename = sanitizeFilename(file.name);
    +
       const IS_PUBLIC = forcePrivate
         ? false
         : await getConfig('FILE_SYSTEM_PUBLIC', 'true', models);
    @@ -674,10 +683,7 @@ export const uploadFileAWS = async (
     
       // generate unique name
     
    -  const fileName = `${AWS_PREFIX}${Math.random()}${file.name.replace(
    -    / /g,
    -    '',
    -  )}`;
    +  const fileName = `${AWS_PREFIX}${Math.random()}${sanitizedFilename}`;
     
       // read file
       const buffer = await fs.readFileSync(file.path);
    @@ -763,13 +769,15 @@ export const uploadFileLocal = async (file: {
       path: string;
       type: string;
     }): Promise<string> => {
    +  const sanitizedFilename = sanitizeFilename(file.name);
    +
       const oldPath = file.path;
     
       if (!fs.existsSync(uploadsFolderPath)) {
         fs.mkdirSync(uploadsFolderPath);
       }
     
    -  const fileName = `${Math.random()}${file.name.replace(/ /g, '')}`;
    +  const fileName = `${Math.random()}${sanitizedFilename}`;
       const newPath = `${uploadsFolderPath}/${fileName}`;
       const rawData = fs.readFileSync(oldPath);
     
    @@ -795,6 +803,8 @@ export const uploadFileGCS = async (
       },
       models: IModels,
     ): Promise<string> => {
    +  const sanitizedFilename = sanitizeFilename(file.name);
    +
       const BUCKET = await getConfig('GOOGLE_CLOUD_STORAGE_BUCKET', '', models);
       const IS_PUBLIC = await getConfig('FILE_SYSTEM_PUBLIC', '', models);
     
    @@ -805,7 +815,7 @@ export const uploadFileGCS = async (
       const bucket = storage.bucket(BUCKET);
     
       // generate unique name
    -  const fileName = `${Math.random()}${file.name}`;
    +  const fileName = `${Math.random()}${sanitizedFilename}`;
     
       bucket.file(fileName);
     
    @@ -969,6 +979,7 @@ export const readFileRequest = async ({
       width?: number;
     }): Promise<any> => {
       const services = await getServices();
    +  const sanitizedFileKey = sanitizeFilename(key);
     
       for (const serviceName of services) {
         const service = await getService(serviceName);
    @@ -1001,7 +1012,7 @@ export const readFileRequest = async ({
     
         const bucket = storage.bucket(GCS_BUCKET);
     
    -    const file = bucket.file(key);
    +    const file = bucket.file(sanitizedFileKey);
     
         // get a file buffer
         const [contents] = await file.download({});
    @@ -1017,7 +1028,7 @@ export const readFileRequest = async ({
           s3.getObject(
             {
               Bucket: AWS_BUCKET,
    -          Key: key,
    +          Key: sanitizedFileKey,
             },
             (error, response) => {
               if (error) {
    @@ -1026,7 +1037,7 @@ export const readFileRequest = async ({
                   error.message.includes('key does not exist')
                 ) {
                   debugBase(
    -                `Error occurred when fetching s3 file with key: "${key}"`,
    +                `Error occurred when fetching s3 file with key: "${sanitizedFileKey}"`,
                   );
                 }
     
    @@ -1053,18 +1064,21 @@ export const readFileRequest = async ({
           return readFromCFImages(key, width, models);
         }
     
    -    return readFromCR2(key, models);
    +    return readFromCR2(sanitizedFileKey, models);
       }
     
       if (UPLOAD_SERVICE_TYPE === 'local') {
         return new Promise((resolve, reject) => {
    -      fs.readFile(`${uploadsFolderPath}/${key}`, (error, response) => {
    -        if (error) {
    -          return reject(error);
    -        }
    +      fs.readFile(
    +        `${uploadsFolderPath}/${sanitizedFileKey}`,
    +        (error, response) => {
    +          if (error) {
    +            return reject(error);
    +          }
     
    -        return resolve(response);
    -      });
    +          return resolve(response);
    +        },
    +      );
         });
       }
     };
    
  • packages/workers/src/data/utils.ts+13 7 modified
    @@ -8,6 +8,7 @@ import { getFileUploadConfigs } from '../messageBroker';
     import { getService } from '@erxes/api-utils/src/serviceDiscovery';
     import fetch from 'node-fetch';
     import { pipeline } from 'node:stream/promises';
    +import sanitizeFilename from '@erxes/api-utils/src/sanitize-filename';
     
     export const uploadsFolderPath = path.join(__dirname, '../private/uploads');
     
    @@ -128,10 +129,11 @@ export const createCFR2 = async (subdomain) => {
     
     export const getImportCsvInfo = async (subdomain, fileName: string) => {
       const { UPLOAD_SERVICE_TYPE } = await getFileUploadConfigs(subdomain);
    +  const sanitizedFilename = sanitizeFilename(fileName);
     
       const service: any = await getService('core');
     
    -  const url = `${service.address}/get-import-file/${fileName}`;
    +  const url = `${service.address}/get-import-file/${sanitizedFilename}`;
     
       try {
         const response = await fetch(url);
    @@ -141,11 +143,11 @@ export const getImportCsvInfo = async (subdomain, fileName: string) => {
         }
         await pipeline(
           response.body,
    -      fs.createWriteStream(`${uploadsFolderPath}/${fileName}`),
    +      fs.createWriteStream(`${uploadsFolderPath}/${sanitizedFilename}`),
         );
       } catch (e) {
         console.error(
    -      `${service.name} csv download from ${url} to ${uploadsFolderPath}/${fileName} failed.`,
    +      `${service.name} csv download from ${url} to ${uploadsFolderPath}/${sanitizedFilename} failed.`,
           e.message,
         );
       }
    @@ -156,7 +158,7 @@ export const getImportCsvInfo = async (subdomain, fileName: string) => {
           let i = 0;
     
           const readStream = fs.createReadStream(
    -        `${uploadsFolderPath}/${fileName}`,
    +        `${uploadsFolderPath}/${sanitizedFilename}`,
           );
     
           readStream
    @@ -179,6 +181,7 @@ export const getImportCsvInfo = async (subdomain, fileName: string) => {
         } else {
           const { AWS_BUCKET, CLOUDFLARE_BUCKET_NAME } =
             await getFileUploadConfigs(subdomain);
    +
           const s3 =
             UPLOAD_SERVICE_TYPE === 'AWS'
               ? await createAWS(subdomain)
    @@ -187,7 +190,7 @@ export const getImportCsvInfo = async (subdomain, fileName: string) => {
           const bucket =
             UPLOAD_SERVICE_TYPE === 'AWS' ? AWS_BUCKET : CLOUDFLARE_BUCKET_NAME;
     
    -      const params = { Bucket: bucket, Key: fileName };
    +      const params = { Bucket: bucket, Key: sanitizedFilename };
     
           const request = s3.getObject(params);
           const readStream = request.createReadStream();
    @@ -219,10 +222,13 @@ export const getImportCsvInfo = async (subdomain, fileName: string) => {
     
     export const getCsvHeadersInfo = async (subdomain, fileName: string) => {
       const { UPLOAD_SERVICE_TYPE } = await getFileUploadConfigs(subdomain);
    +  const sanitizedFilename = sanitizeFilename(fileName);
     
       return new Promise(async (resolve) => {
         if (UPLOAD_SERVICE_TYPE === 'local') {
    -      const readSteam = fs.createReadStream(`${uploadsFolderPath}/${fileName}`);
    +      const readSteam = fs.createReadStream(
    +        `${uploadsFolderPath}/${sanitizedFilename}`,
    +      );
     
           let columns;
           let total = 0;
    @@ -259,7 +265,7 @@ export const getCsvHeadersInfo = async (subdomain, fileName: string) => {
           const bucket =
             UPLOAD_SERVICE_TYPE === 'AWS' ? AWS_BUCKET : CLOUDFLARE_BUCKET_NAME;
     
    -      const params = { Bucket: bucket, Key: fileName };
    +      const params = { Bucket: bucket, Key: sanitizedFilename };
           // exclude column
     
           const columns = await getS3FileInfo({
    
  • packages/workers/src/worker/import/utils.ts+18 9 modified
    @@ -65,6 +65,7 @@ const getCsvInfo = (
     ) => {
       return new Promise(async (resolve) => {
         let readSteam;
    +    const sanitizedFilename = sanitizeFilename(fileName);
     
         if (uploadType !== 'local') {
           const { AWS_BUCKET, CLOUDFLARE_BUCKET_NAME } =
    @@ -77,8 +78,7 @@ const getCsvInfo = (
     
           const bucket = uploadType === 'AWS' ? AWS_BUCKET : CLOUDFLARE_BUCKET_NAME;
     
    -      const params = { Bucket: bucket, Key: fileName };
    -
    +      const params = { Bucket: bucket, Key: sanitizedFilename };
           const file = (await s3.getObject(params).promise()) as any;
     
           try {
    @@ -87,16 +87,20 @@ const getCsvInfo = (
             }
     
             await fs.promises.writeFile(
    -          `${uploadsFolderPath}/${fileName}`,
    +          `${uploadsFolderPath}/${sanitizedFilename}`,
               file.Body,
             );
           } catch (e) {
             console.error(e.message);
           }
     
    -      readSteam = fs.createReadStream(`${uploadsFolderPath}/${fileName}`);
    +      readSteam = fs.createReadStream(
    +        `${uploadsFolderPath}/${sanitizedFilename}`,
    +      );
         } else {
    -      readSteam = fs.createReadStream(`${uploadsFolderPath}/${fileName}`);
    +      readSteam = fs.createReadStream(
    +        `${uploadsFolderPath}/${sanitizedFilename}`,
    +      );
         }
     
         let columns;
    @@ -152,6 +156,7 @@ const importBulkStream = ({
         let rows: any = [];
         let readSteam;
         let rowIndex = 0;
    +    const sanitizedFilename = sanitizeFilename(fileName);
     
         if (uploadType !== 'local') {
           const { AWS_BUCKET, CLOUDFLARE_BUCKET_NAME } =
    @@ -164,18 +169,22 @@ const importBulkStream = ({
     
           const bucket = uploadType === 'AWS' ? AWS_BUCKET : CLOUDFLARE_BUCKET_NAME;
     
    -      const params = { Bucket: bucket, Key: fileName };
    +      const params = { Bucket: bucket, Key: sanitizedFilename };
     
           const file = (await s3.getObject(params).promise()) as any;
     
           await fs.promises.writeFile(
    -        `${uploadsFolderPath}/${fileName}`,
    +        `${uploadsFolderPath}/${sanitizedFilename}`,
             file.Body,
           );
     
    -      readSteam = fs.createReadStream(`${uploadsFolderPath}/${fileName}`);
    +      readSteam = fs.createReadStream(
    +        `${uploadsFolderPath}/${sanitizedFilename}`,
    +      );
         } else {
    -      readSteam = fs.createReadStream(`${uploadsFolderPath}/${fileName}`);
    +      readSteam = fs.createReadStream(
    +        `${uploadsFolderPath}/${sanitizedFilename}`,
    +      );
         }
     
         const write = (row, _, next) => {
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.