Fix security issue on CsvEncoder

This commit is contained in:
Mathieu Santostefano 2017-10-10 15:00:30 +02:00
parent 8776ccee03
commit a1b0bdbbac
No known key found for this signature in database
GPG Key ID: EB610773AF2B5B5B
3 changed files with 120 additions and 7 deletions

View File

@ -8,6 +8,7 @@ CHANGELOG
of objects that needs data insertion in constructor of objects that needs data insertion in constructor
* added an optional `default_constructor_arguments` option of context to specify a default data in * added an optional `default_constructor_arguments` option of context to specify a default data in
case the object is not initializable by its constructor because of data missing case the object is not initializable by its constructor because of data missing
* added optional `bool $escapeFormulas = false` argument to `CsvEncoder::__construct`
4.0.0 4.0.0
----- -----

View File

@ -27,18 +27,22 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
const ESCAPE_CHAR_KEY = 'csv_escape_char'; const ESCAPE_CHAR_KEY = 'csv_escape_char';
const KEY_SEPARATOR_KEY = 'csv_key_separator'; const KEY_SEPARATOR_KEY = 'csv_key_separator';
const HEADERS_KEY = 'csv_headers'; const HEADERS_KEY = 'csv_headers';
const ESCAPE_FORMULAS_KEY = 'csv_escape_formulas';
private $delimiter; private $delimiter;
private $enclosure; private $enclosure;
private $escapeChar; private $escapeChar;
private $keySeparator; private $keySeparator;
private $escapeFormulas;
private $formulasStartCharacters = array('=', '-', '+', '@');
public function __construct(string $delimiter = ',', string $enclosure = '"', string $escapeChar = '\\', string $keySeparator = '.') public function __construct(string $delimiter = ',', string $enclosure = '"', string $escapeChar = '\\', string $keySeparator = '.', bool $escapeFormulas = false)
{ {
$this->delimiter = $delimiter; $this->delimiter = $delimiter;
$this->enclosure = $enclosure; $this->enclosure = $enclosure;
$this->escapeChar = $escapeChar; $this->escapeChar = $escapeChar;
$this->keySeparator = $keySeparator; $this->keySeparator = $keySeparator;
$this->escapeFormulas = $escapeFormulas;
} }
/** /**
@ -65,11 +69,11 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
} }
} }
list($delimiter, $enclosure, $escapeChar, $keySeparator, $headers) = $this->getCsvOptions($context); list($delimiter, $enclosure, $escapeChar, $keySeparator, $headers, $escapeFormulas) = $this->getCsvOptions($context);
foreach ($data as &$value) { foreach ($data as &$value) {
$flattened = array(); $flattened = array();
$this->flatten($value, $flattened, $keySeparator); $this->flatten($value, $flattened, $keySeparator, '', $escapeFormulas);
$value = $flattened; $value = $flattened;
} }
unset($value); unset($value);
@ -172,13 +176,17 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
/** /**
* Flattens an array and generates keys including the path. * Flattens an array and generates keys including the path.
*/ */
private function flatten(array $array, array &$result, string $keySeparator, string $parentKey = '') private function flatten(array $array, array &$result, string $keySeparator, string $parentKey = '', bool $escapeFormulas = false)
{ {
foreach ($array as $key => $value) { foreach ($array as $key => $value) {
if (is_array($value)) { if (is_array($value)) {
$this->flatten($value, $result, $keySeparator, $parentKey.$key.$keySeparator); $this->flatten($value, $result, $keySeparator, $parentKey.$key.$keySeparator, $escapeFormulas);
} else { } else {
$result[$parentKey.$key] = $value; if ($escapeFormulas && \in_array(substr($value, 0, 1), $this->formulasStartCharacters, true)) {
$result[$parentKey.$key] = "\t".$value;
} else {
$result[$parentKey.$key] = $value;
}
} }
} }
} }
@ -190,12 +198,13 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
$escapeChar = isset($context[self::ESCAPE_CHAR_KEY]) ? $context[self::ESCAPE_CHAR_KEY] : $this->escapeChar; $escapeChar = isset($context[self::ESCAPE_CHAR_KEY]) ? $context[self::ESCAPE_CHAR_KEY] : $this->escapeChar;
$keySeparator = isset($context[self::KEY_SEPARATOR_KEY]) ? $context[self::KEY_SEPARATOR_KEY] : $this->keySeparator; $keySeparator = isset($context[self::KEY_SEPARATOR_KEY]) ? $context[self::KEY_SEPARATOR_KEY] : $this->keySeparator;
$headers = isset($context[self::HEADERS_KEY]) ? $context[self::HEADERS_KEY] : array(); $headers = isset($context[self::HEADERS_KEY]) ? $context[self::HEADERS_KEY] : array();
$escapeFormulas = isset($context[self::ESCAPE_FORMULAS_KEY]) ? $context[self::ESCAPE_FORMULAS_KEY] : $this->escapeFormulas;
if (!is_array($headers)) { if (!is_array($headers)) {
throw new InvalidArgumentException(sprintf('The "%s" context variable must be an array or null, given "%s".', self::HEADERS_KEY, gettype($headers))); throw new InvalidArgumentException(sprintf('The "%s" context variable must be an array or null, given "%s".', self::HEADERS_KEY, gettype($headers)));
} }
return array($delimiter, $enclosure, $escapeChar, $keySeparator, $headers); return array($delimiter, $enclosure, $escapeChar, $keySeparator, $headers, $escapeFormulas);
} }
/** /**

View File

@ -173,6 +173,109 @@ CSV;
$this->assertEquals($csv, $this->encoder->encode($value, 'csv', $context)); $this->assertEquals($csv, $this->encoder->encode($value, 'csv', $context));
} }
public function testEncodeFormulas()
{
$this->encoder = new CsvEncoder(',', '"', '\\', '.', true);
$this->assertSame(<<<'CSV'
0
" =2+3"
CSV
, $this->encoder->encode(array('=2+3'), 'csv'));
$this->assertSame(<<<'CSV'
0
" -2+3"
CSV
, $this->encoder->encode(array('-2+3'), 'csv'));
$this->assertSame(<<<'CSV'
0
" +2+3"
CSV
, $this->encoder->encode(array('+2+3'), 'csv'));
$this->assertSame(<<<'CSV'
0
" @MyDataColumn"
CSV
, $this->encoder->encode(array('@MyDataColumn'), 'csv'));
}
public function testDoNotEncodeFormulas()
{
$this->assertSame(<<<'CSV'
0
=2+3
CSV
, $this->encoder->encode(array('=2+3'), 'csv'));
$this->assertSame(<<<'CSV'
0
-2+3
CSV
, $this->encoder->encode(array('-2+3'), 'csv'));
$this->assertSame(<<<'CSV'
0
+2+3
CSV
, $this->encoder->encode(array('+2+3'), 'csv'));
$this->assertSame(<<<'CSV'
0
@MyDataColumn
CSV
, $this->encoder->encode(array('@MyDataColumn'), 'csv'));
}
public function testEncodeFormulasWithSettingsPassedInContext()
{
$this->assertSame(<<<'CSV'
0
" =2+3"
CSV
, $this->encoder->encode(array('=2+3'), 'csv', array(
CsvEncoder::ESCAPE_FORMULAS_KEY => true,
)));
$this->assertSame(<<<'CSV'
0
" -2+3"
CSV
, $this->encoder->encode(array('-2+3'), 'csv', array(
CsvEncoder::ESCAPE_FORMULAS_KEY => true,
)));
$this->assertSame(<<<'CSV'
0
" +2+3"
CSV
, $this->encoder->encode(array('+2+3'), 'csv', array(
CsvEncoder::ESCAPE_FORMULAS_KEY => true,
)));
$this->assertSame(<<<'CSV'
0
" @MyDataColumn"
CSV
, $this->encoder->encode(array('@MyDataColumn'), 'csv', array(
CsvEncoder::ESCAPE_FORMULAS_KEY => true,
)));
}
public function testSupportsDecoding() public function testSupportsDecoding()
{ {
$this->assertTrue($this->encoder->supportsDecoding('csv')); $this->assertTrue($this->encoder->supportsDecoding('csv'));