CVE-2024-57189
Description
In Erxes <1.6.2, an authenticated attacker can write to arbitrary files on the system using a Path Traversal vulnerability in the importHistoriesCreate GraphQL mutation handler.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A path traversal in Erxes ≤1.6.2 lets authenticated attackers write arbitrary files via the importHistoriesCreate GraphQL mutation.
Vulnerability — Path Traversal in GraphQL Mutation
In Erxes versions before 1.6.2, the importHistoriesCreate GraphQL mutation handler does not properly sanitize file paths provided by authenticated users. This allows an attacker to traverse outside the intended upload directory by supplying path traversal sequences (e.g., ../) in the file name or path parameter [1][4]. The flaw is rooted in the absence of input validation or a sanitization function such as the sanitizeFilename helper that was later added in the fix [3].
Exploitation — Remote Write with Low Privileges
An attacker with valid authentication—any authenticated user account—can exploit this by crafting a malicious GraphQL request to importHistoriesCreate that includes a path payload containing ../ sequences. No special privileges beyond a standard user account are required. The mutation processes the user-controlled path when writing uploaded files, enabling the attacker to select arbitrary destinations on the file system [1][4].
Impact — Potential Code Execution or Configuration Poisoning
By writing files outside the intended upload folder, the attacker could overwrite critical system files such as configuration files, application scripts, or web resources. If the application processes files from the written location (e.g., PHP, JavaScript, or template files), the attacker may achieve remote code execution. Even without code execution, overwriting configuration could alter application behavior or leak sensitive data [1]. SonarSource researchers demonstrated that chaining this path traversal with an authentication bypass in inter-service communications (separate vulnerability) could lead to full server compromise [1].
Mitigation — Patched in Erxes 1.6.2
The vulnerability was fixed in Erxes version 1.6.2 by introducing the sanitizeFilename utility function that strips path traversal sequences and other dangerous characters from file names passed to upload handlers [3]. Users should upgrade to Erxes 1.6.2 or later immediately. There is no known workaround for versions earlier than 1.6.2. The CVE is not currently listed on CISA's Known Exploited Vulnerabilities (KEV) catalog as of the publication date.
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-2977-5php-6789ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-57189ghsaADVISORY
- 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.