PluginProbe
Adminify – White Label, Admin Menu Editor, Login Customizer / 4.0.3.2
Adminify – White Label, Admin Menu Editor, Login Customizer v4.0.3.2
4.3.2 4.3.1 4.3.0 4.2.26 4.2.25 4.2.24 4.2.23 4.2.22 4.2.21 4.2.20 4.2.19 4.2.18 4.2.17 4.2.16 4.2.15 4.2.14 4.2.13 4.2.12 4.2.11 4.2.10 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 All 165 releases
adminify / vendor / enshrined / svg-sanitize / src / Sanitizer.php

Sanitizer.php in Adminify – White Label, Admin Menu Editor, Login Customizer 4.0.3.2, at vendor/enshrined/svg-sanitize/src/Sanitizer.php

474 lines 12.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace enshrined\svgSanitize;
4
5 use DOMDocument;
6 use enshrined\svgSanitize\data\AllowedAttributes;
7 use enshrined\svgSanitize\data\AllowedTags;
8 use enshrined\svgSanitize\data\AttributeInterface;
9 use enshrined\svgSanitize\data\TagInterface;
10
11 /**
12 * Class Sanitizer
13 *
14 * @package enshrined\svgSanitize
15 */
16 class Sanitizer
17 {
18
19 /**
20 * Regex to catch script and data values in attributes
21 */
22 const SCRIPT_REGEX = '/(?:\w+script|data):/xi';
23
24 /**
25 * @var DOMDocument
26 */
27 protected $xmlDocument;
28
29 /**
30 * @var array
31 */
32 protected $allowedTags;
33
34 /**
35 * @var array
36 */
37 protected $allowedAttrs;
38
39 /**
40 * @var
41 */
42 protected $xmlLoaderValue;
43
44 /**
45 * @var bool
46 */
47 protected $minifyXML = false;
48
49 /**
50 * @var bool
51 */
52 protected $removeRemoteReferences = false;
53
54 /**
55 * @var bool
56 */
57 protected $removeXMLTag = false;
58
59 /**
60 * @var int
61 */
62 protected $xmlOptions = LIBXML_NOEMPTYTAG;
63
64 /**
65 * @var array
66 */
67 protected $xmlIssues = array();
68
69 /**
70 *
71 */
72 function __construct()
73 {
74 // Load default tags/attributes
75 $this->allowedAttrs = array_map('strtolower', AllowedAttributes::getAttributes());
76 $this->allowedTags = array_map('strtolower', AllowedTags::getTags());
77 }
78
79 /**
80 * Set up the DOMDocument
81 */
82 protected function resetInternal()
83 {
84 $this->xmlDocument = new DOMDocument();
85 $this->xmlDocument->preserveWhiteSpace = false;
86 $this->xmlDocument->strictErrorChecking = false;
87 $this->xmlDocument->formatOutput = !$this->minifyXML;
88 }
89
90 /**
91 * Set XML options to use when saving XML
92 * See: DOMDocument::saveXML
93 *
94 * @param int $xmlOptions
95 */
96 public function setXMLOptions($xmlOptions)
97 {
98 $this->xmlOptions = $xmlOptions;
99 }
100
101 /**
102 * Get XML options to use when saving XML
103 * See: DOMDocument::saveXML
104 *
105 * @return int
106 */
107 public function getXMLOptions()
108 {
109 return $this->xmlOptions;
110 }
111
112 /**
113 * Get the array of allowed tags
114 *
115 * @return array
116 */
117 public function getAllowedTags()
118 {
119 return $this->allowedTags;
120 }
121
122 /**
123 * Set custom allowed tags
124 *
125 * @param TagInterface $allowedTags
126 */
127 public function setAllowedTags(TagInterface $allowedTags)
128 {
129 $this->allowedTags = array_map('strtolower', $allowedTags::getTags());
130 }
131
132 /**
133 * Get the array of allowed attributes
134 *
135 * @return array
136 */
137 public function getAllowedAttrs()
138 {
139 return $this->allowedAttrs;
140 }
141
142 /**
143 * Set custom allowed attributes
144 *
145 * @param AttributeInterface $allowedAttrs
146 */
147 public function setAllowedAttrs(AttributeInterface $allowedAttrs)
148 {
149 $this->allowedAttrs = array_map('strtolower', $allowedAttrs::getAttributes());
150 }
151
152 /**
153 * Should we remove references to remote files?
154 *
155 * @param bool $removeRemoteRefs
156 */
157 public function removeRemoteReferences($removeRemoteRefs = false)
158 {
159 $this->removeRemoteReferences = $removeRemoteRefs;
160 }
161
162 /**
163 * Get XML issues.
164 *
165 * @return array
166 */
167 public function getXmlIssues() {
168 return $this->xmlIssues;
169 }
170
171
172 /**
173 * Sanitize the passed string
174 *
175 * @param string $dirty
176 * @return string
177 */
178 public function sanitize($dirty)
179 {
180 // Don't run on an empty string
181 if (empty($dirty)) {
182 return '';
183 }
184
185 // Strip php tags
186 $dirty = preg_replace('/<\?(=|php)(.+?)\?>/i', '', $dirty);
187
188 $this->resetInternal();
189 $this->setUpBefore();
190
191 $loaded = $this->xmlDocument->loadXML($dirty);
192
193 // If we couldn't parse the XML then we go no further. Reset and return false
194 if (!$loaded) {
195 $this->resetAfter();
196 return false;
197 }
198
199 $this->removeDoctype();
200
201 // Grab all the elements
202 $allElements = $this->xmlDocument->getElementsByTagName("*");
203
204 // Start the cleaning proccess
205 $this->startClean($allElements);
206
207 // Save cleaned XML to a variable
208 if ($this->removeXMLTag) {
209 $clean = $this->xmlDocument->saveXML($this->xmlDocument->documentElement, $this->xmlOptions);
210 } else {
211 $clean = $this->xmlDocument->saveXML($this->xmlDocument, $this->xmlOptions);
212 }
213
214 $this->resetAfter();
215
216 // Remove any extra whitespaces when minifying
217 if ($this->minifyXML) {
218 $clean = preg_replace('/\s+/', ' ', $clean);
219 }
220
221 // Return result
222 return $clean;
223 }
224
225 /**
226 * Set up libXML before we start
227 */
228 protected function setUpBefore()
229 {
230 // Turn off the entity loader
231 $this->xmlLoaderValue = libxml_disable_entity_loader(true);
232
233 // Suppress the errors because we don't really have to worry about formation before cleansing
234 libxml_use_internal_errors(true);
235
236 // Reset array of altered XML
237 $this->xmlIssues = array();
238 }
239
240 /**
241 * Reset the class after use
242 */
243 protected function resetAfter()
244 {
245 // Reset the entity loader
246 libxml_disable_entity_loader($this->xmlLoaderValue);
247 }
248
249 /**
250 * Remove the XML Doctype
251 * It may be caught later on output but that seems to be buggy, so we need to make sure it's gone
252 */
253 protected function removeDoctype()
254 {
255 foreach ($this->xmlDocument->childNodes as $child) {
256 if ($child->nodeType === XML_DOCUMENT_TYPE_NODE) {
257 $child->parentNode->removeChild($child);
258 }
259 }
260 }
261
262 /**
263 * Start the cleaning with tags, then we move onto attributes and hrefs later
264 *
265 * @param \DOMNodeList $elements
266 */
267 protected function startClean(\DOMNodeList $elements)
268 {
269 // loop through all elements
270 // we do this backwards so we don't skip anything if we delete a node
271 // see comments at: http://php.net/manual/en/class.domnamednodemap.php
272 for ($i = $elements->length - 1; $i >= 0; $i--) {
273 $currentElement = $elements->item($i);
274
275 // If the tag isn't in the whitelist, remove it and continue with next iteration
276 if (!in_array(strtolower($currentElement->tagName), $this->allowedTags)) {
277 $currentElement->parentNode->removeChild($currentElement);
278 $this->xmlIssues[] = array(
279 'message' => 'Suspicious tag \'' . $currentElement->tagName . '\'',
280 'line' => $currentElement->getLineNo(),
281 );
282 continue;
283 }
284
285 $this->cleanAttributesOnWhitelist($currentElement);
286
287 $this->cleanXlinkHrefs($currentElement);
288
289 $this->cleanHrefs($currentElement);
290
291 if (strtolower($currentElement->tagName) === 'use') {
292 if ($this->isUseTagDirty($currentElement)) {
293 $currentElement->parentNode->removeChild($currentElement);
294 $this->xmlIssues[] = array(
295 'message' => 'Suspicious \'' . $currentElement->tagName . '\'',
296 'line' => $currentElement->getLineNo(),
297 );
298 continue;
299 }
300 }
301 }
302 }
303
304 /**
305 * Only allow attributes that are on the whitelist
306 *
307 * @param \DOMElement $element
308 */
309 protected function cleanAttributesOnWhitelist(\DOMElement $element)
310 {
311 for ($x = $element->attributes->length - 1; $x >= 0; $x--) {
312 // get attribute name
313 $attrName = $element->attributes->item($x)->name;
314
315 // Remove attribute if not in whitelist
316 if (!in_array(strtolower($attrName), $this->allowedAttrs) && !$this->isAriaAttribute(strtolower($attrName)) && !$this->isDataAttribute(strtolower($attrName))) {
317
318 $element->removeAttribute($attrName);
319 $this->xmlIssues[] = array(
320 'message' => 'Suspicious attribute \'' . $attrName . '\'',
321 'line' => $element->getLineNo(),
322 );
323 }
324
325 // Do we want to strip remote references?
326 if($this->removeRemoteReferences) {
327 // Remove attribute if it has a remote reference
328 if (isset($element->attributes->item($x)->value) && $this->hasRemoteReference($element->attributes->item($x)->value)) {
329 $element->removeAttribute($attrName);
330 $this->xmlIssues[] = array(
331 'message' => 'Suspicious attribute \'' . $attrName . '\'',
332 'line' => $element->getLineNo(),
333 );
334 }
335 }
336 }
337 }
338
339 /**
340 * Clean the xlink:hrefs of script and data embeds
341 *
342 * @param \DOMElement $element
343 */
344 protected function cleanXlinkHrefs(\DOMElement $element)
345 {
346 $xlinks = $element->getAttributeNS('http://www.w3.org/1999/xlink', 'href');
347 if (preg_match(self::SCRIPT_REGEX, $xlinks) === 1) {
348 if (!in_array(substr($xlinks, 0, 14), array(
349 'data:image/png', // PNG
350 'data:image/gif', // GIF
351 'data:image/jpg', // JPG
352 'data:image/jpe', // JPEG
353 'data:image/pjp', // PJPEG
354 ))) {
355 $element->removeAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
356 $this->xmlIssues[] = array(
357 'message' => 'Suspicious attribute \'href\'',
358 'line' => $element->getLineNo(),
359 );
360
361
362 }
363 }
364 }
365
366 /**
367 * Clean the hrefs of script and data embeds
368 *
369 * @param \DOMElement $element
370 */
371 protected function cleanHrefs(\DOMElement $element)
372 {
373 $href = $element->getAttribute('href');
374 if (preg_match(self::SCRIPT_REGEX, $href) === 1) {
375 $element->removeAttribute('href');
376 $this->xmlIssues[] = array(
377 'message' => 'Suspicious attribute \'href\'',
378 'line' => $element->getLineNo(),
379 );
380 }
381 }
382
383 /**
384 * Removes non-printable ASCII characters from string & trims it
385 *
386 * @param string $value
387 * @return bool
388 */
389 protected function removeNonPrintableCharacters($value)
390 {
391 return trim(preg_replace('/[^ -~]/xu','',$value));
392 }
393
394 /**
395 * Does this attribute value have a remote reference?
396 *
397 * @param $value
398 * @return bool
399 */
400 protected function hasRemoteReference($value)
401 {
402 $value = $this->removeNonPrintableCharacters($value);
403
404 $wrapped_in_url = preg_match('~^url\(\s*[\'"]\s*(.*)\s*[\'"]\s*\)$~xi', $value, $match);
405 if (!$wrapped_in_url){
406 return false;
407 }
408
409 $value = trim($match[1], '\'"');
410
411 return preg_match('~^((https?|ftp|file):)?//~xi', $value);
412 }
413
414 /**
415 * Should we minify the output?
416 *
417 * @param bool $shouldMinify
418 */
419 public function minify($shouldMinify = false)
420 {
421 $this->minifyXML = (bool) $shouldMinify;
422 }
423
424 /**
425 * Should we remove the XML tag in the header?
426 *
427 * @param bool $removeXMLTag
428 */
429 public function removeXMLTag($removeXMLTag = false)
430 {
431 $this->removeXMLTag = (bool) $removeXMLTag;
432 }
433
434 /**
435 * Check to see if an attribute is an aria attribute or not
436 *
437 * @param $attributeName
438 *
439 * @return bool
440 */
441 protected function isAriaAttribute($attributeName)
442 {
443 return strpos($attributeName, 'aria-') === 0;
444 }
445
446 /**
447 * Check to see if an attribute is an data attribute or not
448 *
449 * @param $attributeName
450 *
451 * @return bool
452 */
453 protected function isDataAttribute($attributeName)
454 {
455 return strpos($attributeName, 'data-') === 0;
456 }
457
458 /**
459 * Make sure our use tag is only referencing internal resources
460 *
461 * @param \DOMElement $element
462 * @return bool
463 */
464 protected function isUseTagDirty(\DOMElement $element)
465 {
466 $xlinks = $element->getAttributeNS('http://www.w3.org/1999/xlink', 'href');
467 if ($xlinks && substr($xlinks, 0, 1) !== '#') {
468 return true;
469 }
470
471 return false;
472 }
473 }
474