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].
- Micro Services, Major Headaches: Detecting Vulnerabilities in Erxes' Microservices
- GitHub - erxes/erxes: Experience Operating System (XOS) that unifies marketing, sales, operations, and support — run your core business seamlessly while replacing HubSpot, Zendesk, Linear, Wix and more.
- upload and read file name update to sanitized · erxes/erxes@d626070
- NVD - CVE-2024-57186
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.
| Package | Affected versions | Patched versions |
|---|---|---|
erxesnpm | < 1.6.2 | 1.6.2 |
Affected products
2- Erxes/Erxesdescription
Patches
1d626070a0fcdupload and read file name update to sanitized
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- github.com/advisories/GHSA-rq9r-qvwg-829qghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-57186ghsaADVISORY
- github.com/erxes/erxes/commit/d626070a0fcd435ae29e689aca051ccfb440c2f3ghsaWEB
- www.sonarsource.com/blog/micro-services-major-headaches-detecting-vulnerabilities-in-erxes-microservicesghsaWEB
- www.sonarsource.com/blog/micro-services-major-headaches-detecting-vulnerabilities-in-erxes-microservices/mitre
News mentions
0No linked articles in our index yet.