AbstractDynamicContentCheck   A
last analyzed

Complexity

Total Complexity 37

Size/Duplication

Total Lines 316
Duplicated Lines 0 %

Test Coverage

Coverage 98.57%

Importance

Changes 0
Metric Value
wmc 37
eloc 58
dl 0
loc 316
ccs 69
cts 70
cp 0.9857
rs 9.44
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A checkContext() 0 9 2
A check() 0 6 2
A checkVariable() 0 5 1
A checkAttributeExpression() 0 6 2
A detectUnknownAttributes() 0 3 1
A checkElementNode() 0 23 4
A checkSelectNode() 0 3 1
A isExpressionSafe() 0 3 1
A checkVariableDeclaration() 0 17 3
A checkNode() 0 15 5
A checkExpression() 0 17 4
A tagFiltersAttributes() 0 3 1
A checkAttribute() 0 17 5
A ignoreUnknownAttributes() 0 3 1
A checkCopyOfNode() 0 3 1
A checkAttributeNode() 0 8 3
1
<?php
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) The s9e authors
6
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
7
*/
8
namespace s9e\TextFormatter\Configurator\TemplateChecks;
9
10
use DOMAttr;
11
use DOMElement;
12
use DOMNode;
13
use DOMXPath;
14
use s9e\TextFormatter\Configurator\Exceptions\UnsafeTemplateException;
15
use s9e\TextFormatter\Configurator\Helpers\AVTHelper;
16
use s9e\TextFormatter\Configurator\Items\Attribute;
17
use s9e\TextFormatter\Configurator\Items\Tag;
18
use s9e\TextFormatter\Configurator\TemplateCheck;
19
20
abstract class AbstractDynamicContentCheck extends TemplateCheck
21
{
22
	/**
23
	* @var bool Whether to ignore unknown attributes
24
	*/
25
	protected $ignoreUnknownAttributes = false;
26
27
	/**
28
	* Get the nodes targeted by this check
29
	*
30
	* @param  DOMElement $template <xsl:template/> node
31
	* @return array             Array of DOMElement instances
32
	*/
33
	abstract protected function getNodes(DOMElement $template);
34
35
	/**
36
	* Return whether an attribute is considered safe
37
	*
38
	* @param  Attribute $attribute Attribute
39
	* @return bool
40
	*/
41
	abstract protected function isSafe(Attribute $attribute);
42
43
	/**
44
	* Look for improperly-filtered dynamic content
45
	*
46
	* @param  DOMElement $template <xsl:template/> node
47
	* @param  Tag        $tag      Tag this template belongs to
48
	* @return void
49
	*/
50 46
	public function check(DOMElement $template, Tag $tag)
51
	{
52 46
		foreach ($this->getNodes($template) as $node)
53
		{
54
			// Test this node's safety
55 46
			$this->checkNode($node, $tag);
56
		}
57
	}
58
59
	/**
60
	* Configure this template check to detect unknown attributes
61
	*
62
	* @return void
63
	*/
64 1
	public function detectUnknownAttributes()
65
	{
66 1
		$this->ignoreUnknownAttributes = false;
67
	}
68
69
	/**
70
	* Configure this template check to ignore unknown attributes
71
	*
72
	* @return void
73
	*/
74 1
	public function ignoreUnknownAttributes()
75
	{
76 1
		$this->ignoreUnknownAttributes = true;
77
	}
78
79
	/**
80
	* Test whether a tag attribute is safe
81
	*
82
	* @param  DOMNode $node     Context node
83
	* @param  Tag     $tag      Source tag
84
	* @param  string  $attrName Name of the attribute
85
	* @return void
86
	*/
87 34
	protected function checkAttribute(DOMNode $node, Tag $tag, $attrName)
88
	{
89
		// Test whether the attribute exists
90 34
		if (!isset($tag->attributes[$attrName]))
91
		{
92 15
			if ($this->ignoreUnknownAttributes)
93
			{
94
				return;
95
			}
96
97 15
			throw new UnsafeTemplateException("Cannot assess the safety of unknown attribute '" . $attrName . "'", $node);
98
		}
99
100
		// Test whether the attribute is safe to be used in this content type
101 19
		if (!$this->tagFiltersAttributes($tag) || !$this->isSafe($tag->attributes[$attrName]))
102
		{
103 19
			throw new UnsafeTemplateException("Attribute '" . $attrName . "' is not properly sanitized to be used in this context", $node);
104
		}
105
	}
106
107
	/**
108
	* Test whether an attribute expression is safe
109
	*
110
	* @param  DOMNode $node Context node
111
	* @param  Tag     $tag  Source tag
112
	* @param  string  $expr XPath expression that evaluates to one or multiple named attributes
113
	* @return void
114
	*/
115 34
	protected function checkAttributeExpression(DOMNode $node, Tag $tag, $expr)
116
	{
117 34
		preg_match_all('(@([-\\w]+))', $expr, $matches);
118 34
		foreach ($matches[1] as $attrName)
119
		{
120 34
			$this->checkAttribute($node, $tag, $attrName);
121
		}
122
	}
123
124
	/**
125
	* Test whether an attribute node is safe
126
	*
127
	* @param  DOMAttr $attribute Attribute node
128
	* @param  Tag     $tag       Reference tag
129
	* @return void
130
	*/
131 14
	protected function checkAttributeNode(DOMAttr $attribute, Tag $tag)
132
	{
133
		// Parse the attribute value for XPath expressions and assess their safety
134 14
		foreach (AVTHelper::parse($attribute->value) as $token)
135
		{
136 14
			if ($token[0] === 'expression')
137
			{
138 14
				$this->checkExpression($attribute, $token[1], $tag);
139
			}
140
		}
141
	}
142
143
	/**
144
	* Test whether a node's context can be safely assessed
145
	*
146
	* @param  DOMNode $node Source node
147
	* @return void
148
	*/
149 43
	protected function checkContext(DOMNode $node)
150
	{
151
		// Test whether we know in what context this node is used. An <xsl:for-each/> ancestor would // change this node's context
152 43
		$xpath     = new DOMXPath($node->ownerDocument);
153 43
		$ancestors = $xpath->query('ancestor::xsl:for-each', $node);
154
155 43
		if ($ancestors->length)
156
		{
157 3
			throw new UnsafeTemplateException("Cannot assess context due to '" . $ancestors->item(0)->nodeName . "'", $node);
158
		}
159
	}
160
161
	/**
162
	* Test whether an <xsl:copy-of/> node is safe
163
	*
164
	* @param  DOMElement $node <xsl:copy-of/> node
165
	* @param  Tag        $tag  Reference tag
166
	* @return void
167
	*/
168 7
	protected function checkCopyOfNode(DOMElement $node, Tag $tag)
169
	{
170 7
		$this->checkSelectNode($node->getAttributeNode('select'), $tag);
171
	}
172
173
	/**
174
	* Test whether an element node is safe
175
	*
176
	* @param  DOMElement $element Element
177
	* @param  Tag        $tag     Reference tag
178
	* @return void
179
	*/
180 26
	protected function checkElementNode(DOMElement $element, Tag $tag)
181
	{
182 26
		$xpath = new DOMXPath($element->ownerDocument);
0 ignored issues
show
Bug introduced by
It seems like $element->ownerDocument can also be of type null; however, parameter $document of DOMXPath::__construct() does only seem to accept DOMDocument, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

182
		$xpath = new DOMXPath(/** @scrutinizer ignore-type */ $element->ownerDocument);
Loading history...
183
184
		// If current node is not an <xsl:attribute/> element, we exclude descendants
185
		// with an <xsl:attribute/> ancestor so that content such as:
186
		//   <script><xsl:attribute name="id"><xsl:value-of/></xsl:attribute></script>
187
		// would not trigger a false-positive due to the presence of an <xsl:value-of/>
188
		// element in a <script>
189 26
		$predicate = ($element->localName === 'attribute') ? '' : '[not(ancestor::xsl:attribute)]';
190
191
		// Test the select expression of <xsl:value-of/> nodes
192 26
		$query = './/xsl:value-of' . $predicate;
193 26
		foreach ($xpath->query($query, $element) as $valueOf)
194
		{
195 22
			$this->checkSelectNode($valueOf->getAttributeNode('select'), $tag);
196
		}
197
198
		// Reject all <xsl:apply-templates/> nodes
199 4
		$query = './/xsl:apply-templates' . $predicate;
200 4
		foreach ($xpath->query($query, $element) as $applyTemplates)
201
		{
202 3
			throw new UnsafeTemplateException('Cannot allow unfiltered data in this context', $applyTemplates);
203
		}
204
	}
205
206
	/**
207
	* Test the safety of an XPath expression
208
	*
209
	* @param  DOMNode $node Source node
210
	* @param  string  $expr XPath expression
211
	* @param  Tag     $tag  Source tag
212
	* @return void
213
	*/
214 43
	protected function checkExpression(DOMNode $node, $expr, Tag $tag)
215
	{
216 43
		$this->checkContext($node);
217
218 40
		if (preg_match('/^\\$(\\w+)$/', $expr, $m))
219
		{
220
			// Either this expression came from a variable that is considered safe, or it's a
221
			// stylesheet parameters, which are considered safe by default
222 5
			$this->checkVariable($node, $tag, $m[1]);
223
		}
224 40
		elseif (preg_match('/^@[-\\w]+(?:\\s*\\|\\s*@[-\\w]+)*$/', $expr))
225
		{
226 34
			$this->checkAttributeExpression($node, $tag, $expr);
227
		}
228 6
		elseif (!$this->isExpressionSafe($expr))
229
		{
230 6
			throw new UnsafeTemplateException("Cannot assess the safety of expression '" . $expr . "'", $node);
231
		}
232
	}
233
234
	/**
235
	* Test whether a node is safe
236
	*
237
	* @param  DOMNode $node Source node
238
	* @param  Tag     $tag  Reference tag
239
	* @return void
240
	*/
241 46
	protected function checkNode(DOMNode $node, Tag $tag)
242
	{
243 46
		if ($node instanceof DOMAttr)
244
		{
245 14
			$this->checkAttributeNode($node, $tag);
246
		}
247 32
		elseif ($node instanceof DOMElement)
248
		{
249 32
			if ($node->namespaceURI === self::XMLNS_XSL && $node->localName === 'copy-of')
250
			{
251 7
				$this->checkCopyOfNode($node, $tag);
252
			}
253
			else
254
			{
255 26
				$this->checkElementNode($node, $tag);
256
			}
257
		}
258
	}
259
260
	/**
261
	* Check whether a variable is safe in context
262
	*
263
	* @param  DOMNode $node  Context node
264
	* @param  Tag     $tag   Source tag
265
	* @param  string  $qname Name of the variable
266
	* @return void
267
	*/
268 5
	protected function checkVariable(DOMNode $node, $tag, $qname)
269
	{
270
		// Test whether this variable comes from a previous xsl:param or xsl:variable element
271 5
		$this->checkVariableDeclaration($node, $tag, 'xsl:param[@name="' . $qname . '"]');
272 4
		$this->checkVariableDeclaration($node, $tag, 'xsl:variable[@name="' . $qname . '"]');
273
	}
274
275
	/**
276
	* Check whether a variable declaration is safe in context
277
	*
278
	* @param  DOMNode $node  Context node
279
	* @param  Tag     $tag   Source tag
280
	* @param  string  $query XPath query
281
	* @return void
282
	*/
283 5
	protected function checkVariableDeclaration(DOMNode $node, $tag, $query)
284
	{
285 5
		$query = 'ancestor-or-self::*/preceding-sibling::' . $query . '[@select]';
286 5
		$xpath = new DOMXPath($node->ownerDocument);
287 5
		foreach ($xpath->query($query, $node) as $varNode)
288
		{
289
			// Intercept the UnsafeTemplateException and change the node to the one we're
290
			// really checking before rethrowing it
291
			try
292
			{
293 5
				$this->checkExpression($varNode, $varNode->getAttribute('select'), $tag);
294
			}
295 5
			catch (UnsafeTemplateException $e)
296
			{
297 5
				$e->setNode($node);
298
299 5
				throw $e;
300
			}
301
		}
302
	}
303
304
	/**
305
	* Test whether a select attribute of a node is safe
306
	*
307
	* @param  DOMAttr $select Select attribute node
308
	* @param  Tag     $tag    Reference tag
309
	* @return void
310
	*/
311 29
	protected function checkSelectNode(DOMAttr $select, Tag $tag)
312
	{
313 29
		$this->checkExpression($select, $select->value, $tag);
314
	}
315
316
	/**
317
	* Test whether given expression is safe in context
318
	*
319
	* @param  string $expr XPath expression
320
	* @return bool         Whether the expression is safe in context
321
	*/
322 2
	protected function isExpressionSafe($expr)
323
	{
324 2
		return false;
325
	}
326
327
	/**
328
	* Test whether given tag filters attribute values
329
	*
330
	* @param  Tag  $tag
331
	* @return bool
332
	*/
333 19
	protected function tagFiltersAttributes(Tag $tag)
334
	{
335 19
		return $tag->filterChain->containsCallback('s9e\\TextFormatter\\Parser\\FilterProcessing::filterAttributes');
336
	}
337
}