Merge pull request #10376 from creative-commoners/pulls/4.10/cve-2021-41559

CVE-2021-41559 Disable xml entities
This commit is contained in:
Guy Sartorelli 2022-06-28 17:27:08 +12:00 committed by GitHub
commit 410c2a8966
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 92 additions and 69 deletions

View File

@ -2,12 +2,11 @@
namespace SilverStripe\Core; namespace SilverStripe\Core;
use InvalidArgumentException;
use SimpleXMLElement;
use SilverStripe\Dev\Deprecation; use SilverStripe\Dev\Deprecation;
use SilverStripe\ORM\DB; use SilverStripe\ORM\DB;
use SilverStripe\View\Parsers\URLSegmentFilter; use SilverStripe\View\Parsers\URLSegmentFilter;
use InvalidArgumentException;
use SimpleXMLElement;
use Exception;
/** /**
* Library of conversion functions, implemented as static methods. * Library of conversion functions, implemented as static methods.
@ -29,7 +28,6 @@ use Exception;
*/ */
class Convert class Convert
{ {
/** /**
* Convert a value to be suitable for an XML attribute. * Convert a value to be suitable for an XML attribute.
* *
@ -296,40 +294,34 @@ class Convert
* @param string $val * @param string $val
* @param boolean $disableDoctypes Disables the use of DOCTYPE, and will trigger an error if encountered. * @param boolean $disableDoctypes Disables the use of DOCTYPE, and will trigger an error if encountered.
* false by default. * false by default.
* @param boolean $disableExternals Disables the loading of external entities. false by default. No-op in PHP 8. * @param boolean $disableExternals Does nothing because xml entities are removed
* @deprecated 4.11.0:5.0.0
* @return array * @return array
* @throws Exception * @throws Exception
*/ */
public static function xml2array($val, $disableDoctypes = false, $disableExternals = false) public static function xml2array($val, $disableDoctypes = false, $disableExternals = false)
{ {
// PHP 8 deprecates libxml_disable_entity_loader() as it is no longer needed Deprecation::notice('4.10', 'Use a dedicated XML library instead');
if (\PHP_VERSION_ID >= 80000) {
$disableExternals = false;
}
// Check doctype // Check doctype
if ($disableDoctypes && preg_match('/\<\!DOCTYPE.+]\>/', $val)) { if ($disableDoctypes && strpos($val ?? '', '<!DOCTYPE') !== false) {
throw new InvalidArgumentException('XML Doctype parsing disabled'); throw new InvalidArgumentException('XML Doctype parsing disabled');
} }
// Disable external entity loading // CVE-2021-41559 Ensure entities are removed due to their inherent security risk via
$oldVal = null; // XXE attacks and quadratic blowup attacks, and also lack of consistent support
if ($disableExternals) { $val = preg_replace('/(?s)<!ENTITY.*?>/', '', $val ?? '');
$oldVal = libxml_disable_entity_loader($disableExternals);
// If there's still an <!ENTITY> present, then it would be the result of a maliciously
// crafted XML document e.g. <!ENTITY><!<!ENTITY>ENTITY ext SYSTEM "http://evil.com">
if (strpos($val ?? '', '<!ENTITY') !== false) {
throw new InvalidArgumentException('Malicious XML entity detected');
} }
try {
$xml = new SimpleXMLElement($val); // This will throw an exception if the XML contains references to any internal entities
$result = self::recursiveXMLToArray($xml); // that were defined in an <!ENTITY /> before it was removed
} catch (Exception $ex) { $xml = new SimpleXMLElement($val ?? '');
if ($disableExternals) { return self::recursiveXMLToArray($xml);
libxml_disable_entity_loader($oldVal);
}
throw $ex;
}
if ($disableExternals) {
libxml_disable_entity_loader($oldVal);
}
return $result;
} }
/** /**

View File

@ -403,55 +403,86 @@ PHP
*/ */
public function testXML2Array() public function testXML2Array()
{ {
// Ensure an XML file at risk of entity expansion can be avoided safely
$inputXML = <<<XML $inputXML = <<<XML
<?xml version="1.0"?> <?xml version="1.0"?>
<!DOCTYPE results [<!ENTITY long "SOME_SUPER_LONG_STRING">]> <!DOCTYPE results [
<!ENTITY long "SOME_SUPER_LONG_STRING">
]>
<results> <results>
<result>Now include &long; lots of times to expand the in-memory size of this XML structure</result> <result>My para</result>
<result>&long;&long;&long;</result> <result>Ampersand &amp; is retained and not double encoded</result>
</results> </results>
XML XML
; ;
try {
Convert::xml2array($inputXML, true);
} catch (Exception $ex) {
}
$this->assertTrue(
isset($ex)
&& $ex instanceof InvalidArgumentException
&& $ex->getMessage() === 'XML Doctype parsing disabled'
);
// Test without doctype validation
$expected = [ $expected = [
'result' => [ 'result' => [
'Now include SOME_SUPER_LONG_STRING lots of times to expand the in-memory size of this XML structure', 'My para',
[ 'Ampersand & is retained and not double encoded'
'long' => [
[
'long' => 'SOME_SUPER_LONG_STRING'
],
[
'long' => 'SOME_SUPER_LONG_STRING'
],
[
'long' => 'SOME_SUPER_LONG_STRING'
]
]
]
] ]
]; ];
$result = Convert::xml2array($inputXML, false, true); $actual = Convert::xml2array($inputXML, false);
$this->assertEquals( $this->assertEquals($expected, $actual);
$expected, $this->expectException(InvalidArgumentException::class);
$result $this->expectExceptionMessage('XML Doctype parsing disabled');
); Convert::xml2array($inputXML, true);
$result = Convert::xml2array($inputXML, false, false); }
$this->assertEquals(
$expected, /**
$result * Tests {@link Convert::xml2array()} if an exception the contains a reference to a removed <!ENTITY />
); */
public function testXML2ArrayEntityException()
{
$inputXML = <<<XML
<?xml version="1.0"?>
<!DOCTYPE results [
<!ENTITY long "SOME_SUPER_LONG_STRING">
]>
<results>
<result>Now include &long; lots of times to expand the in-memory size of this XML structure</result>
<result>&long;&long;&long;</result>
</results>
XML;
$this->expectException(Exception::class);
$this->expectExceptionMessage('String could not be parsed as XML');
Convert::xml2array($inputXML);
}
/**
* Tests {@link Convert::xml2array()} if an exception the contains a reference to a multiple removed <!ENTITY />
*/
public function testXML2ArrayMultipleEntitiesException()
{
$inputXML = <<<XML
<?xml version="1.0"?>
<!DOCTYPE results [<!ENTITY long "SOME_SUPER_LONG_STRING"><!ENTITY short "SHORT_STRING">]>
<results>
<result>Now include &long; and &short; lots of times</result>
<result>&long;&long;&long;&short;&short;&short;</result>
</results>
XML;
$this->expectException(Exception::class);
$this->expectExceptionMessage('String could not be parsed as XML');
Convert::xml2array($inputXML);
}
/**
* Tests {@link Convert::xml2array()} if there is a malicious <!ENTITY /> present
*/
public function testXML2ArrayMaliciousEntityException()
{
$inputXML = <<<XML
<?xml version="1.0"?>
<!DOCTYPE results [
<!ENTITY><!<!ENTITY>ENTITY ext SYSTEM "http://evil.com">
]>
<results>
<result>Evil document</result>
</results>
XML;
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Malicious XML entity detected');
Convert::xml2array($inputXML);
} }
/** /**