Seditio Source
Root |
./othercms/phpBB3/vendor/s9e/text-formatter/src/Configurator/TemplateChecks/AbstractDynamicContentCheck.php
<?php

/**
* @package   s9e\TextFormatter
* @copyright Copyright (c) 2010-2021 The s9e authors
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Configurator\TemplateChecks;

use
DOMAttr;
use
DOMElement;
use
DOMNode;
use
DOMXPath;
use
s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException;
use
s9e\TextFormatter\Configurator\Helpers\AVTHelper;
use
s9e\TextFormatter\Configurator\Items\Attribute;
use
s9e\TextFormatter\Configurator\Items\Tag;
use
s9e\TextFormatter\Configurator\TemplateCheck;

abstract class
AbstractDynamicContentCheck extends TemplateCheck
{
   
/**
    * @var bool Whether to ignore unknown attributes
    */
   
protected $ignoreUnknownAttributes = false;

   
/**
    * Get the nodes targeted by this check
    *
    * @param  DOMElement $template <xsl:template/> 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 <xsl: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 <xsl:for-each/> 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 <xsl:copy-of/> node is safe
    *
    * @param  DOMElement $node <xsl:copy-of/> 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 <xsl:attribute/> element, we exclude descendants
        // with an <xsl:attribute/> ancestor so that content such as:
        //   <script><xsl:attribute name="id"><xsl:value-of/></xsl:attribute></script>
        // would not trigger a false-positive due to the presence of an <xsl:value-of/>
        // element in a <script>
       
$predicate = ($element->localName === 'attribute') ? '' : '[not(ancestor::xsl:attribute)]';

       
// Test the select expression of <xsl:value-of/> nodes
       
$query = './/xsl:value-of' . $predicate;
        foreach (
$xpath->query($query, $element) as $valueOf)
        {
           
$this->checkSelectNode($valueOf->getAttributeNode('select'), $tag);
        }

       
// Reject all <xsl:apply-templates/> nodes
       
$query = './/xsl:apply-templates' . $predicate;
        foreach (
$xpath->query($query, $element) as $applyTemplates)
        {
            throw new
UnsafeTemplateException('Cannot allow unfiltered data in this context', $applyTemplates);
        }
    }

   
/**
    * Test the safety of an XPath expression
    *
    * @param  DOMNode $node Source node
    * @param  string  $expr XPath expression
    * @param  Tag     $tag  Source tag
    * @return void
    */
   
protected function checkExpression(DOMNode $node, $expr, Tag $tag)
    {
       
$this->checkContext($node);

        if (
preg_match('/^\\$(\\w+)$/', $expr, $m))
        {
           
// Either this expression came from a variable that is considered safe, or it's a
            // stylesheet parameters, which are considered safe by default
           
$this->checkVariable($node, $tag, $m[1]);
        }
        elseif (
preg_match('/^@[-\\w]+(?:\\s*\\|\\s*@[-\\w]+)*$/', $expr))
        {
           
$this->checkAttributeExpression($node, $tag, $expr);
        }
        elseif (!
$this->isExpressionSafe($expr))
        {
            throw new
UnsafeTemplateException("Cannot assess the safety of expression '" . $expr . "'", $node);
        }
    }

   
/**
    * Test whether a node is safe
    *
    * @param  DOMNode $node Source node
    * @param  Tag     $tag  Reference tag
    * @return void
    */
   
protected function checkNode(DOMNode $node, Tag $tag)
    {
        if (
$node instanceof DOMAttr)
        {
           
$this->checkAttributeNode($node, $tag);
        }
        elseif (
$node instanceof DOMElement)
        {
            if (
$node->namespaceURI === self::XMLNS_XSL && $node->localName === 'copy-of')
            {
               
$this->checkCopyOfNode($node, $tag);
            }
            else
            {
               
$this->checkElementNode($node, $tag);
            }
        }
    }

   
/**
    * Check whether a variable is safe in context
    *
    * @param  DOMNode $node  Context node
    * @param  Tag     $tag   Source tag
    * @param  string  $qname Name of the variable
    * @return void
    */
   
protected function checkVariable(DOMNode $node, $tag, $qname)
    {
       
// Test whether this variable comes from a previous xsl:param or xsl:variable element
       
$this->checkVariableDeclaration($node, $tag, 'xsl:param[@name="' . $qname . '"]');
       
$this->checkVariableDeclaration($node, $tag, 'xsl:variable[@name="' . $qname . '"]');
    }

   
/**
    * Check whether a variable declaration is safe in context
    *
    * @param  DOMNode $node  Context node
    * @param  Tag     $tag   Source tag
    * @param  string  $query XPath query
    * @return void
    */
   
protected function checkVariableDeclaration(DOMNode $node, $tag, $query)
    {
       
$query = 'ancestor-or-self::*/preceding-sibling::' . $query . '[@select]';
       
$xpath = new DOMXPath($node->ownerDocument);
        foreach (
$xpath->query($query, $node) as $varNode)
        {
           
// Intercept the UnsafeTemplateException and change the node to the one we're
            // really checking before rethrowing it
           
try
            {
               
$this->checkExpression($varNode, $varNode->getAttribute('select'), $tag);
            }
            catch (
UnsafeTemplateException $e)
            {
               
$e->setNode($node);

                throw
$e;
            }
        }
    }

   
/**
    * Test whether a select attribute of a node is safe
    *
    * @param  DOMAttr $select Select attribute node
    * @param  Tag     $tag    Reference tag
    * @return void
    */
   
protected function checkSelectNode(DOMAttr $select, Tag $tag)
    {
       
$this->checkExpression($select, $select->value, $tag);
    }

   
/**
    * Test whether given expression is safe in context
    *
    * @param  string $expr XPath expression
    * @return bool         Whether the expression is safe in context
    */
   
protected function isExpressionSafe($expr)
    {
        return
false;
    }

   
/**
    * Test whether given tag filters attribute values
    *
    * @param  Tag  $tag
    * @return bool
    */
   
protected function tagFiltersAttributes(Tag $tag)
    {
        return
$tag->filterChain->containsCallback('s9e\\TextFormatter\\Parser\\FilterProcessing::filterAttributes');
    }
}