CVE-2020-5299: Improper Neutralization of Special Elements used in a Command ('Command Injection') (CWE-77)
value: In OctoberCMS (october/october composer package) versions from 1.0.319 and before 1.0.466, any users with the ability to modify any data that could eventually be exported as a CSV file from the `ImportExportController` could potentially introduce a CSV injection into the data to cause the generated CSV export file to be malicious. This requires attackers to achieve the following before a successful attack can be completed: 1. Have found a vulnerability in the victims spreadsheet software of choice. 2. Control data that would potentially be exported through the `ImportExportController` by a theoretical victim. 3. Convince the victim to export above data as a CSV and run it in vulnerable spreadsheet software while also bypassing any sanity checks by said software. Issue has been patched in Build 466 (v1.0.466).
CWE description: The product constructs all or part of a command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended command when it is sent to a downstream component.
CVSS v3 base score: 5.1
Published: 2020-06-03T22:15Z
Repository: https://github.com/octobercms/library
Language: PHP
Files changed: src/Parse/League/EscapeFormula.php
Commit date: 2020-03-31 09:51:57 -0600
Commit message: Temporary workaround until the L6 upgrade can be merged in to use league/csv >= 9.1

Diff: diff --git a/src/Parse/League/EscapeFormula.php b/src/Parse/League/EscapeFormula.php
new file mode 100644
index 00000000..4e5ca796
--- /dev/null
+++ b/src/Parse/League/EscapeFormula.php
@@ -0,0 +1,145 @@
+<?php namespace October\Rain\Parse\League;
+
+/**
+ * League.Csv (https://csv.thephpleague.com)
+ *
+ * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+use InvalidArgumentException;
+
+/**
+ * A Formatter to tackle CSV Formula Injection.
+ *
+ * @see http://georgemauer.net/2017/10/07/csv-injection.html
+ */
+class EscapeFormula
+{
+    /**
+     * Spreadsheet formula starting character.
+     */
+    const FORMULA_STARTING_CHARS = ['=', '-', '+', '@'];
+
+    /**
+     * Effective Spreadsheet formula starting characters.
+     *
+     * @var array
+     */
+    protected $special_chars = [];
+
+    /**
+     * Escape character to escape each CSV formula field.
+     *
+     * @var string
+     */
+    protected $escape;
+
+    /**
+     * New instance.
+     *
+     * @param string   $escape        escape character to escape each CSV formula field
+     * @param string[] $special_chars additional spreadsheet formula starting characters
+     *
+     */
+    public function __construct(string $escape = "\t", array $special_chars = [])
+    {
+        $this->escape = $escape;
+        if ([] !== $special_chars) {
+            $special_chars = $this->filterSpecialCharacters(...$special_chars);
+        }
+
+        $chars = array_merge(self::FORMULA_STARTING_CHARS, $special_chars);
+        $chars = array_unique($chars);
+        $this->special_chars = array_fill_keys($chars, 1);
+    }
+
+    /**
+     * Filter submitted special characters.
+     *
+     * @param string ...$characters
+     *
+     * @throws InvalidArgumentException if the string is not a single character
+     *
+     * @return string[]
+     */
+    protected function filterSpecialCharacters(string ...$characters): array
+    {
+        foreach ($characters as $str) {
+            if (1 != strlen($str)) {
+                throw new InvalidArgumentException(sprintf('The submitted string %s must be a single character', $str));
+            }
+        }
+
+        return $characters;
+    }
+
+    /**
+     * Returns the list of character the instance will escape.
+     *
+     * @return string[]
+     */
+    public function getSpecialCharacters(): array
+    {
+        return array_keys($this->special_chars);
+    }
+
+    /**
+     * Returns the escape character.
+     */
+    public function getEscape(): string
+    {
+        return $this->escape;
+    }
+
+    /**
+     * League CSV formatter hook.
+     *
+     * @see escapeRecord
+     */
+    public function __invoke(array $record): array
+    {
+        return $this->escapeRecord($record);
+    }
+
+    /**
+     * Escape a CSV record.
+     */
+    public function escapeRecord(array $record): array
+    {
+        return array_map([$this, 'escapeField'], $record);
+    }
+
+    /**
+     * Escape a CSV cell if its content is stringable.
+     *
+     * @param mixed $cell the content of the cell
+     *
+     * @return mixed|string the escaped content
+     */
+    protected function escapeField($cell)
+    {
+        if (!$this->isStringable($cell)) {
+            return $cell;
+        }
+
+        $str_cell = (string) $cell;
+        if (isset($str_cell[0], $this->special_chars[$str_cell[0]])) {
+            return $this->escape.$str_cell;
+        }
+
+        return $cell;
+    }
+
+    /**
+     * Tells whether the submitted value is stringable.
+     *
+     * @param string|object $value
+     */
+    protected function isStringable($value): bool
+    {
+        return is_string($value) || method_exists($value, '__toString');
+    }
+}
\ No newline at end of file

