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