Skip to content

Commit 8918888

Browse files
Philipp KolesnikovPowerKiKi
Philipp Kolesnikov
authored andcommitted
libxml_disable_entity_loader() changes global state so it should be used as local as possible
Fixes #801 Closes #802 Closes #803
1 parent 6a48b50 commit 8918888

File tree

3 files changed

+57
-27
lines changed

3 files changed

+57
-27
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
1919
- Fix LOOKUP function which was breaking on edge cases - [#796](https://github.com/PHPOffice/PhpSpreadsheet/issues/796)
2020
- Fix VLOOKUP with exact matches - [#809](https://github.com/PHPOffice/PhpSpreadsheet/pull/809)
2121
- Support COUNTIFS multiple arguments - [#830](https://github.com/PHPOffice/PhpSpreadsheet/pull/830)
22+
- Change `libxml_disable_entity_loader()` as shortly as possible - [#819](https://github.com/PHPOffice/PhpSpreadsheet/pull/819)
2223

2324
## [1.5.2] - 2018-11-25
2425

src/PhpSpreadsheet/Reader/Security/XmlScanner.php

+16-23
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,6 @@ class XmlScanner
1313
*/
1414
private $libxmlDisableEntityLoader = false;
1515

16-
/**
17-
* Store the initial setting of libxmlDisableEntityLoader so that we can resore t later.
18-
*
19-
* @var bool
20-
*/
21-
private $previousLibxmlDisableEntityLoaderValue;
22-
2316
/**
2417
* String used to identify risky xml elements.
2518
*
@@ -33,17 +26,6 @@ private function __construct($pattern = '<!DOCTYPE')
3326
{
3427
$this->pattern = $pattern;
3528
$this->libxmlDisableEntityLoader = $this->identifyLibxmlDisableEntityLoaderAvailability();
36-
37-
if ($this->libxmlDisableEntityLoader) {
38-
$this->previousLibxmlDisableEntityLoaderValue = libxml_disable_entity_loader(true);
39-
}
40-
}
41-
42-
public function __destruct()
43-
{
44-
if ($this->libxmlDisableEntityLoader) {
45-
libxml_disable_entity_loader($this->previousLibxmlDisableEntityLoaderValue);
46-
}
4729
}
4830

4931
public static function getInstance(Reader\IReader $reader)
@@ -95,6 +77,10 @@ public function setAdditionalCallback(callable $callback)
9577
*/
9678
public function scan($xml)
9779
{
80+
if ($this->libxmlDisableEntityLoader) {
81+
$previousLibxmlDisableEntityLoaderValue = libxml_disable_entity_loader(true);
82+
}
83+
9884
$pattern = '/encoding="(.*?)"/';
9985
$result = preg_match($pattern, $xml, $matches);
10086
$charset = $result ? $matches[1] : 'UTF-8';
@@ -105,12 +91,19 @@ public function scan($xml)
10591

10692
// Don't rely purely on libxml_disable_entity_loader()
10793
$pattern = '/\\0?' . implode('\\0?', str_split($this->pattern)) . '\\0?/';
108-
if (preg_match($pattern, $xml)) {
109-
throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
110-
}
11194

112-
if ($this->callback !== null && is_callable($this->callback)) {
113-
$xml = call_user_func($this->callback, $xml);
95+
try {
96+
if (preg_match($pattern, $xml)) {
97+
throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
98+
}
99+
100+
if ($this->callback !== null && is_callable($this->callback)) {
101+
$xml = call_user_func($this->callback, $xml);
102+
}
103+
} finally {
104+
if ($this->libxmlDisableEntityLoader) {
105+
libxml_disable_entity_loader($previousLibxmlDisableEntityLoaderValue);
106+
}
114107
}
115108

116109
return $xml;

tests/PhpSpreadsheetTests/Reader/Security/XmlScannerTest.php

+40-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
66
use PhpOffice\PhpSpreadsheet\Reader\Xls;
77
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
8+
use PhpOffice\PhpSpreadsheet\Reader\Xml;
89
use PHPUnit\Framework\TestCase;
910

1011
class XmlScannerTest extends TestCase
@@ -14,19 +15,26 @@ class XmlScannerTest extends TestCase
1415
*
1516
* @param mixed $filename
1617
* @param mixed $expectedResult
18+
* @param $libxmlDisableEntityLoader
1719
*/
18-
public function testValidXML($filename, $expectedResult)
20+
public function testValidXML($filename, $expectedResult, $libxmlDisableEntityLoader)
1921
{
22+
libxml_disable_entity_loader($libxmlDisableEntityLoader);
23+
2024
$reader = XmlScanner::getInstance(new \PhpOffice\PhpSpreadsheet\Reader\Xml());
2125
$result = $reader->scanFile($filename);
2226
self::assertEquals($expectedResult, $result);
27+
self::assertEquals($libxmlDisableEntityLoader, libxml_disable_entity_loader());
2328
}
2429

2530
public function providerValidXML()
2631
{
2732
$tests = [];
2833
foreach (glob(__DIR__ . '/../../../data/Reader/Xml/XEETestValid*.xml') as $file) {
29-
$tests[basename($file)] = [realpath($file), file_get_contents($file)];
34+
$filename = realpath($file);
35+
$expectedResult = file_get_contents($file);
36+
$tests[basename($file) . '_libxml_entity_loader_disabled'] = [$filename, $expectedResult, true];
37+
$tests[basename($file) . '_libxml_entity_loader_enabled'] = [$filename, $expectedResult, false];
3038
}
3139

3240
return $tests;
@@ -36,22 +44,28 @@ public function providerValidXML()
3644
* @dataProvider providerInvalidXML
3745
*
3846
* @param mixed $filename
47+
* @param $libxmlDisableEntityLoader
3948
*/
40-
public function testInvalidXML($filename)
49+
public function testInvalidXML($filename, $libxmlDisableEntityLoader)
4150
{
4251
$this->expectException(\PhpOffice\PhpSpreadsheet\Reader\Exception::class);
4352

53+
libxml_disable_entity_loader($libxmlDisableEntityLoader);
54+
4455
$reader = XmlScanner::getInstance(new \PhpOffice\PhpSpreadsheet\Reader\Xml());
4556
$expectedResult = 'FAILURE: Should throw an Exception rather than return a value';
4657
$result = $reader->scanFile($filename);
4758
self::assertEquals($expectedResult, $result);
59+
self::assertEquals($libxmlDisableEntityLoader, libxml_disable_entity_loader());
4860
}
4961

5062
public function providerInvalidXML()
5163
{
5264
$tests = [];
5365
foreach (glob(__DIR__ . '/../../../data/Reader/Xml/XEETestInvalidUTF*.xml') as $file) {
54-
$tests[basename($file)] = [realpath($file)];
66+
$filename = realpath($file);
67+
$tests[basename($file) . '_libxml_entity_loader_disabled'] = [$filename, true];
68+
$tests[basename($file) . '_libxml_entity_loader_enabled'] = [$filename, false];
5569
}
5670

5771
return $tests;
@@ -101,4 +115,26 @@ public function providerValidXMLForCallback()
101115

102116
return $tests;
103117
}
118+
119+
/**
120+
* @dataProvider providerLibxmlSettings
121+
*
122+
* @param $libxmDisableLoader
123+
*/
124+
public function testNewInstanceCreationDoesntChangeLibxmlSettings($libxmDisableLoader)
125+
{
126+
libxml_disable_entity_loader($libxmDisableLoader);
127+
128+
$reader = new Xml();
129+
130+
self::assertEquals($libxmDisableLoader, libxml_disable_entity_loader($libxmDisableLoader));
131+
}
132+
133+
public function providerLibxmlSettings()
134+
{
135+
return [
136+
[true],
137+
[false],
138+
];
139+
}
104140
}

0 commit comments

Comments
 (0)