如何使用PHP的各种XML库来获取类似DOM的功能并避免DoS漏洞,比如Billion Laughs或Quadratic Blowup?

时间:2012-04-18 15:30:02

标签: php security xml-parsing doctype denial-of-service

我正在编写一个在PHP中使用XML API的Web应用程序,我担心三个特定的漏洞,这些漏洞都与内联DOCTYPE定义有关:本地文件包含,二次实体爆炸和指数实体爆炸。我喜欢使用PHP(5.3)内置库,但我想确保我不会受这些影响。

我发现我可以使用libxml_disable_entity_loader消除LFI,但这对内联ENTITY声明没有帮助,包括引用其他实体的实体。

SimpleXML库(SimpleXMLElement,simplexml_load_string等)很棒,因为它是一个DOM解析器,我的所有输入都很小;它允许我使用xpath并非常容易地操作DOM。我无法想象如何停止ENTITY声明。 (如果可能的话,我很乐意禁用所有内联DOCTYPE定义。)

XML Parser库(xml_parser_create,xml_set_element_handler等)允许我使用xml_set_default_handler设置包含实体的默认处理程序。我可以破解它,所以对于无法识别的实体,它只返回原始字符串(即“& ent;”)。这个库令人沮丧:因为它是一个SAX解析器,我必须编写一堆处理程序(多达9 ..)。

那么是否可以使用内置库,获取类似DOM的对象,并保护自己免受这些各种DoS漏洞的影响?感谢

此页面描述了三个漏洞,并提供了一个解决方案......如果我只使用.NET:http://msdn.microsoft.com/en-us/magazine/ee335713.aspx

更新:

<?php
$s = <<<EOF
<?xml version="1.0?>
<!DOCTYPE data [
<!ENTITY en "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa....">
]>
<data>&en;&en;&en;&en;&en;&en;&en;&en;&en;&en;&en;&en;.....</data>
EOF;
$doc = new DOMDocument();
$doc->loadXML($s);
var_dump($d->lastChild->nodeValue);
?>

我也试过loadXML($s, LIBXML_NOENT);。在这两种情况下,我最终倾销300多MB。有什么东西我还在遗失吗?

2 个答案:

答案 0 :(得分:12)

  

注意:如果使用以下包含XML块的文件创建测试用例,则期望编辑器也可能容易受到这些攻击,并且可能会冻结/崩溃。

十亿笑

