CVE-2021-41270 (published 2021-11-24T19:15Z)

Symfony/Serializer handles serializing and deserializing data structures for Symfony, a PHP framework for web and console applications and a set of reusable PHP components. Symfony versions 4.1.0 before 4.4.35 and versions 5.0.0 before 5.3.12 are vulnerable to CSV injection, also known as formula injection. In Symfony 4.1, maintainers added the opt-in `csv_escape_formulas` option in the `CsvEncoder`, to prefix all cells starting with `=`, `+`, `-` or `@` with a tab `\\t`. Since then, OWASP added 2 chars in that list: Tab (0x09) and Carriage return (0x0D). This makes the previous prefix char (Tab `\\t`) part of the vulnerable characters, and OWASP suggests using the single quote `'` for prefixing the value. Starting with versions 4.4.34 and 5.3.12, Symfony now follows the OWASP recommendations and uses the single quote `'` to prefix formulas and add the prefix to cells starting by `\\t`, `\\r` as well as `=`, `+`, `-` and `@`.

Severity: MEDIUM
CVSS v2 base score: 4.0 (AV:N/AC:L/Au:S/C:P/I:N/A:N)
CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-1236
cwe_name: Improper Neutralization of Formula Elements in a CSV File
description: The product saves user-provided information into a Comma-Separated Value (CSV) file, but it does not neutralize or incorrectly neutralizes special elements that could be interpreted as a command when the file is opened by a spreadsheet product.
extended_description: User-provided data is often saved to traditional databases.  This data can be exported to a CSV file, which allows users to read the data using spreadsheet software such as Excel, Numbers, or Calc.  This software interprets entries beginning with '=' as formulas, which are then executed by the spreadsheet software.  The software's formula language often allows methods to access hyperlinks or the local command line, and frequently allows enough characters to invoke an entire script. Attackers can populate data fields which, when saved to a CSV file, may attempt information exfiltration or other malicious activity when automatically executed by the spreadsheet software.
			

Fix information:
repo_url: https://github.com/symfony/symfony
commit_details: repo_url: https://github.com/symfony/symfony
author: Jérémy Derussé
author_date: 2021-11-15 11:47:04+01:00
msg: Use single quote to escape formulas
repository: repo_url: https://github.com/symfony/symfony
repo_name: symfony/symfony
description: The Symfony PHP framework
repo_language: PHP
owner: symfony
file_changes: filename: CsvEncoder.php
change_type: ModificationType.MODIFY
diff: @@ -35,7 +35,8 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
 
     private const UTF8_BOM = "\xEF\xBB\xBF";
 
