VYPR
Low severityNVD Advisory· Published Aug 22, 2025· Updated Aug 22, 2025

UnoPim Quick Export feature is vulnerable to CSV injection

CVE-2025-55745

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.

PackageAffected versionsPatched versions
unopim/unopimPackagist
< 0.3.10.3.1

Affected products

2
  • UnoPim/UnoPimllm-fuzzy
    Range: <=0.3.0
  • unopim/unopimv5
    Range: < 0.3.1

Patches

2
b25db9496fc1

Merge pull request #193 from devansh-pal-webkul/fix/escape-csv-formula-operators

https://github.com/unopim/unopimNavneet KumarAug 18, 2025via ghsa
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;
    
8325b7856741

fix: escape formula operators when exporting to CSV/XLSX files

https://github.com/unopim/unopimdevansh.pal507Aug 18, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.