Skip to content

Commit

Permalink
Merge pull request #49 from silverstripe-security/pulls/3.5/ss-2017-007
Browse files Browse the repository at this point in the history
[ss-2017-007] Ensure xls formulae are safely sanitised on output (3.5)
  • Loading branch information
Damian Mooyman authored Dec 6, 2017
2 parents 44de03d + 22ccf3e commit dd4c541
Show file tree
Hide file tree
Showing 6 changed files with 59 additions and 20 deletions.
4 changes: 3 additions & 1 deletion dev/CSVParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,9 @@ protected function fetchCSVRow() {
array($this->enclosure, $this->delimiter),
$value
);

// Trim leading tab
// [SS-2017-007] Ensure all cells with leading [@=+] have a leading tab
$value = ltrim($value, "\t");
if(array_key_exists($i, $this->headerRow)) {
if($this->headerRow[$i]) {
$row[$this->headerRow[$i]] = $value;
Expand Down
24 changes: 20 additions & 4 deletions forms/gridfield/GridFieldExportButton.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ class GridFieldExportButton implements GridField_HTMLProvider, GridField_ActionP
*/
protected $targetFragment;

/**
* Set to true to disable XLS sanitisation
* [SS-2017-007] Ensure all cells with leading [@=+] have a leading tab
*
* @config
* @var bool
*/
private static $xls_export_disabled = false;

/**
* @param string $targetFragment The HTML fragment to write the button into
* @param array $exportColumns The columns to include in the export
Expand Down Expand Up @@ -91,12 +100,12 @@ public function handleExport($gridField, $request = null) {
return SS_HTTPRequest::send_file($fileData, $fileName, 'text/csv');
}
}

/**
* Return the columns to export
*
* @param GridField $gridField
*
*
* @param GridField $gridField
*
* @return array
*/
protected function getExportColumnsForGridField(GridField $gridField) {
Expand Down Expand Up @@ -174,6 +183,13 @@ public function generateExportFileData($gridField) {
}

$value = str_replace(array("\r", "\n"), "\n", $value);

// [SS-2017-007] Sanitise XLS executable column values with a leading tab
if (!Config::inst()->get(get_class($this), 'xls_export_disabled')
&& preg_match('/^[-@=+].*/', $value)
) {
$value = "\t" . $value;
}
$columnData[] = '"' . str_replace('"', '""', $value) . '"';
}

Expand Down
28 changes: 16 additions & 12 deletions tests/dev/CSVParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,18 @@ public function testParsingWithHeaders() {
$registered[] = $record['IsRegistered'];
}

$this->assertEquals(array('John','Jane','Jamie','Järg'), $firstNames);
$this->assertEquals(array('John','Jane','Jamie','Järg','Jacob'), $firstNames);

$this->assertEquals(array(
"He's a good guy",
"She is awesome." . PHP_EOL
. "So awesome that she gets multiple rows and \"escaped\" strings in her biography",
"Pretty old, with an escaped comma",
"Unicode FTW"), $biographies);
$this->assertEquals(array("31/01/1988","31/01/1982","31/01/1882","31/06/1982"), $birthdays);
$this->assertEquals(array('1', '0', '1', '1'), $registered);
"Unicode FTW",
"Likes leading tabs in his biography",
), $biographies);
$this->assertEquals(array("31/01/1988","31/01/1982","31/01/1882","31/06/1982","31/4/2000"), $birthdays);
$this->assertEquals(array('1', '0', '1', '1', '0'), $registered);
}

public function testParsingWithHeadersAndColumnMap() {
Expand All @@ -54,15 +56,16 @@ public function testParsingWithHeadersAndColumnMap() {
$registered[] = $record['IsRegistered'];
}

$this->assertEquals(array('John','Jane','Jamie','Järg'), $firstNames);
$this->assertEquals(array('John','Jane','Jamie','Järg','Jacob'), $firstNames);
$this->assertEquals(array(
"He's a good guy",
"She is awesome."
. PHP_EOL . "So awesome that she gets multiple rows and \"escaped\" strings in her biography",
"Pretty old, with an escaped comma",
"Unicode FTW"), $biographies);
$this->assertEquals(array("31/01/1988","31/01/1982","31/01/1882","31/06/1982"), $birthdays);
$this->assertEquals(array('1', '0', '1', '1'), $registered);
"Unicode FTW",
"Likes leading tabs in his biography"), $biographies);
$this->assertEquals(array("31/01/1988","31/01/1982","31/01/1882","31/06/1982","31/4/2000"), $birthdays);
$this->assertEquals(array('1', '0', '1', '1', '0'), $registered);
}

