UnoPim Quick Export feature is vulnerable to CSV injection
Description
UnoPim is an open-source Product Information Management (PIM) system built on the Laravel framework. Versions 0.3.0 and prior are vulnerable to CSV injection, also known as formula injection, in the Quick Export feature. This vulnerability allows attackers to inject malicious content into exported CSV files. When the CSV file is opened in spreadsheet applications such as Microsoft Excel, the malicious input may be interpreted as a formula or command, potentially resulting in the execution of arbitrary code on the victim's device. Successful exploitation can lead to remote code execution, including the establishment of a reverse shell. Users are advised to upgrade to version 0.3.1 or later.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
UnoPim versions ≤0.3.0 are vulnerable to CSV formula injection in the Quick Export feature, allowing attackers to execute arbitrary code when a malicious CSV is opened in Excel.
Vulnerability
Overview
CVE-2025-55745 is a CSV injection (formula injection) vulnerability in the Quick Export feature of UnoPim, an open-source Laravel-based Product Information Management (PIM) system. The vulnerability exists in versions 0.3.0 and prior. The root cause is that the application does not sanitize or escape certain characters—specifically =, -, +, and @—when exporting product data to CSV files [1][2]. These characters are treated as formula prefixes by spreadsheet applications such as Microsoft Excel.
Exploitation and
Attack Vector
An attacker with the ability to influence product data that gets exported (for example, by creating or editing product attributes with specially crafted values) can inject malicious formula payloads into exported CSV files. No authentication is required on the victim side; the attack primarily targets operators who open the exported CSV file in a desktop spreadsheet application. The injected formula may trigger macro-like behavior, such as executing external commands via the DDE or CMD protocols, leading to remote code execution on the victim's machine [1].
Impact
Successful exploitation can lead to arbitrary code execution on the user's system when the malicious CSV is opened. The official advisory notes that this can include establishing a reverse shell, giving the attacker full control over the victim's device [1]. The impact is limited to users who open the exported file, but it poses a significant risk in environments where exported CSVs are commonly shared or reviewed.
Mitigation
The vendor has patched the vulnerability by introducing an EscapeFormulaOperators class that prepends a single quote to values starting with the dangerous operators, effectively neutralizing formula injection [2][3]. The fix was merged via pull request #193 and is included in version 0.3.1 [1][3]. Users are strongly advised to upgrade to version 0.3.1 or later. As a workaround, users can manually inspect CSV files and avoid opening them in spreadsheet applications that interpret formulas.
AI Insight generated on May 19, 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 |
|---|---|---|
unopim/unopimPackagist | < 0.3.1 | 0.3.1 |
Affected products
2- unopim/unopimv5Range: < 0.3.1
Patches
2b25db9496fc1Merge pull request #193 from devansh-pal-webkul/fix/escape-csv-formula-operators
7 files changed · +113 −2
packages/Webkul/DataTransfer/src/Helpers/Exporters/Category/Exporter.php+3 −0 modified@@ -8,6 +8,7 @@ use Webkul\DataTransfer\Contracts\JobTrackBatch as JobTrackBatchContract; use Webkul\DataTransfer\Helpers\Export; use Webkul\DataTransfer\Helpers\Exporters\AbstractExporter; +use Webkul\DataTransfer\Helpers\Formatters\EscapeFormulaOperators; use Webkul\DataTransfer\Jobs\Export\File\FlatItemBuffer as FileExportFileBuffer; use Webkul\DataTransfer\Repositories\JobTrackBatchRepository; @@ -128,6 +129,8 @@ protected function setFieldsAdditionalData(array $additionalData, $filePath, $op $this->copyMedia($exitingFilePath, $newfilePath); } } + + $fieldValues[$field->code] = EscapeFormulaOperators::escapeValue($additionalData[$field->code] ?? null); } return $fieldValues;
packages/Webkul/DataTransfer/src/Helpers/Exporters/Product/Exporter.php+2 −1 modified@@ -9,6 +9,7 @@ use Webkul\DataTransfer\Contracts\JobTrackBatch as JobTrackBatchContract; use Webkul\DataTransfer\Helpers\Export; use Webkul\DataTransfer\Helpers\Exporters\AbstractExporter; +use Webkul\DataTransfer\Helpers\Formatters\EscapeFormulaOperators; use Webkul\DataTransfer\Helpers\Sources\Export\ProductSource; use Webkul\DataTransfer\Jobs\Export\File\FlatItemBuffer as FileExportFileBuffer; use Webkul\DataTransfer\Repositories\JobTrackBatchRepository; @@ -249,7 +250,7 @@ protected function setAttributesValues(array $values, mixed $filePath) $rawValue = implode(', ', $rawValue); } - $attributeValues[$code] = $rawValue; + $attributeValues[$code] = EscapeFormulaOperators::escapeValue($rawValue); } return $attributeValues;
packages/Webkul/DataTransfer/src/Helpers/Formatters/EscapeFormulaOperators.php+50 −0 added@@ -0,0 +1,50 @@ +<?php + +namespace Webkul\DataTransfer\Helpers\Formatters; + +class EscapeFormulaOperators +{ + public static $operatorsToEscape = ['=', '-', '+', '@']; + + /** + * Escape the value by adding a single quote at the beginning and end + * if it starts with a dangerous operator. + */ + public static function escapeValue(mixed $value): mixed + { + if (! is_string($value)) { + return $value; + } + + $trimmedValue = ltrim($value); + + if ($trimmedValue !== '' && in_array($trimmedValue[0], self::$operatorsToEscape, true)) { + return "'".$value."'"; + } + + return $value; + } + + /** + * Unescape the value by removing surrounding single quotes if it was escaped. + */ + public static function unescapeValue(mixed $value): mixed + { + if (! is_string($value)) { + return $value; + } + + $trimmedValue = ltrim($value); + + if ( + strlen($trimmedValue) >= 2 + && in_array($trimmedValue[1], self::$operatorsToEscape, true) + && $trimmedValue[0] === "'" + && $trimmedValue[strlen($trimmedValue) - 1] === "'" + ) { + return substr($trimmedValue, 1, -1); + } + + return $value; + } +}
packages/Webkul/DataTransfer/src/Helpers/Importers/Category/Importer.php+3 −0 modified@@ -13,6 +13,7 @@ use Webkul\Core\Repositories\LocaleRepository; use Webkul\Core\Rules\Code; use Webkul\DataTransfer\Contracts\JobTrackBatch as JobTrackBatchContract; +use Webkul\DataTransfer\Helpers\Formatters\EscapeFormulaOperators; use Webkul\DataTransfer\Helpers\Import; use Webkul\DataTransfer\Helpers\Importers\AbstractImporter; use Webkul\DataTransfer\Helpers\Importers\FieldProcessor; @@ -369,6 +370,8 @@ public function prepareCategories(array $rowData, array &$categories): void $value = $this->fieldProcessor->handleField($catalogField, $value, $imageDirPath); + $value = EscapeFormulaOperators::unescapeValue($value); + if ($catalogField->value_per_locale === self::VALUE_PER_LOCALE) { $locale = $rowData['locale'] ?? null; if ($locale) {
packages/Webkul/DataTransfer/src/Helpers/Importers/Product/Importer.php+3 −0 modified@@ -22,6 +22,7 @@ use Webkul\Core\Repositories\ChannelRepository; use Webkul\Core\Rules\Slug; use Webkul\DataTransfer\Contracts\JobTrackBatch as JobTrackBatchContract; +use Webkul\DataTransfer\Helpers\Formatters\EscapeFormulaOperators; use Webkul\DataTransfer\Helpers\Import; use Webkul\DataTransfer\Helpers\Importers\AbstractImporter; use Webkul\DataTransfer\Helpers\Importers\FieldProcessor; @@ -791,6 +792,8 @@ public function prepareAttributeValues(array $rowData, array &$attributeValues): $value = $this->formatPriceValueWithCurrency($currencyCode, $value, $attribute->getValueFromProductValues($attributeValues, $rowData['channel'] ?? null, $rowData['locale'] ?? null)); } + $value = EscapeFormulaOperators::unescapeValue($value); + $attribute->setProductValue($value, $attributeValues, $rowData['channel'] ?? null, $rowData['locale'] ?? null); } }
packages/Webkul/DataTransfer/tests/Unit/Helpers/Formatters/EscapeFormulaOperatorsTest.php+50 −0 added@@ -0,0 +1,50 @@ +<?php + +use Webkul\DataTransfer\Helpers\Formatters\EscapeFormulaOperators; + +it('should escape string when it starts with a dangerous formula operator', function () { + expect(EscapeFormulaOperators::escapeValue('=SUM(A1:A2)'))->toBe("'=SUM(A1:A2)'"); + expect(EscapeFormulaOperators::escapeValue('+123456'))->toBe("'+123456'"); + expect(EscapeFormulaOperators::escapeValue('-42'))->toBe("'-42'"); + expect(EscapeFormulaOperators::escapeValue('@cmd'))->toBe("'@cmd'"); +}); + +it('should not escape string when it starts with a safe character', function () { + expect(EscapeFormulaOperators::escapeValue('Hello, world!'))->toBe('Hello, world!'); + expect(EscapeFormulaOperators::escapeValue('1234'))->toBe('1234'); + expect(EscapeFormulaOperators::escapeValue('text@example.com'))->toBe('text@example.com'); +}); + +it('should not escape value when it is not a string', function () { + expect(EscapeFormulaOperators::escapeValue(123))->toBe(123); + expect(EscapeFormulaOperators::escapeValue(null))->toBeNull(); + expect(EscapeFormulaOperators::escapeValue(['=SUM(A1:A2)']))->toBe(['=SUM(A1:A2)']); +}); + +it('should escape string when it starts with whitespace followed by a dangerous operator', function () { + expect(EscapeFormulaOperators::escapeValue(' =HACK()'))->toBe("' =HACK()'"); + expect(EscapeFormulaOperators::escapeValue(' +123'))->toBe("' +123'"); +}); + +it('should unescape string when it is wrapped in single quotes and starts with a dangerous operator', function () { + expect(EscapeFormulaOperators::unescapeValue("'=SUM(A1:A2)'"))->toBe('=SUM(A1:A2)'); + expect(EscapeFormulaOperators::unescapeValue("'-10'"))->toBe('-10'); +}); + +it('should not unescape string when it does not match escape pattern', function () { + expect(EscapeFormulaOperators::unescapeValue('Normal text'))->toBe('Normal text'); + expect(EscapeFormulaOperators::unescapeValue("'Unmatched"))->toBe("'Unmatched"); + expect(EscapeFormulaOperators::unescapeValue("Still unmatched'"))->toBe("Still unmatched'"); + expect(EscapeFormulaOperators::unescapeValue("'aNormal'"))->toBe("'aNormal'"); +}); + +it('should not unescape value when it is not a string', function () { + expect(EscapeFormulaOperators::unescapeValue(123))->toBe(123); + expect(EscapeFormulaOperators::unescapeValue(null))->toBeNull(); + expect(EscapeFormulaOperators::unescapeValue(['=SUM(A1:A2)']))->toBe(['=SUM(A1:A2)']); +}); + +it('should return empty string when given empty input', function () { + expect(EscapeFormulaOperators::escapeValue(''))->toBe(''); + expect(EscapeFormulaOperators::unescapeValue(''))->toBe(''); +});
packages/Webkul/Product/src/Normalizer/ProductAttributeValuesNormalizer.php+2 −1 modified@@ -3,6 +3,7 @@ namespace Webkul\Product\Normalizer; use Webkul\Attribute\Services\AttributeService; +use Webkul\DataTransfer\Helpers\Formatters\EscapeFormulaOperators; use Webkul\Product\Type\AbstractType; /** @@ -49,7 +50,7 @@ public function normalizeAttributes(array $attributeValues, array $options = []) $value = implode(', ', $value); } - $values[$attributeCode] = $value; + $values[$attributeCode] = EscapeFormulaOperators::escapeValue($value); } return $values;
8325b7856741fix: escape formula operators when exporting to CSV/XLSX files
7 files changed · +113 −2
packages/Webkul/DataTransfer/src/Helpers/Exporters/Category/Exporter.php+3 −0 modified@@ -8,6 +8,7 @@ use Webkul\DataTransfer\Contracts\JobTrackBatch as JobTrackBatchContract; use Webkul\DataTransfer\Helpers\Export; use Webkul\DataTransfer\Helpers\Exporters\AbstractExporter; +use Webkul\DataTransfer\Helpers\Formatters\EscapeFormulaOperators; use Webkul\DataTransfer\Jobs\Export\File\FlatItemBuffer as FileExportFileBuffer; use Webkul\DataTransfer\Repositories\JobTrackBatchRepository; @@ -128,6 +129,8 @@ protected function setFieldsAdditionalData(array $additionalData, $filePath, $op $this->copyMedia($exitingFilePath, $newfilePath); } } + + $fieldValues[$field->code] = EscapeFormulaOperators::escapeValue($additionalData[$field->code] ?? null); } return $fieldValues;
packages/Webkul/DataTransfer/src/Helpers/Exporters/Product/Exporter.php+2 −1 modified@@ -9,6 +9,7 @@ use Webkul\DataTransfer\Contracts\JobTrackBatch as JobTrackBatchContract; use Webkul\DataTransfer\Helpers\Export; use Webkul\DataTransfer\Helpers\Exporters\AbstractExporter; +use Webkul\DataTransfer\Helpers\Formatters\EscapeFormulaOperators; use Webkul\DataTransfer\Helpers\Sources\Export\ProductSource; use Webkul\DataTransfer\Jobs\Export\File\FlatItemBuffer as FileExportFileBuffer; use Webkul\DataTransfer\Repositories\JobTrackBatchRepository; @@ -249,7 +250,7 @@ protected function setAttributesValues(array $values, mixed $filePath) $rawValue = implode(', ', $rawValue); } - $attributeValues[$code] = $rawValue; + $attributeValues[$code] = EscapeFormulaOperators::escapeValue($rawValue); } return $attributeValues;
packages/Webkul/DataTransfer/src/Helpers/Formatters/EscapeFormulaOperators.php+50 −0 added@@ -0,0 +1,50 @@ +<?php + +namespace Webkul\DataTransfer\Helpers\Formatters; + +class EscapeFormulaOperators +{ + public static $operatorsToEscape = ['=', '-', '+', '@']; + + /** + * Escape the value by adding a single quote at the beginning and end + * if it starts with a dangerous operator. + */ + public static function escapeValue(mixed $value): mixed + { + if (! is_string($value)) { + return $value; + } + + $trimmedValue = ltrim($value); + + if ($trimmedValue !== '' && in_array($trimmedValue[0], self::$operatorsToEscape, true)) { + return "'".$value."'"; + } + + return $value; + } + + /** + * Unescape the value by removing surrounding single quotes if it was escaped. + */ + public static function unescapeValue(mixed $value): mixed + { + if (! is_string($value)) { + return $value; + } + + $trimmedValue = ltrim($value); + + if ( + strlen($trimmedValue) >= 2 + && in_array($trimmedValue[1], self::$operatorsToEscape, true) + && $trimmedValue[0] === "'" + && $trimmedValue[strlen($trimmedValue) - 1] === "'" + ) { + return substr($trimmedValue, 1, -1); + } + + return $value; + } +}
packages/Webkul/DataTransfer/src/Helpers/Importers/Category/Importer.php+3 −0 modified@@ -13,6 +13,7 @@ use Webkul\Core\Repositories\LocaleRepository; use Webkul\Core\Rules\Code; use Webkul\DataTransfer\Contracts\JobTrackBatch as JobTrackBatchContract; +use Webkul\DataTransfer\Helpers\Formatters\EscapeFormulaOperators; use Webkul\DataTransfer\Helpers\Import; use Webkul\DataTransfer\Helpers\Importers\AbstractImporter; use Webkul\DataTransfer\Helpers\Importers\FieldProcessor; @@ -369,6 +370,8 @@ public function prepareCategories(array $rowData, array &$categories): void $value = $this->fieldProcessor->handleField($catalogField, $value, $imageDirPath); + $value = EscapeFormulaOperators::unescapeValue($value); + if ($catalogField->value_per_locale === self::VALUE_PER_LOCALE) { $locale = $rowData['locale'] ?? null; if ($locale) {
packages/Webkul/DataTransfer/src/Helpers/Importers/Product/Importer.php+3 −0 modified@@ -22,6 +22,7 @@ use Webkul\Core\Repositories\ChannelRepository; use Webkul\Core\Rules\Slug; use Webkul\DataTransfer\Contracts\JobTrackBatch as JobTrackBatchContract; +use Webkul\DataTransfer\Helpers\Formatters\EscapeFormulaOperators; use Webkul\DataTransfer\Helpers\Import; use Webkul\DataTransfer\Helpers\Importers\AbstractImporter; use Webkul\DataTransfer\Helpers\Importers\FieldProcessor; @@ -791,6 +792,8 @@ public function prepareAttributeValues(array $rowData, array &$attributeValues): $value = $this->formatPriceValueWithCurrency($currencyCode, $value, $attribute->getValueFromProductValues($attributeValues, $rowData['channel'] ?? null, $rowData['locale'] ?? null)); } + $value = EscapeFormulaOperators::unescapeValue($value); + $attribute->setProductValue($value, $attributeValues, $rowData['channel'] ?? null, $rowData['locale'] ?? null); } }
packages/Webkul/DataTransfer/tests/Unit/Helpers/Formatters/EscapeFormulaOperatorsTest.php+50 −0 added@@ -0,0 +1,50 @@ +<?php + +use Webkul\DataTransfer\Helpers\Formatters\EscapeFormulaOperators; + +it('should escape string when it starts with a dangerous formula operator', function () { + expect(EscapeFormulaOperators::escapeValue('=SUM(A1:A2)'))->toBe("'=SUM(A1:A2)'"); + expect(EscapeFormulaOperators::escapeValue('+123456'))->toBe("'+123456'"); + expect(EscapeFormulaOperators::escapeValue('-42'))->toBe("'-42'"); + expect(EscapeFormulaOperators::escapeValue('@cmd'))->toBe("'@cmd'"); +}); + +it('should not escape string when it starts with a safe character', function () { + expect(EscapeFormulaOperators::escapeValue('Hello, world!'))->toBe('Hello, world!'); + expect(EscapeFormulaOperators::escapeValue('1234'))->toBe('1234'); + expect(EscapeFormulaOperators::escapeValue('text@example.com'))->toBe('text@example.com'); +}); + +it('should not escape value when it is not a string', function () { + expect(EscapeFormulaOperators::escapeValue(123))->toBe(123); + expect(EscapeFormulaOperators::escapeValue(null))->toBeNull(); + expect(EscapeFormulaOperators::escapeValue(['=SUM(A1:A2)']))->toBe(['=SUM(A1:A2)']); +}); + +it('should escape string when it starts with whitespace followed by a dangerous operator', function () { + expect(EscapeFormulaOperators::escapeValue(' =HACK()'))->toBe("' =HACK()'"); + expect(EscapeFormulaOperators::escapeValue(' +123'))->toBe("' +123'"); +}); + +it('should unescape string when it is wrapped in single quotes and starts with a dangerous operator', function () { + expect(EscapeFormulaOperators::unescapeValue("'=SUM(A1:A2)'"))->toBe('=SUM(A1:A2)'); + expect(EscapeFormulaOperators::unescapeValue("'-10'"))->toBe('-10'); +}); + +it('should not unescape string when it does not match escape pattern', function () { + expect(EscapeFormulaOperators::unescapeValue('Normal text'))->toBe('Normal text'); + expect(EscapeFormulaOperators::unescapeValue("'Unmatched"))->toBe("'Unmatched"); + expect(EscapeFormulaOperators::unescapeValue("Still unmatched'"))->toBe("Still unmatched'"); + expect(EscapeFormulaOperators::unescapeValue("'aNormal'"))->toBe("'aNormal'"); +}); + +it('should not unescape value when it is not a string', function () { + expect(EscapeFormulaOperators::unescapeValue(123))->toBe(123); + expect(EscapeFormulaOperators::unescapeValue(null))->toBeNull(); + expect(EscapeFormulaOperators::unescapeValue(['=SUM(A1:A2)']))->toBe(['=SUM(A1:A2)']); +}); + +it('should return empty string when given empty input', function () { + expect(EscapeFormulaOperators::escapeValue(''))->toBe(''); + expect(EscapeFormulaOperators::unescapeValue(''))->toBe(''); +});
packages/Webkul/Product/src/Normalizer/ProductAttributeValuesNormalizer.php+2 −1 modified@@ -3,6 +3,7 @@ namespace Webkul\Product\Normalizer; use Webkul\Attribute\Services\AttributeService; +use Webkul\DataTransfer\Helpers\Formatters\EscapeFormulaOperators; use Webkul\Product\Type\AbstractType; /** @@ -49,7 +50,7 @@ public function normalizeAttributes(array $attributeValues, array $options = []) $value = implode(', ', $value); } - $values[$attributeCode] = $value; + $values[$attributeCode] = EscapeFormulaOperators::escapeValue($value); } return $values;
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-74rg-6f92-g6wxghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-55745ghsaADVISORY
- drive.proton.me/urls/3TP1QEMXNCghsaWEB
- github.com/unopim/unopim/commit/8325b78567411ad78d44c0385f192360e608ff71ghsaWEB
- github.com/unopim/unopim/commit/b25db9496fc147842a519d1dd42ec03c3bf00a34ghsax_refsource_MISCWEB
- github.com/unopim/unopim/security/advisories/GHSA-74rg-6f92-g6wxghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.