PluginProbe ʕ •ᴥ•ʔ
Kirki – Freeform Page Builder, Website Builder & Customizer / 6.2.3
Kirki – Freeform Page Builder, Website Builder & Customizer v6.2.3
6.2.5 6.2.4 6.2.3 6.2.2 6.2.1 6.2.0 6.1.1 6.1.0 6.0.14 6.0.13 6.0.12 6.0.11 6.0.10 6.0.9 6.0.8 6.0.7 6.0.6 6.0.5 6.0.4 6.0.3 6.0.2 6.0.1 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 4.0.19 4.0.20 4.0.21 4.0.22 4.0.23 4.0.24 4.1 4.2.0 5.0.0 5.1.0 5.1.1 5.2.0 5.2.1 5.2.2 5.2.3 6.0.0 trunk 3.0.40 3.0.41 3.0.42 3.0.43 3.0.44 3.0.45 3.1.0 3.1.1 3.1.2
kirki / vendor / enshrined / svg-sanitize / src / Sanitizer.php
kirki / vendor / enshrined / svg-sanitize / src Last commit date
ElementReference 3 months ago Exceptions 3 months ago data 3 months ago Helper.php 3 months ago Sanitizer.php 3 months ago svg-scanner.php 3 months ago
Sanitizer.php
748 lines
1 <?php
2 namespace enshrined\svgSanitize;
3
4 use enshrined\svgSanitize\data\AllowedAttributes;
5 use enshrined\svgSanitize\data\AllowedTags;
6 use enshrined\svgSanitize\data\AttributeInterface;
7 use enshrined\svgSanitize\data\TagInterface;
8 use enshrined\svgSanitize\data\XPath;
9 use enshrined\svgSanitize\ElementReference\Resolver;
10
11 /**
12 * Class Sanitizer
13 *
14 * @package enshrined\svgSanitize
15 */
16 class Sanitizer
17 {
18
19 /**
20 * @var \DOMDocument
21 */
22 protected $xmlDocument;
23
24 /**
25 * @var array
26 */
27 protected $allowedTags;
28
29 /**
30 * @var array
31 */
32 protected $allowedAttrs;
33
34 /**
35 * @var
36 */
37 protected $xmlLoaderValue;
38
39 /**
40 * @var bool
41 */
42 protected $xmlErrorHandlerPreviousValue;
43
44 /**
45 * @var bool
46 */
47 protected $minifyXML = false;
48
49 /**
50 * @var bool
51 */
52 protected $removeRemoteReferences = false;
53
54 /**
55 * @var int
56 */
57 protected $useThreshold = 1000;
58
59 /**
60 * @var bool
61 */
62 protected $removeXMLTag = false;
63
64 /**
65 * @var int
66 */
67 protected $xmlOptions = LIBXML_NOEMPTYTAG;
68
69 /**
70 * @var array
71 */
72 protected $xmlIssues = array();
73
74 /**
75 * @var Resolver
76 */
77 protected $elementReferenceResolver;
78
79 /**
80 * @var int
81 */
82 protected $useNestingLimit = 15;
83
84 /**
85 * @var bool
86 */
87 protected $allowHugeFiles = false;
88
89 /**
90 *
91 */
92 function __construct()
93 {
94 // Load default tags/attributes
95 $this->allowedAttrs = array_map('strtolower', AllowedAttributes::getAttributes());
96 $this->allowedTags = array_map('strtolower', AllowedTags::getTags());
97 }
98
99 /**
100 * Set up the DOMDocument
101 */
102 protected function resetInternal()
103 {
104 $this->xmlDocument = new \DOMDocument();
105 $this->xmlDocument->preserveWhiteSpace = false;
106 $this->xmlDocument->strictErrorChecking = false;
107 $this->xmlDocument->formatOutput = !$this->minifyXML;
108 }
109
110 /**
111 * Set XML options to use when saving XML
112 * See: DOMDocument::saveXML
113 *
114 * @param int $xmlOptions
115 */
116 public function setXMLOptions($xmlOptions)
117 {
118 $this->xmlOptions = $xmlOptions;
119 }
120
121 /**
122 * Get XML options to use when saving XML
123 * See: DOMDocument::saveXML
124 *
125 * @return int
126 */
127 public function getXMLOptions()
128 {
129 return $this->xmlOptions;
130 }
131
132 /**
133 * Get the array of allowed tags
134 *
135 * @return array
136 */
137 public function getAllowedTags()
138 {
139 return $this->allowedTags;
140 }
141
142 /**
143 * Set custom allowed tags
144 *
145 * @param TagInterface $allowedTags
146 */
147 public function setAllowedTags(TagInterface $allowedTags)
148 {
149 $this->allowedTags = array_map('strtolower', $allowedTags::getTags());
150 }
151
152 /**
153 * Get the array of allowed attributes
154 *
155 * @return array
156 */
157 public function getAllowedAttrs()
158 {
159 return $this->allowedAttrs;
160 }
161
162 /**
163 * Set custom allowed attributes
164 *
165 * @param AttributeInterface $allowedAttrs
166 */
167 public function setAllowedAttrs(AttributeInterface $allowedAttrs)
168 {
169 $this->allowedAttrs = array_map('strtolower', $allowedAttrs::getAttributes());
170 }
171
172 /**
173 * Should we remove references to remote files?
174 *
175 * @param bool $removeRemoteRefs
176 */
177 public function removeRemoteReferences($removeRemoteRefs = false)
178 {
179 $this->removeRemoteReferences = $removeRemoteRefs;
180 }
181
182 /**
183 * Get XML issues.
184 *
185 * @return array
186 */
187 public function getXmlIssues() {
188 return $this->xmlIssues;
189 }
190
191 /**
192 * Can we allow huge files?
193 *
194 * @return bool
195 */
196 public function getAllowHugeFiles() {
197 return $this->allowHugeFiles;
198 }
199
200 /**
201 * Set whether we can allow huge files.
202 *
203 * @param bool $allowHugeFiles
204 */
205 public function setAllowHugeFiles( $allowHugeFiles ) {
206 $this->allowHugeFiles = $allowHugeFiles;
207 }
208
209
210 /**
211 * Sanitize the passed string
212 *
213 * @param string $dirty
214 * @return string|false
215 */
216 public function sanitize($dirty)
217 {
218 // Don't run on an empty string
219 if (empty($dirty)) {
220 return '';
221 }
222
223 do {
224 /*
225 * recursively remove php tags because they can be hidden inside tags
226 * i.e. <?p<?php test?>hp echo . ' danger! ';?>
227 */
228 $dirty = preg_replace('/<\?(=|php)(.+?)\?>/i', '', $dirty);
229 } while (preg_match('/<\?(=|php)(.+?)\?>/i', $dirty) != 0);
230
231 $this->resetInternal();
232 $this->setUpBefore();
233
234 $loaded = $this->xmlDocument->loadXML($dirty, $this->getAllowHugeFiles() ? LIBXML_PARSEHUGE : 0);
235
236 // If we couldn't parse the XML then we go no further. Reset and return false
237 if (!$loaded) {
238 $this->xmlIssues = self::getXmlErrors();
239 $this->resetAfter();
240 return false;
241 }
242
243 // Pre-process all identified elements
244 $xPath = new XPath($this->xmlDocument);
245 $this->elementReferenceResolver = new Resolver($xPath, $this->useNestingLimit);
246 $this->elementReferenceResolver->collect();
247 $elementsToRemove = $this->elementReferenceResolver->getElementsToRemove();
248
249 // Start the cleaning process
250 $this->startClean($this->xmlDocument->childNodes, $elementsToRemove);
251
252 // Save cleaned XML to a variable
253 if ($this->removeXMLTag) {
254 $clean = $this->xmlDocument->saveXML($this->xmlDocument->documentElement, $this->xmlOptions);
255 } else {
256 $clean = $this->xmlDocument->saveXML($this->xmlDocument, $this->xmlOptions);
257 }
258
259 $this->resetAfter();
260
261 // Remove any extra whitespaces when minifying
262 if ($this->minifyXML) {
263 $clean = preg_replace('/\s+/', ' ', $clean);
264 }
265
266 // Return result
267 return $clean;
268 }
269
270 /**
271 * Set up libXML before we start
272 */
273 protected function setUpBefore()
274 {
275 // This function has been deprecated in PHP 8.0 because in libxml 2.9.0, external entity loading is
276 // disabled by default, so this function is no longer needed to protect against XXE attacks.
277 if (\LIBXML_VERSION < 20900) {
278 // Turn off the entity loader
279 $this->xmlLoaderValue = libxml_disable_entity_loader(true);
280 }
281
282 // Suppress the errors because we don't really have to worry about formation before cleansing.
283 // See reset in resetAfter().
284 $this->xmlErrorHandlerPreviousValue = libxml_use_internal_errors(true);
285
286 // Reset array of altered XML
287 $this->xmlIssues = array();
288 }
289
290 /**
291 * Reset the class after use
292 */
293 protected function resetAfter()
294 {
295 // This function has been deprecated in PHP 8.0 because in libxml 2.9.0, external entity loading is
296 // disabled by default, so this function is no longer needed to protect against XXE attacks.
297 if (\LIBXML_VERSION < 20900) {
298 // Reset the entity loader
299 libxml_disable_entity_loader($this->xmlLoaderValue);
300 }
301
302 libxml_clear_errors();
303 libxml_use_internal_errors($this->xmlErrorHandlerPreviousValue);
304 }
305
306 /**
307 * Start the cleaning with tags, then we move onto attributes and hrefs later
308 *
309 * @param \DOMNodeList $elements
310 * @param array $elementsToRemove
311 */
312 protected function startClean(\DOMNodeList $elements, array $elementsToRemove)
313 {
314 // loop through all elements
315 // we do this backwards so we don't skip anything if we delete a node
316 // see comments at: http://php.net/manual/en/class.domnamednodemap.php
317 for ($i = $elements->length - 1; $i >= 0; $i--) {
318 /** @var \DOMElement $currentElement */
319 $currentElement = $elements->item($i);
320
321 /**
322 * If the element has exceeded the nesting limit, we should remove it.
323 *
324 * As it's only <use> elements that cause us issues with nesting DOS attacks
325 * we should check what the element is before removing it. For now we'll only
326 * remove <use> elements.
327 */
328 if (in_array($currentElement, $elementsToRemove) && 'use' === $currentElement->nodeName) {
329 $currentElement->parentNode->removeChild($currentElement);
330 $this->xmlIssues[] = array(
331 'message' => 'Invalid \'' . $currentElement->tagName . '\'',
332 'line' => $currentElement->getLineNo(),
333 );
334 continue;
335 }
336
337 if ($currentElement instanceof \DOMElement) {
338 // If the tag isn't in the whitelist, remove it and continue with next iteration
339 if (!in_array(strtolower($currentElement->tagName), $this->allowedTags)) {
340 $currentElement->parentNode->removeChild($currentElement);
341 $this->xmlIssues[] = array(
342 'message' => 'Suspicious tag \'' . $currentElement->tagName . '\'',
343 'line' => $currentElement->getLineNo(),
344 );
345 continue;
346 }
347
348 $this->cleanHrefs( $currentElement );
349
350 $this->cleanXlinkHrefs( $currentElement );
351
352 $this->cleanAttributesOnWhitelist($currentElement);
353
354 if (strtolower($currentElement->tagName) === 'use') {
355 if ($this->isUseTagDirty($currentElement)
356 || $this->isUseTagExceedingThreshold($currentElement)
357 ) {
358 $currentElement->parentNode->removeChild($currentElement);
359 $this->xmlIssues[] = array(
360 'message' => 'Suspicious \'' . $currentElement->tagName . '\'',
361 'line' => $currentElement->getLineNo(),
362 );
363 continue;
364 }
365 }
366
367 // Strip out font elements that will break out of foreign content.
368 if (strtolower($currentElement->tagName) === 'font') {
369 $breaksOutOfForeignContent = false;
370 for ($x = $currentElement->attributes->length - 1; $x >= 0; $x--) {
371 // get attribute name
372 $attrName = $currentElement->attributes->item( $x )->nodeName;
373
374 if (in_array(strtolower($attrName), ['face', 'color', 'size'])) {
375 $breaksOutOfForeignContent = true;
376 }
377 }
378
379 if ($breaksOutOfForeignContent) {
380 $currentElement->parentNode->removeChild($currentElement);
381 $this->xmlIssues[] = array(
382 'message' => 'Suspicious tag \'' . $currentElement->tagName . '\'',
383 'line' => $currentElement->getLineNo(),
384 );
385 continue;
386 }
387 }
388 }
389
390 $this->cleanUnsafeNodes($currentElement);
391
392 if ($currentElement->hasChildNodes()) {
393 $this->startClean($currentElement->childNodes, $elementsToRemove);
394 }
395 }
396 }
397
398 /**
399 * Only allow attributes that are on the whitelist
400 *
401 * @param \DOMElement $element
402 */
403 protected function cleanAttributesOnWhitelist(\DOMElement $element)
404 {
405 for ($x = $element->attributes->length - 1; $x >= 0; $x--) {
406 // get attribute name
407 $attrName = $element->attributes->item($x)->nodeName;
408
409 // Remove attribute if not in whitelist
410 if (!in_array(strtolower($attrName), $this->allowedAttrs) && !$this->isAriaAttribute(strtolower($attrName)) && !$this->isDataAttribute(strtolower($attrName))) {
411
412 $element->removeAttribute($attrName);
413 $this->xmlIssues[] = array(
414 'message' => 'Suspicious attribute \'' . $attrName . '\'',
415 'line' => $element->getLineNo(),
416 );
417 }
418
419 /**
420 * This is used for when a namespace isn't imported properly.
421 * Such as xlink:href when the xlink namespace isn't imported.
422 * We have to do this as the link is still ran in this case.
423 */
424 if (false !== stripos($attrName, 'href')) {
425 $href = $element->getAttribute($attrName);
426 if (false === $this->isHrefSafeValue($href)) {
427 $element->removeAttribute($attrName);
428 $this->xmlIssues[] = array(
429 'message' => 'Suspicious attribute \'href\'',
430 'line' => $element->getLineNo(),
431 );
432 }
433 }
434
435 // Do we want to strip remote references?
436 if($this->removeRemoteReferences) {
437 // Remove attribute if it has a remote reference
438 if (isset($element->attributes->item($x)->value) && $this->hasRemoteReference($element->attributes->item($x)->value)) {
439 $element->removeAttribute($attrName);
440 $this->xmlIssues[] = array(
441 'message' => 'Suspicious attribute \'' . $attrName . '\'',
442 'line' => $element->getLineNo(),
443 );
444 }
445 }
446 }
447 }
448
449 /**
450 * Clean the xlink:hrefs of script and data embeds
451 *
452 * @param \DOMElement $element
453 */
454 protected function cleanXlinkHrefs(\DOMElement $element)
455 {
456 foreach ($element->attributes as $attribute) {
457 // remove attributes with unexpected namespace prefix, e.g. `XLinK:href` (instead of `xlink:href`)
458 if ($attribute->prefix === '' && strtolower($attribute->nodeName) === 'xlink:href') {
459 $element->removeAttribute($attribute->nodeName);
460 $this->xmlIssues[] = array(
461 'message' => sprintf('Unexpected attribute \'%s\'', $attribute->nodeName),
462 'line' => $element->getLineNo(),
463 );
464 }
465 }
466 $this->cleanHrefAttributes($element, 'xlink');
467 }
468
469 /**
470 * Clean the hrefs of script and data embeds
471 *
472 * @param \DOMElement $element
473 */
474 protected function cleanHrefs(\DOMElement $element)
475 {
476 $this->cleanHrefAttributes($element);
477 }
478
479 protected function cleanHrefAttributes(\DOMElement $element, string $prefix = ''): void
480 {
481 $relevantAttributes = array_filter(
482 iterator_to_array($element->attributes),
483 static function (\DOMAttr $attr) use ($prefix) {
484 return strtolower($attr->name) === 'href' && strtolower($attr->prefix) === $prefix;
485 }
486 );
487 foreach ($relevantAttributes as $attribute) {
488 if (!$this->isHrefSafeValue($attribute->value)) {
489 $element->removeAttribute($attribute->nodeName);
490 $this->xmlIssues[] = array(
491 'message' => sprintf('Suspicious attribute \'%s\'', $attribute->nodeName),
492 'line' => $element->getLineNo(),
493 );
494 continue;
495 }
496 // in case the attribute name is `HrEf`/`xlink:HrEf`, adjust it to `href`/`xlink:href`
497 if (!in_array($attribute->nodeName, $this->allowedAttrs, true)
498 && in_array(strtolower($attribute->nodeName), $this->allowedAttrs, true)
499 ) {
500 $element->removeAttribute($attribute->nodeName);
501 $element->setAttribute(strtolower($attribute->nodeName), $attribute->value);
502 }
503 }
504 }
505
506 /**
507 * Only allow whitelisted starts to be within the href.
508 *
509 * This will stop scripts etc from being passed through, with or without attempting to hide bypasses.
510 * This stops the need for us to use a complicated script regex.
511 *
512 * @param $value
513 * @return bool
514 */
515 protected function isHrefSafeValue($value) {
516
517 // Allow empty values
518 if (empty($value)) {
519 return true;
520 }
521
522 // Allow fragment identifiers.
523 if ('#' === substr($value, 0, 1)) {
524 return true;
525 }
526
527 // Allow relative URIs.
528 if ('/' === substr($value, 0, 1)) {
529 return true;
530 }
531
532 // Allow HTTPS domains.
533 if ('https://' === substr($value, 0, 8)) {
534 return true;
535 }
536
537 // Allow HTTP domains.
538 if ('http://' === substr($value, 0, 7)) {
539 return true;
540 }
541
542 // Allow known data URIs.
543 if (in_array(substr($value, 0, 14), array(
544 'data:image/png', // PNG
545 'data:image/gif', // GIF
546 'data:image/jpg', // JPG
547 'data:image/jpe', // JPEG
548 'data:image/pjp', // PJPEG
549 ))) {
550 return true;
551 }
552
553 // Allow known short data URIs.
554 if (in_array(substr($value, 0, 12), array(
555 'data:img/png', // PNG
556 'data:img/gif', // GIF
557 'data:img/jpg', // JPG
558 'data:img/jpe', // JPEG
559 'data:img/pjp', // PJPEG
560 ))) {
561 return true;
562 }
563
564 return false;
565 }
566
567 /**
568 * Removes non-printable ASCII characters from string & trims it
569 *
570 * @param string $value
571 * @return bool
572 */
573 protected function removeNonPrintableCharacters($value)
574 {
575 return trim(preg_replace('/[^ -~]/xu','',$value));
576 }
577
578 /**
579 * Does this attribute value have a remote reference?
580 *
581 * @param $value
582 * @return bool
583 */
584 protected function hasRemoteReference($value)
585 {
586 $value = $this->removeNonPrintableCharacters($value);
587
588 $wrapped_in_url = preg_match('~^url\(\s*[\'"]\s*(.*)\s*[\'"]\s*\)$~xi', $value, $match);
589 if (!$wrapped_in_url){
590 return false;
591 }
592
593 $value = trim($match[1], '\'"');
594
595 return preg_match('~^((https?|ftp|file):)?//~xi', $value);
596 }
597
598 /**
599 * Should we minify the output?
600 *
601 * @param bool $shouldMinify
602 */
603 public function minify($shouldMinify = false)
604 {
605 $this->minifyXML = (bool) $shouldMinify;
606 }
607
608 /**
609 * Should we remove the XML tag in the header?
610 *
611 * @param bool $removeXMLTag
612 */
613 public function removeXMLTag($removeXMLTag = false)
614 {
615 $this->removeXMLTag = (bool) $removeXMLTag;
616 }
617
618 /**
619 * Whether `<use ... xlink:href="#identifier">` elements shall be
620 * removed in case expansion would exceed this threshold.
621 *
622 * @param int $useThreshold
623 */
624 public function useThreshold($useThreshold = 1000)
625 {
626 $this->useThreshold = (int)$useThreshold;
627 }
628
629 /**
630 * Check to see if an attribute is an aria attribute or not
631 *
632 * @param $attributeName
633 *
634 * @return bool
635 */
636 protected function isAriaAttribute($attributeName)
637 {
638 return strpos($attributeName, 'aria-') === 0;
639 }
640
641 /**
642 * Check to see if an attribute is an data attribute or not
643 *
644 * @param $attributeName
645 *
646 * @return bool
647 */
648 protected function isDataAttribute($attributeName)
649 {
650 return strpos($attributeName, 'data-') === 0;
651 }
652
653 /**
654 * Make sure our use tag is only referencing internal resources
655 *
656 * @param \DOMElement $element
657 * @return bool
658 */
659 protected function isUseTagDirty(\DOMElement $element)
660 {
661 $href = Helper::getElementHref($element);
662 return $href && strpos($href, '#') !== 0;
663 }
664
665 /**
666 * Determines whether `<use ... xlink:href="#identifier">` is expanded
667 * recursively in order to create DoS scenarios. The amount of a actually
668 * used element needs to be below `$this->useThreshold`.
669 *
670 * @param \DOMElement $element
671 * @return bool
672 */
673 protected function isUseTagExceedingThreshold(\DOMElement $element)
674 {
675 if ($this->useThreshold <= 0) {
676 return false;
677 }
678 $useId = Helper::extractIdReferenceFromHref(
679 Helper::getElementHref($element)
680 );
681 if ($useId === null) {
682 return false;
683 }
684 foreach ($this->elementReferenceResolver->findByElementId($useId) as $subject) {
685 if ($subject->countUse() >= $this->useThreshold) {
686 return true;
687 }
688 }
689 return false;
690 }
691
692 /**
693 * Set the nesting limit for <use> tags.
694 *
695 * @param $limit
696 */
697 public function setUseNestingLimit($limit)
698 {
699 $this->useNestingLimit = (int) $limit;
700 }
701
702 /**
703 * Remove nodes that are either invalid or malformed.
704 *
705 * @param \DOMNode $currentElement The current element.
706 */
707 protected function cleanUnsafeNodes(\DOMNode $currentElement) {
708 // Replace CDATA node with encoded text node
709 if ($currentElement instanceof \DOMCdataSection) {
710 $textNode = $currentElement->ownerDocument->createTextNode($currentElement->nodeValue);
711 $currentElement->parentNode->replaceChild($textNode, $currentElement);
712 // If the element doesn't have a tagname, remove it and continue with next iteration
713 } elseif (!$currentElement instanceof \DOMElement && !$currentElement instanceof \DOMText) {
714 $currentElement->parentNode->removeChild($currentElement);
715 $this->xmlIssues[] = array(
716 'message' => 'Suspicious node \'' . $currentElement->nodeName . '\'',
717 'line' => $currentElement->getLineNo(),
718 );
719 return;
720 }
721
722 if ( $currentElement->childNodes && $currentElement->childNodes->length > 0 ) {
723 for ($j = $currentElement->childNodes->length - 1; $j >= 0; $j--) {
724 /** @var \DOMElement $childElement */
725 $childElement = $currentElement->childNodes->item($j);
726 $this->cleanUnsafeNodes($childElement);
727 }
728 }
729 }
730
731 /**
732 * Retrieve array of errors
733 * @return array
734 */
735 private static function getXmlErrors()
736 {
737 $errors = [];
738 foreach (libxml_get_errors() as $error) {
739 $errors[] = [
740 'message' => trim($error->message),
741 'line' => $error->line,
742 ];
743 }
744
745 return $errors;
746 }
747 }
748