Я играл с PHP DOMDocument и связанными с ним классами с целью написания HTML-парсера, который может справиться с подобными вещами.В данный момент он находится на очень ранней стадии разработки и еще далеко не готов к реальному использованию, но мои ранние эксперименты, похоже, показывают некоторую перспективу этой идеи.
По сути, вы загружаете вашу разметку в DOMDocument, а затем пересекаете дерево.Для каждого узла в дереве вы проверяете тип узла по списку разрешенных типов узлов.Если тип узла отсутствует в списке, он удаляется из дерева.
Вы можете использовать подход, подобный этому, чтобы найти все теги SCRIPT в фрагменте разметки и удалить их.XSS на основе DOM становится беззубым, если вы можете извлечь любые встроенные сценарии из предоставленной вами разметки.
Это код, который я использую вместе с тестовым примером, который обрабатывает домашнюю страницу StackOverflow.Как я уже сказал, он далек от производственного кода качества и является не более чем доказательством концепции.Тем не менее, я надеюсь, что вы найдете это полезным.
<code><?php
class HtmlClean
{
private $whiteList = array (
'#cdata-section', '#comment', '#text', 'a', 'abbr', 'acronym', 'address', 'b',
'big', 'blockquote', 'body', 'br', 'caption', 'cite', 'code', 'col', 'colgroup',
'dd', 'del', 'dfn', 'div', 'dl', 'dt', 'em', 'fieldset', 'h1', 'h2', 'h3', 'h4',
'h5', 'h6', 'head', 'hr', 'html', 'i', 'img', 'ins', 'kbd', 'li', 'link', 'meta',
'ol', 'p', 'pre', 'q', 'samp', 'small', 'span', 'strike', 'strong', 'style', 'sub',
'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'title', 'tr', 'tt', 'ul',
'var'
);
private $attrWhiteList = array (
'class', 'id', 'title'
);
private $dom = NULL;
/**
* Get current tag whitelist
* @return array
*/
public function getWhiteListTags ()
{
$this -> whiteList = array_values ($this -> whiteList);
return ($this -> whiteList);
}
/**
* Add tag to the whitelist
* @param string $tagName
*/
public function addWhiteListTag ($tagName)
{
$tagName = strtolower (trin ($tagName));
if (!in_array ($tagName, $this -> whiteList))
{
$this -> whiteList [] = $tagName;
}
}
/**
* Remove a tag from the whitelist
* @param string $tagName
*/
public function removeWhiteListTag ($tagName)
{
if ($index = array_search ($tagName, $this -> whiteList))
{
unset ($this -> whiteList [$index]);
}
}
/**
* Load document markup into the class for cleaning
* @param string $html The markup to clean
* @return bool
*/
public function loadHTML ($html)
{
if (!$this -> dom)
{
$this -> dom = new DOMDocument();
}
$this -> dom -> preserveWhiteSpace = false;
$this -> dom -> formatOutput = true;
return $this -> dom -> loadHTML ($html);
}
public function outputHtml ()
{
$ret = '';
if ($this -> dom)
{
$ret = $this -> dom -> saveXML ();
}
return ($ret);
}
private function cleanAttrs (DOMnode $elem)
{
$attrs = $elem -> attributes;
$index = $attrs -> length;
while (--$index >= 0)
{
$attrName = strtolower ($attrs -> item ($indes) -> name);
if (!in_array ($attrName, $this -> attrWhiteList))
{
$elem -> removeAttribute ($attrName);
}
}
}
/**
* Recursivly remove elements from the DOM that aren't whitelisted
* @param DOMNode $elem
* @return array List of elements removed from the DOM
* @throws Exception If removal of a node failed than an exception is thrown
*/
private function cleanNodes (DOMNode $elem)
{
$removed = array ();
if (in_array (strtolower ($elem -> nodeName), $this -> whiteList))
{
// Remove non-whitelisted attributes
if ($elem -> hasAttributes ())
{
$this -> cleanAttrs ($elem);
}
/*
* Iterate over the element's children. The reason we go backwards is because
* going forwards will cause indexes to change when elements get removed
*/
if ($elem -> hasChildNodes ())
{
$children = $elem -> childNodes;
$index = $children -> length;
while (--$index >= 0)
{
$removed = array_merge ($removed, $this -> cleanNodes ($children -> item ($index)));
}
}
}
else
{
// The element is not on the whitelist, so remove it
if ($elem -> parentNode -> removeChild ($elem))
{
$removed [] = $elem;
}
else
{
throw new Exception ('Failed to remove node from DOM');
}
}
return ($removed);
}
/**
* Perform the cleaning of the document
*/
public function clean ()
{
$removed = $this -> cleanNodes ($this -> dom -> getElementsByTagName ('html') -> item (0));
return ($removed);
}
}
$test = file_get_contents( ('http://www.stackoverflow.com/'));
// Windows-stype linebreaks really foul up the works. There's probably a better fix for this
$test = str_replace (chr (13), '', $test);
$cleaner = new HtmlClean ();
$cleaner -> loadHTML ($test);
echo ('<h1>Before</h1><pre>' . htmlspecialchars ($cleaner -> outputHtml ()) . '
');$ start = microtime (true);$ remove = $ cleaner -> clean ();$ cleanTime = microtime (true) - $ start;echo ('
Список удаленных тегов
');foreach ($ удалено как $ elem) {var_dump ($ elem -> nodeName);} echo ('
После
' . htmlspecialchars ($cleaner -> outputHtml ()) . '
');// тест производительности var_dump ($ cleanTime);?>