Fixed code: <?php namespace October\Rain\Parse\League;

/**
 * League.Csv (https://csv.thephpleague.com)
 *
 * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

use InvalidArgumentException;

/**
 * A Formatter to tackle CSV Formula Injection.
 *
 * @see http://georgemauer.net/2017/10/07/csv-injection.html
 */
class EscapeFormula
{
    /**
     * Spreadsheet formula starting character.
     */
    const FORMULA_STARTING_CHARS = ['=', '-', '+', '@'];

    /**
     * Effective Spreadsheet formula starting characters.
     *
     * @var array
     */
    protected $special_chars = [];

    /**
     * Escape character to escape each CSV formula field.
     *
     * @var string
     */
    protected $escape;

    /**
     * New instance.
     *
     * @param string   $escape        escape character to escape each CSV formula field
     * @param string[] $special_chars additional spreadsheet formula starting characters
     *
     */
    public function __construct(string $escape = "\t", array $special_chars = [])
    {
        $this->escape = $escape;
        if ([] !== $special_chars) {
            $special_chars = $this->filterSpecialCharacters(...$special_chars);
        }

        $chars = array_merge(self::FORMULA_STARTING_CHARS, $special_chars);
        $chars = array_unique($chars);
        $this->special_chars = array_fill_keys($chars, 1);
    }

    /**
     * Filter submitted special characters.
     *
     * @param string ...$characters
     *
     * @throws InvalidArgumentException if the string is not a single character
     *
     * @return string[]
     */
    protected function filterSpecialCharacters(string ...$characters): array
    {
        foreach ($characters as $str) {
            if (1 != strlen($str)) {
                throw new InvalidArgumentException(sprintf('The submitted string %s must be a single character', $str));
            }
        }

        return $characters;
    }

    /**
     * Returns the list of character the instance will escape.
     *
     * @return string[]
     */
    public function getSpecialCharacters(): array
    {
        return array_keys($this->special_chars);
    }

    /**
     * Returns the escape character.
     */
    public function getEscape(): string
    {
        return $this->escape;
    }

    /**
     * League CSV formatter hook.
     *
     * @see escapeRecord
     */
    public function __invoke(array $record): array
    {
        return $this->escapeRecord($record);
    }

    /**
     * Escape a CSV record.
     */
    public function escapeRecord(array $record): array
    {
        return array_map([$this, 'escapeField'], $record);
    }

    /**
     * Escape a CSV cell if its content is stringable.
     *
     * @param mixed $cell the content of the cell
     *
     * @return mixed|string the escaped content
     */
    protected function escapeField($cell)
    {
        if (!$this->isStringable($cell)) {
            return $cell;
        }

        $str_cell = (string) $cell;
        if (isset($str_cell[0], $this->special_chars[$str_cell[0]])) {
            return $this->escape.$str_cell;
        }

        return $cell;
    }

    /**
     * Tells whether the submitted value is stringable.
     *
     * @param string|object $value
     */
    protected function isStringable($value): bool
    {
        return is_string($value) || method_exists($value, '__toString');
    }
}