Skip to content

Commit

Permalink
Implemented XXE safe xml parser
Browse files Browse the repository at this point in the history
  • Loading branch information
PeeHaa committed Nov 30, 2018
1 parent 02ec1ba commit 014a258
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 14 deletions.
7 changes: 7 additions & 0 deletions src/Exception/InvalidXml.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php declare(strict_types=1);

namespace HarmonyIO\Validation\Exception;

class InvalidXml extends Exception
{
}
15 changes: 8 additions & 7 deletions src/Rule/File/Image/Type/Svg/ValidAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
use Amp\Promise;
use Amp\Success;
use HarmonyIO\Validation\Enum\File\Image\Svg\Attribute;
use HarmonyIO\Validation\Exception\InvalidXml;
use HarmonyIO\Validation\Rule\File\MimeType;
use HarmonyIO\Validation\Rule\FileSystem\File;
use HarmonyIO\Validation\Rule\Rule;
use HarmonyIO\Validation\Xml\SafeParser;
use function Amp\call;
use function Amp\ParallelFunctions\parallel;

Expand Down Expand Up @@ -41,21 +43,20 @@ public function validate($value): Promise

return parallel(function () use ($value) {
// @codeCoverageIgnoreStart
$useInternalErrors = libxml_use_internal_errors(true);

$domDocument = new \DOMDocument();
$domDocument->load($value);
try {
$xmlParser = new SafeParser(file_get_contents($value));
} catch (InvalidXml $e) {
return false;
}

foreach ($domDocument->getElementsByTagName('*') as $node) {
foreach ($xmlParser->getElementsByTagName('*') as $node) {
foreach ($node->attributes as $attribute) {
if (!$this->attribute->exists($attribute->nodeName)) {
return false;
}
}
}

libxml_use_internal_errors($useInternalErrors);

return true;
// @codeCoverageIgnoreEnd
})();
Expand Down
15 changes: 8 additions & 7 deletions src/Rule/File/Image/Type/Svg/ValidElements.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
use Amp\Promise;
use Amp\Success;
use HarmonyIO\Validation\Enum\File\Image\Svg\Element;
use HarmonyIO\Validation\Exception\InvalidXml;
use HarmonyIO\Validation\Rule\File\MimeType;
use HarmonyIO\Validation\Rule\FileSystem\File;
use HarmonyIO\Validation\Rule\Rule;
use HarmonyIO\Validation\Xml\SafeParser;
use function Amp\call;
use function Amp\ParallelFunctions\parallel;

Expand Down Expand Up @@ -41,20 +43,19 @@ public function validate($value): Promise

return parallel(function () use ($value) {
// @codeCoverageIgnoreStart
$useInternalErrors = libxml_use_internal_errors(true);

$domDocument = new \DOMDocument();
$domDocument->load($value);
try {
$xmlParser = new SafeParser(file_get_contents($value));
} catch (InvalidXml $e) {
return false;
}

/** @var \DOMElement $node */
foreach ($domDocument->getElementsByTagName('*') as $node) {
foreach ($xmlParser->getElementsByTagName('*') as $node) {
if (!$this->element->exists($node->nodeName)) {
return false;
}
}

libxml_use_internal_errors($useInternalErrors);

return true;
// @codeCoverageIgnoreEnd
})();
Expand Down
38 changes: 38 additions & 0 deletions src/Xml/SafeParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php declare(strict_types=1);

namespace HarmonyIO\Validation\Xml;

use HarmonyIO\Validation\Exception\InvalidXml;

class SafeParser
{
/** @var \DOMDocument */
private $domDocument;

public function __construct(string $xml)
{
$useInternalErrors = libxml_use_internal_errors(true);
$disableEntityLoader = libxml_disable_entity_loader(true);

$domDocument = new \DOMDocument();
$domDocument->loadXML($xml);

$libXmlErrors = libxml_get_errors();

libxml_clear_errors();

libxml_use_internal_errors($useInternalErrors);
libxml_disable_entity_loader($disableEntityLoader);

if ($libXmlErrors) {
throw new InvalidXml($libXmlErrors[0]->message, $libXmlErrors[0]->code);
}

$this->domDocument = $domDocument;
}

public function getElementsByTagName(string $tagName): \DOMNodeList
{
return $this->domDocument->getElementsByTagName($tagName);
}
}
4 changes: 4 additions & 0 deletions tests/Data/image/broken.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions tests/Unit/Rule/File/Image/Type/Svg/ValidAttributesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ public function testValidateReturnsFalseWhenNotMatchingMimeType(): void
$this->assertFalse((new ValidAttributes())->validate(TEST_DATA_DIR . '/image/mspaint.gif'));
}

public function testValidateReturnsFalseWhenImageContainsBrokenXml(): void
{
$this->assertFalse((new ValidAttributes())->validate(TEST_DATA_DIR . '/image/broken.svg'));
}

public function testValidateReturnsFalseWhenImageContainsInvalidElements(): void
{
$this->assertFalse((new ValidAttributes())->validate(TEST_DATA_DIR . '/image/invalid-attributes.svg'));
Expand Down
5 changes: 5 additions & 0 deletions tests/Unit/Rule/File/Image/Type/Svg/ValidElementsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ public function testValidateReturnsFalseWhenNotMatchingMimeType(): void
$this->assertFalse((new ValidElements())->validate(TEST_DATA_DIR . '/image/mspaint.gif'));
}

public function testValidateReturnsFalseWhenImageContainsBrokenXml(): void
{
$this->assertFalse((new ValidElements())->validate(TEST_DATA_DIR . '/image/broken.svg'));
}

public function testValidateReturnsFalseWhenImageContainsInvalidElements(): void
{
$this->assertFalse((new ValidElements())->validate(TEST_DATA_DIR . '/image/invalid-elements.svg'));
Expand Down
5 changes: 5 additions & 0 deletions tests/Unit/Rule/File/Image/Type/SvgTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ public function testValidateReturnsFalseWhenNotMatchingMimeType(): void
$this->assertFalse((new Svg())->validate(TEST_DATA_DIR . '/image/mspaint.gif'));
}

public function testValidateReturnsFalseWhenImageContainsBrokenXml(): void
{
$this->assertFalse((new Svg())->validate(TEST_DATA_DIR . '/image/broken.svg'));
}

public function testValidateReturnsFalseWhenImageContainsInvalidElements(): void
{
$this->assertFalse((new Svg())->validate(TEST_DATA_DIR . '/image/invalid-elements.svg'));
Expand Down
31 changes: 31 additions & 0 deletions tests/Unit/Xml/SafeParserTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php declare(strict_types=1);

namespace HarmonyIO\ValidationTest\Unit\Xml;

use HarmonyIO\PHPUnitExtension\TestCase;
use HarmonyIO\Validation\Exception\InvalidXml;
use HarmonyIO\Validation\Xml\SafeParser;

class SafeParserTest extends TestCase
{
public function testConstructorThrowsOnBrokenXml(): void
{
$this->expectException(InvalidXml::class);

new SafeParser(file_get_contents(TEST_DATA_DIR . '/image/broken.svg'));
}

public function testGetElementsByTagNameReturnsNodeList(): void
{
$xmlParser = new SafeParser(file_get_contents(TEST_DATA_DIR . '/image/example.svg'));

$this->assertInstanceOf(\DOMNodeList::class, $xmlParser->getElementsByTagName('*'));
}

public function testGetElementsByTagNameReturnsNodes(): void
{
$xmlParser = new SafeParser(file_get_contents(TEST_DATA_DIR . '/image/example.svg'));

$this->assertSame(3, $xmlParser->getElementsByTagName('*')->count());
}
}

0 comments on commit 014a258

Please sign in to comment.