node
* @return array Array of DOMElement instances
*/
abstract protected function getNodes(DOMElement $template);
/**
* Return whether an attribute is considered safe
*
* @param Attribute $attribute Attribute
* @return bool
*/
abstract protected function isSafe(Attribute $attribute);
/**
* Look for improperly-filtered dynamic content
*
* @param DOMElement $template node
* @param Tag $tag Tag this template belongs to
* @return void
*/
public function check(DOMElement $template, Tag $tag)
{
foreach ($this->getNodes($template) as $node)
{
// Test this node's safety
$this->checkNode($node, $tag);
}
}
/**
* Configure this template check to detect unknown attributes
*
* @return void
*/
public function detectUnknownAttributes()
{
$this->ignoreUnknownAttributes = false;
}
/**
* Configure this template check to ignore unknown attributes
*
* @return void
*/
public function ignoreUnknownAttributes()
{
$this->ignoreUnknownAttributes = true;
}
/**
* Test whether a tag attribute is safe
*
* @param DOMNode $node Context node
* @param Tag $tag Source tag
* @param string $attrName Name of the attribute
* @return void
*/
protected function checkAttribute(DOMNode $node, Tag $tag, $attrName)
{
// Test whether the attribute exists
if (!isset($tag->attributes[$attrName]))
{
if ($this->ignoreUnknownAttributes)
{
return;
}
throw new UnsafeTemplateException("Cannot assess the safety of unknown attribute '" . $attrName . "'", $node);
}
// Test whether the attribute is safe to be used in this content type
if (!$this->tagFiltersAttributes($tag) || !$this->isSafe($tag->attributes[$attrName]))
{
throw new UnsafeTemplateException("Attribute '" . $attrName . "' is not properly sanitized to be used in this context", $node);
}
}
/**
* Test whether an attribute expression is safe
*
* @param DOMNode $node Context node
* @param Tag $tag Source tag
* @param string $expr XPath expression that evaluates to one or multiple named attributes
* @return void
*/
protected function checkAttributeExpression(DOMNode $node, Tag $tag, $expr)
{
preg_match_all('(@([-\\w]+))', $expr, $matches);
foreach ($matches[1] as $attrName)
{
$this->checkAttribute($node, $tag, $attrName);
}
}
/**
* Test whether an attribute node is safe
*
* @param DOMAttr $attribute Attribute node
* @param Tag $tag Reference tag
* @return void
*/
protected function checkAttributeNode(DOMAttr $attribute, Tag $tag)
{
// Parse the attribute value for XPath expressions and assess their safety
foreach (AVTHelper::parse($attribute->value) as $token)
{
if ($token[0] === 'expression')
{
$this->checkExpression($attribute, $token[1], $tag);
}
}
}
/**
* Test whether a node's context can be safely assessed
*
* @param DOMNode $node Source node
* @return void
*/
protected function checkContext(DOMNode $node)
{
// Test whether we know in what context this node is used. An ancestor would // change this node's context
$xpath = new DOMXPath($node->ownerDocument);
$ancestors = $xpath->query('ancestor::xsl:for-each', $node);
if ($ancestors->length)
{
throw new UnsafeTemplateException("Cannot assess context due to '" . $ancestors->item(0)->nodeName . "'", $node);
}
}
/**
* Test whether an node is safe
*
* @param DOMElement $node node
* @param Tag $tag Reference tag
* @return void
*/
protected function checkCopyOfNode(DOMElement $node, Tag $tag)
{
$this->checkSelectNode($node->getAttributeNode('select'), $tag);
}
/**
* Test whether an element node is safe
*
* @param DOMElement $element Element
* @param Tag $tag Reference tag
* @return void
*/
protected function checkElementNode(DOMElement $element, Tag $tag)
{
$xpath = new DOMXPath($element->ownerDocument);
// If current node is not an element, we exclude descendants
// with an ancestor so that content such as:
//
// would not trigger a false-positive due to the presence of an
// element in a