-    private $formulasStartCharacters = ['=', '-', '+', '@'];
+    private const FORMULAS_START_CHARACTERS = ['=', '-', '+', '@', "\t", "\r"];
+
     private $defaultContext = [
         self::DELIMITER_KEY => ',',
         self::ENCLOSURE_KEY => '"',
@@ -238,8 +239,8 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
             if (is_iterable($value)) {
                 $this->flatten($value, $result, $keySeparator, $parentKey.$key.$keySeparator, $escapeFormulas);
             } else {
-                if ($escapeFormulas && \in_array(substr((string) $value, 0, 1), $this->formulasStartCharacters, true)) {
-                    $result[$parentKey.$key] = "\t".$value;
+                if ($escapeFormulas && \in_array(substr((string) $value, 0, 1), self::FORMULAS_START_CHARACTERS, true)) {
+                    $result[$parentKey.$key] = "'".$value;
                 } else {
                     // Ensures an actual value is used when dealing with true and false
                     $result[$parentKey.$key] = false === $value ? 0 : (true === $value ? 1 : $value);

programming_language: PHP
filename: CsvEncoderTest.php
change_type: ModificationType.MODIFY
diff: @@ -285,31 +285,52 @@ CSV;
 
         $this->assertSame(<<<'CSV'
 0
-"	=2+3"
+'=2+3
 
 CSV
             , $this->encoder->encode(['=2+3'], 'csv'));
 
         $this->assertSame(<<<'CSV'
 0
-"	-2+3"
+'-2+3
 
 CSV
             , $this->encoder->encode(['-2+3'], 'csv'));
 
         $this->assertSame(<<<'CSV'
 0
-"	+2+3"
+'+2+3
 
 CSV
             , $this->encoder->encode(['+2+3'], 'csv'));
 
         $this->assertSame(<<<'CSV'
 0
-"	@MyDataColumn"
+'@MyDataColumn
 
 CSV
             , $this->encoder->encode(['@MyDataColumn'], 'csv'));
+
+        $this->assertSame(<<<'CSV'
+0
+"'	tab"
+
+CSV
+            , $this->encoder->encode(["\ttab"], 'csv'));
+
+        $this->assertSame(<<<'CSV'
+0
+"'=1+2"";=1+2"
+
+CSV
+            , $this->encoder->encode(['=1+2";=1+2'], 'csv'));
+
+        $this->assertSame(<<<'CSV'
+0
+"'=1+2'"" ;,=1+2"
+
+CSV
+            , $this->encoder->encode(['=1+2\'" ;,=1+2'], 'csv'));
     }
 
     public function testDoNotEncodeFormulas()
@@ -341,13 +362,34 @@ CSV
 
 CSV
             , $this->encoder->encode(['@MyDataColumn'], 'csv'));
+
+        $this->assertSame(<<<'CSV'
+0
+"	tab"
+
+CSV
+            , $this->encoder->encode(["\ttab"], 'csv'));
+
+        $this->assertSame(<<<'CSV'
+0
+"=1+2"";=1+2"
+
+CSV
+            , $this->encoder->encode(['=1+2";=1+2'], 'csv'));
+
+        $this->assertSame(<<<'CSV'
+0
+"=1+2'"" ;,=1+2"
+
+CSV
+            , $this->encoder->encode(['=1+2\'" ;,=1+2'], 'csv'));
     }
 
     public function testEncodeFormulasWithSettingsPassedInContext()
     {
         $this->assertSame(<<<'CSV'
 0
-"	=2+3"
+'=2+3
 
 CSV
             , $this->encoder->encode(['=2+3'], 'csv', [
@@ -356,7 +398,7 @@ CSV
 
         $this->assertSame(<<<'CSV'
 0
-"	-2+3"
+'-2+3
 
 CSV
             , $this->encoder->encode(['-2+3'], 'csv', [
@@ -365,7 +407,7 @@ CSV
 
         $this->assertSame(<<<'CSV'
 0
-"	+2+3"
+'+2+3
 
 CSV
             , $this->encoder->encode(['+2+3'], 'csv', [
@@ -374,12 +416,39 @@ CSV
 
         $this->assertSame(<<<'CSV'
 0
-"	@MyDataColumn"
+'@MyDataColumn
 
 CSV
             , $this->encoder->encode(['@MyDataColumn'], 'csv', [
                 CsvEncoder::ESCAPE_FORMULAS_KEY => true,
             ]));
+
+        $this->assertSame(<<<'CSV'
+0
+"'	tab"
+
+CSV
+            , $this->encoder->encode(["\ttab"], 'csv', [
+                CsvEncoder::ESCAPE_FORMULAS_KEY => true,
+            ]));
+
+        $this->assertSame(<<<'CSV'
+0
+"'=1+2"";=1+2"
+
+CSV
+            , $this->encoder->encode(['=1+2";=1+2'], 'csv', [
+                CsvEncoder::ESCAPE_FORMULAS_KEY => true,
+            ]));
+
+        $this->assertSame(<<<'CSV'
+0
+"'=1+2'"" ;,=1+2"
+
+CSV
+            , $this->encoder->encode(['=1+2\'" ;,=1+2'], 'csv', [
+                CsvEncoder::ESCAPE_FORMULAS_KEY => true,
+            ]));
     }
 
     public function testEncodeWithoutHeader()

programming_language: PHP