libxml_disable_entity_loader() changes global state so it should be used as local as possible

Fixes #801
Closes #802
Closes #803
This commit is contained in:
Philipp Kolesnikov 2018-12-17 13:52:04 +10:00 committed by Adrien Crivelli
parent 6a48b505b6
commit 8918888e7c
No known key found for this signature in database
GPG Key ID: B182FD79DC6DE92E
3 changed files with 57 additions and 27 deletions

View File

@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
- Fix LOOKUP function which was breaking on edge cases - [#796](https://github.com/PHPOffice/PhpSpreadsheet/issues/796) - Fix LOOKUP function which was breaking on edge cases - [#796](https://github.com/PHPOffice/PhpSpreadsheet/issues/796)
- Fix VLOOKUP with exact matches - [#809](https://github.com/PHPOffice/PhpSpreadsheet/pull/809) - Fix VLOOKUP with exact matches - [#809](https://github.com/PHPOffice/PhpSpreadsheet/pull/809)
- Support COUNTIFS multiple arguments - [#830](https://github.com/PHPOffice/PhpSpreadsheet/pull/830) - Support COUNTIFS multiple arguments - [#830](https://github.com/PHPOffice/PhpSpreadsheet/pull/830)
- Change `libxml_disable_entity_loader()` as shortly as possible - [#819](https://github.com/PHPOffice/PhpSpreadsheet/pull/819)
## [1.5.2] - 2018-11-25 ## [1.5.2] - 2018-11-25

View File

@ -13,13 +13,6 @@ class XmlScanner
*/ */
private $libxmlDisableEntityLoader = false; private $libxmlDisableEntityLoader = false;
/**
* Store the initial setting of libxmlDisableEntityLoader so that we can resore t later.
*
* @var bool
*/
private $previousLibxmlDisableEntityLoaderValue;
/** /**
* String used to identify risky xml elements. * String used to identify risky xml elements.
* *
@ -33,17 +26,6 @@ class XmlScanner
{ {
$this->pattern = $pattern; $this->pattern = $pattern;
$this->libxmlDisableEntityLoader = $this->identifyLibxmlDisableEntityLoaderAvailability(); $this->libxmlDisableEntityLoader = $this->identifyLibxmlDisableEntityLoaderAvailability();
if ($this->libxmlDisableEntityLoader) {
$this->previousLibxmlDisableEntityLoaderValue = libxml_disable_entity_loader(true);
}
}
public function __destruct()
{
if ($this->libxmlDisableEntityLoader) {
libxml_disable_entity_loader($this->previousLibxmlDisableEntityLoaderValue);
}
} }
public static function getInstance(Reader\IReader $reader) public static function getInstance(Reader\IReader $reader)
@ -95,6 +77,10 @@ class XmlScanner
*/ */
public function scan($xml) public function scan($xml)
{ {
if ($this->libxmlDisableEntityLoader) {
$previousLibxmlDisableEntityLoaderValue = libxml_disable_entity_loader(true);
}
$pattern = '/encoding="(.*?)"/'; $pattern = '/encoding="(.*?)"/';
$result = preg_match($pattern, $xml, $matches); $result = preg_match($pattern, $xml, $matches);
$charset = $result ? $matches[1] : 'UTF-8'; $charset = $result ? $matches[1] : 'UTF-8';
@ -105,12 +91,19 @@ class XmlScanner
// Don't rely purely on libxml_disable_entity_loader() // Don't rely purely on libxml_disable_entity_loader()
$pattern = '/\\0?' . implode('\\0?', str_split($this->pattern)) . '\\0?/'; $pattern = '/\\0?' . implode('\\0?', str_split($this->pattern)) . '\\0?/';
if (preg_match($pattern, $xml)) {
throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
}
if ($this->callback !== null && is_callable($this->callback)) { try {
$xml = call_user_func($this->callback, $xml); if (preg_match($pattern, $xml)) {
throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
}
if ($this->callback !== null && is_callable($this->callback)) {
$xml = call_user_func($this->callback, $xml);
}
} finally {
if ($this->libxmlDisableEntityLoader) {
libxml_disable_entity_loader($previousLibxmlDisableEntityLoaderValue);
}
} }
return $xml; return $xml;

View File

@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Reader\Security;
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
use PhpOffice\PhpSpreadsheet\Reader\Xls; use PhpOffice\PhpSpreadsheet\Reader\Xls;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Reader\Xml;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
class XmlScannerTest extends TestCase class XmlScannerTest extends TestCase
@ -14,19 +15,26 @@ class XmlScannerTest extends TestCase
* *
* @param mixed $filename * @param mixed $filename
* @param mixed $expectedResult * @param mixed $expectedResult
* @param $libxmlDisableEntityLoader
*/ */
public function testValidXML($filename, $expectedResult) public function testValidXML($filename, $expectedResult, $libxmlDisableEntityLoader)
{ {
libxml_disable_entity_loader($libxmlDisableEntityLoader);
$reader = XmlScanner::getInstance(new \PhpOffice\PhpSpreadsheet\Reader\Xml()); $reader = XmlScanner::getInstance(new \PhpOffice\PhpSpreadsheet\Reader\Xml());
$result = $reader->scanFile($filename); $result = $reader->scanFile($filename);
self::assertEquals($expectedResult, $result); self::assertEquals($expectedResult, $result);
self::assertEquals($libxmlDisableEntityLoader, libxml_disable_entity_loader());
} }
public function providerValidXML() public function providerValidXML()
{ {
$tests = []; $tests = [];
foreach (glob(__DIR__ . '/../../../data/Reader/Xml/XEETestValid*.xml') as $file) { foreach (glob(__DIR__ . '/../../../data/Reader/Xml/XEETestValid*.xml') as $file) {
$tests[basename($file)] = [realpath($file), file_get_contents($file)]; $filename = realpath($file);
$expectedResult = file_get_contents($file);
$tests[basename($file) . '_libxml_entity_loader_disabled'] = [$filename, $expectedResult, true];
$tests[basename($file) . '_libxml_entity_loader_enabled'] = [$filename, $expectedResult, false];
} }
return $tests; return $tests;
@ -36,22 +44,28 @@ class XmlScannerTest extends TestCase
* @dataProvider providerInvalidXML * @dataProvider providerInvalidXML
* *
* @param mixed $filename * @param mixed $filename
* @param $libxmlDisableEntityLoader
*/ */
public function testInvalidXML($filename) public function testInvalidXML($filename, $libxmlDisableEntityLoader)
{ {
$this->expectException(\PhpOffice\PhpSpreadsheet\Reader\Exception::class); $this->expectException(\PhpOffice\PhpSpreadsheet\Reader\Exception::class);
libxml_disable_entity_loader($libxmlDisableEntityLoader);
$reader = XmlScanner::getInstance(new \PhpOffice\PhpSpreadsheet\Reader\Xml()); $reader = XmlScanner::getInstance(new \PhpOffice\PhpSpreadsheet\Reader\Xml());
$expectedResult = 'FAILURE: Should throw an Exception rather than return a value'; $expectedResult = 'FAILURE: Should throw an Exception rather than return a value';
$result = $reader->scanFile($filename); $result = $reader->scanFile($filename);
self::assertEquals($expectedResult, $result); self::assertEquals($expectedResult, $result);
self::assertEquals($libxmlDisableEntityLoader, libxml_disable_entity_loader());
} }
public function providerInvalidXML() public function providerInvalidXML()
{ {
$tests = []; $tests = [];
foreach (glob(__DIR__ . '/../../../data/Reader/Xml/XEETestInvalidUTF*.xml') as $file) { foreach (glob(__DIR__ . '/../../../data/Reader/Xml/XEETestInvalidUTF*.xml') as $file) {
$tests[basename($file)] = [realpath($file)]; $filename = realpath($file);
$tests[basename($file) . '_libxml_entity_loader_disabled'] = [$filename, true];
$tests[basename($file) . '_libxml_entity_loader_enabled'] = [$filename, false];
} }
return $tests; return $tests;
@ -101,4 +115,26 @@ class XmlScannerTest extends TestCase
return $tests; return $tests;
} }
/**
* @dataProvider providerLibxmlSettings
*
* @param $libxmDisableLoader
*/
public function testNewInstanceCreationDoesntChangeLibxmlSettings($libxmDisableLoader)
{
libxml_disable_entity_loader($libxmDisableLoader);
$reader = new Xml();
self::assertEquals($libxmDisableLoader, libxml_disable_entity_loader($libxmDisableLoader));
}
public function providerLibxmlSettings()
{
return [
[true],
[false],
];
}
} }