Completed
Push — master ( d91fed...fd66aa )
by Josh
17:36
created

AbstractDynamicContentCheck::checkNode()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 9.3222
c 0
b 0
f 0
cc 5
nc 4
nop 2
1
<?php
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) 2010-2018 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
	public function check(DOMElement $template, Tag $tag)
51
	{
52
		foreach ($this->getNodes($template) as $node)
53
		{
54
			// Test this node's safety
55
			$this->checkNode($node, $tag);
56
		}
57
	}
58
59
	/**
60
	* Configure this template check to detect unknown attributes
61
	*
62
	* @return void
63
	*/
64
	public function detectUnknownAttributes()
65
	{
66
		$this->ignoreUnknownAttributes = false;
67
	}
68
69
	/**
70
	* Configure this template check to ignore unknown attributes
71
	*
72
	* @return void
73
	*/
74
	public function ignoreUnknownAttributes()
75
	{
76
		$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
	protected function checkAttribute(DOMNode $node, Tag $tag, $attrName)
88
	{
89
		// Test whether the attribute exists
90
		if (!isset($tag->attributes[$attrName]))
91
		{
92
			if ($this->ignoreUnknownAttributes)
93
			{
94
				return;
95
			}
96
97
			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
		if (!$this->tagFiltersAttributes($tag) || !$this->isSafe($tag->attributes[$attrName]))
102
		{
103
			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 node is safe
109
	*
110
	* @param  DOMAttr $attribute Attribute node
111
	* @param  Tag     $tag       Reference tag
112
	* @return void
113
	*/
114
	protected function checkAttributeNode(DOMAttr $attribute, Tag $tag)
115
	{
116
		// Parse the attribute value for XPath expressions and assess their safety
117
		foreach (AVTHelper::parse($attribute->value) as $token)
118
		{
119
			if ($token[0] === 'expression')
120
			{
121
				$this->checkExpression($attribute, $token[1], $tag);
122
			}
123
		}
124
	}
125
126
	/**
127
	* Test whether a node's context can be safely assessed
128
	*
129
	* @param  DOMNode $node Source node
130
	* @return void
131
	*/
132
	protected function checkContext(DOMNode $node)
133
	{
134
		// Test whether we know in what context this node is used. An <xsl:for-each/> ancestor would // change this node's context
135
		$xpath     = new DOMXPath($node->ownerDocument);
136
		$ancestors = $xpath->query('ancestor::xsl:for-each', $node);
137
138
		if ($ancestors->length)
139
		{
140
			throw new UnsafeTemplateException("Cannot assess context due to '" . $ancestors->item(0)->nodeName . "'", $node);
141
		}
142
	}
143
144
	/**
145
	* Test whether an <xsl:copy-of/> node is safe
146
	*
147
	* @param  DOMElement $node <xsl:copy-of/> node
148
	* @param  Tag        $tag  Reference tag
149
	* @return void
150
	*/
151
	protected function checkCopyOfNode(DOMElement $node, Tag $tag)
152
	{
153
		$this->checkSelectNode($node->getAttributeNode('select'), $tag);
154
	}
155
156
	/**
157
	* Test whether an element node is safe
158
	*
159
	* @param  DOMElement $element Element
160
	* @param  Tag        $tag     Reference tag
161
	* @return void
162
	*/
163
	protected function checkElementNode(DOMElement $element, Tag $tag)
164
	{
165
		$xpath = new DOMXPath($element->ownerDocument);
166
167
		// If current node is not an <xsl:attribute/> element, we exclude descendants
168
		// with an <xsl:attribute/> ancestor so that content such as:
169
		//   <script><xsl:attribute name="id"><xsl:value-of/></xsl:attribute></script>
170
		// would not trigger a false-positive due to the presence of an <xsl:value-of/>
171
		// element in a <script>
172
		$predicate = ($element->localName === 'attribute') ? '' : '[not(ancestor::xsl:attribute)]';
173
174
		// Test the select expression of <xsl:value-of/> nodes
175
		$query = './/xsl:value-of' . $predicate;
176
		foreach ($xpath->query($query, $element) as $valueOf)
177
		{
178
			$this->checkSelectNode($valueOf->getAttributeNode('select'), $tag);
179
		}
180
181
		// Reject all <xsl:apply-templates/> nodes
182
		$query = './/xsl:apply-templates' . $predicate;
183
		foreach ($xpath->query($query, $element) as $applyTemplates)
184
		{
185
			throw new UnsafeTemplateException('Cannot allow unfiltered data in this context', $applyTemplates);
186
		}
187
	}
188
189
	/**
190
	* Test the safety of an XPath expression
191
	*
192
	* @param  DOMNode $node Source node
193
	* @param  string  $expr XPath expression
194
	* @param  Tag     $tag  Source tag
195
	* @return void
196
	*/
197
	protected function checkExpression(DOMNode $node, $expr, Tag $tag)
198
	{
199
		$this->checkContext($node);
200
201
		// Consider stylesheet parameters safe but test local variables/params
202
		if (preg_match('/^\\$(\\w+)$/', $expr, $m))
203
		{
204
			$this->checkVariable($node, $tag, $m[1]);
205
206
			// Either this expression came from a variable that is considered safe, or it's a
207
			// stylesheet parameters, which are considered safe by default
208
			return;
209
		}
210
211
		// Test whether the expression is safe as per the concrete implementation
212
		if ($this->isExpressionSafe($expr))
213
		{
214
			return;
215
		}
216
217
		// Test whether the expression contains one single attribute
218
		if (preg_match('/^@(\\w+)$/', $expr, $m))
219
		{
220
			$this->checkAttribute($node, $tag, $m[1]);
221
222
			return;
223
		}
224
225
		throw new UnsafeTemplateException("Cannot assess the safety of expression '" . $expr . "'", $node);
226
	}
227
228
	/**
229
	* Test whether a node is safe
230
	*
231
	* @param  DOMNode $node Source node
232
	* @param  Tag     $tag  Reference tag
233
	* @return void
234
	*/
235
	protected function checkNode(DOMNode $node, Tag $tag)
236
	{
237
		if ($node instanceof DOMAttr)
238
		{
239
			$this->checkAttributeNode($node, $tag);
240
		}
241
		elseif ($node instanceof DOMElement)
242
		{
243
			if ($node->namespaceURI === self::XMLNS_XSL
244
			 && $node->localName    === 'copy-of')
245
			{
246
				$this->checkCopyOfNode($node, $tag);
247
			}
248
			else
249
			{
250
				$this->checkElementNode($node, $tag);
251
			}
252
		}
253
	}
254
255
	/**
256
	* Check whether a variable is safe in context
257
	*
258
	* @param  DOMNode $node  Context node
259
	* @param  Tag     $tag   Source tag
260
	* @param  string  $qname Name of the variable
261
	* @return void
262
	*/
263
	protected function checkVariable(DOMNode $node, $tag, $qname)
264
	{
265
		// Test whether this variable comes from a previous xsl:param or xsl:variable element
266
		$this->checkVariableDeclaration($node, $tag, 'xsl:param[@name="' . $qname . '"]');
267
		$this->checkVariableDeclaration($node, $tag, 'xsl:variable[@name="' . $qname . '"]');
268
	}
269
270
	/**
271
	* Check whether a variable declaration is safe in context
272
	*
273
	* @param  DOMNode $node  Context node
274
	* @param  Tag     $tag   Source tag
275
	* @param  string  $query XPath query
276
	* @return void
277
	*/
278
	protected function checkVariableDeclaration(DOMNode $node, $tag, $query)
279
	{
280
		$query = 'ancestor-or-self::*/preceding-sibling::' . $query . '[@select]';
281
		$xpath = new DOMXPath($node->ownerDocument);
282
		foreach ($xpath->query($query, $node) as $varNode)
283
		{
284
			// Intercept the UnsafeTemplateException and change the node to the one we're
285
			// really checking before rethrowing it
286
			try
287
			{
288
				$this->checkExpression($varNode, $varNode->getAttribute('select'), $tag);
289
			}
290
			catch (UnsafeTemplateException $e)
291
			{
292
				$e->setNode($node);
293
294
				throw $e;
295
			}
296
		}
297
	}
298
299
	/**
300
	* Test whether a select attribute of a node is safe
301
	*
302
	* @param  DOMAttr $select Select attribute node
303
	* @param  Tag     $tag    Reference tag
304
	* @return void
305
	*/
306
	protected function checkSelectNode(DOMAttr $select, Tag $tag)
307
	{
308
		$this->checkExpression($select, $select->value, $tag);
309
	}
310
311
	/**
312
	* Test whether given expression is safe in context
313
	*
314
	* @param  string $expr XPath expression
315
	* @return bool         Whether the expression is safe in context
316
	*/
317
	protected function isExpressionSafe($expr)
318
	{
319
		return false;
320
	}
321
322
	/**
323
	* Test whether given tag filters attribute values
324
	*
325
	* @param  Tag  $tag
326
	* @return bool
327
	*/
328
	protected function tagFiltersAttributes(Tag $tag)
329
	{
330
		return $tag->filterChain->containsCallback('s9e\\TextFormatter\\Parser\\FilterProcessing::filterAttributes');
331
	}
332
}