<?xml version="1.0"?>
<!DOCTYPE lolz [
  <!ENTITY lol "lol">
  <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
  <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
  <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
  <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
  <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
  <!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
  <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
  <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>

加载时:

  

致命:#89:检测到实体参考循环1:7
  ......(加上六次相同=总共七次以上)
  致命:#89:检测到实体参考循环14:13

结果:

<?xml version="1.0"?>

内存使用率很低,DOMDocument未触及峰值。由于此示例显示了7个致命错误,因此可以得出结论,实际上这样可以加载没有错误:

<?xml version="1.0"?>
<!DOCTYPE lolz [
  <!ENTITY lol "lol">
  <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
]>
<lolz>&lol2;</lolz>

由于实体替换没有生效且这项工作,让我们试试

二次爆破

这就是这个,缩短了你的观赏乐趣(我的变体大约是27 / 11kb):

<?xml version="1.0"?>
<!DOCTYPE kaboom [
  <!ENTITY a "aaaaaaaaaaaaaaaaaa...">
]>
<kaboom>&a;&a;&a;&a;&a;&a;&a;&a;&a;...</kaboom>

如果您使用$doc->loadXML($src, LIBXML_NOENT);这可以作为攻击工作,而我写这篇文章时,脚本仍在加载......所以这实际上需要一些时间来加载和消耗内存。你可以用自己的东西玩。没有LIBXML_NOENT它可以完美无缺地运作。

但有一点需要注意,例如,如果您获得了标签的nodeValue,即使您没有使用该加载标记,也会扩展实体。

此问题的解决方法是从文档中删除DocumentType节点。请注意以下代码:

$doc = new DOMDocument();
$doc->loadXML($s); // where $s is a Quadratic attack xml string above.
// now remove the doctype node
foreach ($doc->childNodes as $child) {
    if ($child->nodeType===XML_DOCUMENT_TYPE_NODE) {
        $doc->removeChild($child);
        break;
    }
}
// Now the following is true:
assert($doc->doctype===NULL);
assert($doc->lastChild->nodeValue==='...');
// Note that entities remain unexpanded in the output XML
// This is not so good since this makes the XML invalid.
// Better is a manual walk through all nodes looking for XML_ENTITY_NODE
assert($doc->saveXML()==="<?xml version="1.0"?>\n<kaboom>&a;&a;&a;&a;&a;&a;&a;&a;&a;...</kaboom>\n");
// however, canonicalization will produce warnings because it must resolve entities
assert($doc->C14N()===False);
// Warning will be like:
//    PHP Warning:  DOMNode::C14N(): Node XML_ENTITY_REF_NODE is invalid here 

因此,虽然此解决方法将阻止XML文档消耗DoS中的资源,但它可以轻松生成无效的XML。

一些数字(我缩小了文件大小,否则花费的时间太长)(code):

LIBXML_NOENT disabled                                          LIBXML_NOENT enabled

Mem: 356 184 (Peak: 435 464)                                   Mem: 356 280 (Peak: 435 464)                             
Loaded file quadratic-blowup-2.xml into string.                Loaded file quadratic-blowup-2.xml into string.          
Mem: 368 400 (Peak: 435 464)                                   Mem: 368 496 (Peak: 435 464)                             
DOMDocument loaded XML 11 881 bytes in 0.001368 secs.          DOMDocument loaded XML 11 881 bytes in 15.993627 secs.   
Mem: 369 088 (Peak: 435 464)                                   Mem: 369 184 (Peak: 435 464)                             
Removed load string.                                           Removed load string.                                     
Mem: 357 112 (Peak: 435 464)                                   Mem: 357 208 (Peak: 435 464)                             
Got XML (saveXML()), length: 11 880                            Got XML (saveXML()), length: 11 165 132                  
Got Text (nodeValue), length: 11 160 314; 11.060893 secs.      Got Text (nodeValue), length: 11 160 314; 0.025360 secs. 
Mem: 11 517 776 (Peak: 11 532 016)                             Mem: 11 517 872 (Peak: 22 685 360)                       

到目前为止,我还没有就保护策略做出决定,但现在知道将数十亿的笑声加载到PHPStorm will freeze it for example并且我不再测试后者,因为我不想在写这篇文章时将其冻结。

答案 1 :(得分:7)

您应该使用示例文档测试您的应用程序,看看它是否容易受到攻击。

php的xml库的底层库是libxml2。它的行为主要通过PHP控制optional constants,大多数库在加载xml时都会接受它作为参数。

您可以使用echo LIBXML_DOTTED_VERSION;

确定php的libxml2版本

在更高版本(2.6之后)中,libxml2包含旨在防止指数和二次攻击的实体替换限制。这些可以使用LIBXML_PARSEHUGE选项覆盖。

默认情况下,libxml2不会加载dtd,添加默认属性或执行实体替换。所以默认行为是忽略dtds。

你可以像这样打开部分内容:

  • LIBXML_DTDLOAD将加载dtds。
  • LIBXML_NONET将禁用dtds的网络加载。你应该总是这样,并使用libxml的dtd目录来加载dtds。
  • LIBXML_DTDVALID将在解析时执行dtd验证。
  • LIBXML_NOENT将执行实体替换。
  • LIBXML_DTDATTR将添加默认属性。

因此,使用默认设置PHP / libxml2 可能不容易受到任何这些问题的影响,但唯一可以确定的方法是测试。