public function testParsingWithExplicitHeaderRow() {
Expand All @@ -82,15 +85,16 @@ public function testParsingWithExplicitHeaderRow() {
}

/* And the first row will be returned in the data */
$this->assertEquals(array('FirstName','John','Jane','Jamie','Järg'), $firstNames);
$this->assertEquals(array('FirstName','John','Jane','Jamie','Järg','Jacob'), $firstNames);
$this->assertEquals(array(
'Biography',
"He's a good guy",
"She is awesome." . PHP_EOL
. "So awesome that she gets multiple rows and \"escaped\" strings in her biography",
"Pretty old, with an escaped comma",
"Unicode FTW"), $biographies);
$this->assertEquals(array("Birthday","31/01/1988","31/01/1982","31/01/1882","31/06/1982"), $birthdays);
$this->assertEquals(array('IsRegistered', '1', '0', '1', '1'), $registered);
"Unicode FTW",
"Likes leading tabs in his biography"), $biographies);
$this->assertEquals(array("Birthday","31/01/1988","31/01/1982","31/01/1882","31/06/1982","31/4/2000"), $birthdays);
$this->assertEquals(array('IsRegistered', '1', '0', '1', '1', '0'), $registered);
}
}
6 changes: 3 additions & 3 deletions tests/dev/CsvBulkLoaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public function testLoad() {
$results = $loader->load($filepath);

// Test that right amount of columns was imported
$this->assertEquals(4, $results->Count(), 'Test correct count of imported data');
$this->assertEquals(5, $results->Count(), 'Test correct count of imported data');

// Test that columns were correctly imported
$obj = DataObject::get_one("CsvBulkLoaderTest_Player", array(
Expand All @@ -49,14 +49,14 @@ public function testDeleteExistingRecords() {
$filepath = $this->getCurrentAbsolutePath() . '/CsvBulkLoaderTest_PlayersWithHeader.csv';
$loader->deleteExistingRecords = true;
$results1 = $loader->load($filepath);
$this->assertEquals(4, $results1->Count(), 'Test correct count of imported data on first load');
$this->assertEquals(5, $results1->Count(), 'Test correct count of imported data on first load');

//delete existing data before doing second CSV import
$results2 = $loader->load($filepath, '512MB', true);
//get all instances of the loaded DataObject from the database and count them
$resultDataObject = DataObject::get('CsvBulkLoaderTest_Player');

$this->assertEquals(4, $resultDataObject->Count(),
$this->assertEquals(5, $resultDataObject->Count(),
'Test if existing data is deleted before new data is added');
}

Expand Down
1 change: 1 addition & 0 deletions tests/dev/CsvBulkLoaderTest_PlayersWithHeader.csv
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
So awesome that she gets multiple rows and \"escaped\" strings in her biography","31/01/1982","0"
"Jamie","Pretty old\, with an escaped comma","31/01/1882","1"
"Järg","Unicode FTW","31/06/1982","1"
"Jacob"," Likes leading tabs in his biography","31/4/2000","0"
16 changes: 16 additions & 0 deletions tests/forms/gridfield/GridFieldExportButtonTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@ public function testGenerateFileDataBasicFields() {
);
}

public function testXLSSanitisation() {
// Create risky object
$object = new GridFieldExportButtonTest_Team();
$object->Name = '=SUM(1, 2)';
$object->write();

// Export
$button = new GridFieldExportButton();
$button->setExportColumns(array('Name' => 'My Name'));

$this->assertEquals(
"\"My Name\"\n\"\t=SUM(1, 2)\"\n\"Test\"\n\"Test2\"\n",
$button->generateExportFileData($this->gridField)
);
}

public function testGenerateFileDataAnonymousFunctionField() {
$button = new GridFieldExportButton();
$button->setExportColumns(array(
Expand Down

0 comments on commit dd4c541

Please sign in to comment.