PluginProbe
Elementor Website Builder – more than just a page builder / 4.0.4
Elementor Website Builder – more than just a page builder v4.0.4
4.3.0-beta3 4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 4.1.0-beta1 4.1.0-dev1 All 452 releases
elementor / core / utils / svg / svg-sanitizer.php

svg-sanitizer.php in Elementor Website Builder – more than just a page builder 4.0.4, at core/utils/svg/svg-sanitizer.php

809 lines 16.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Elementor\Core\Utils\Svg;
3
4 use Elementor\Utils;
5
6 if ( ! defined( 'ABSPATH' ) ) {
7 exit; // Exit if accessed directly.
8 }
9
10 /**
11 * Elementor SVG Sanitizer.
12 *
13 * A class that is responsible for sanitizing SVG files.
14 *
15 * @since 3.16.0
16 */
17 class Svg_Sanitizer {
18
19 /**
20 * @var \DOMDocument
21 */
22 private $svg_dom = null;
23
24 /**
25 * Sanitize File
26 *
27 * @since 3.16.0
28 * @access public
29 *
30 * @param $filename
31 * @return bool
32 */
33 public function sanitize_file( $filename ) {
34 $original_content = Utils::file_get_contents( $filename );
35 $is_encoded = $this->is_encoded( $original_content );
36
37 if ( $is_encoded ) {
38 $decoded = $this->decode_svg( $original_content );
39 if ( false === $decoded ) {
40 return false;
41 }
42 $original_content = $decoded;
43 }
44
45 $valid_svg = $this->sanitize( $original_content );
46
47 if ( false === $valid_svg ) {
48 return false;
49 }
50
51 // If we were gzipped, we need to re-zip
52 if ( $is_encoded ) {
53 $valid_svg = $this->encode_svg( $valid_svg );
54 }
55 file_put_contents( $filename, $valid_svg );
56
57 return true;
58 }
59
60 /**
61 * Sanitize
62 *
63 * @since 3.16.0
64 * @access public
65 *
66 * @param $content
67 * @return bool|string
68 */
69 public function sanitize( $content ) {
70 // Strip php tags
71 $content = $this->strip_comments( $content );
72 $content = $this->strip_php_tags( $content );
73 $content = $this->strip_line_breaks( $content );
74
75 // Find the start and end tags so we can cut out miscellaneous garbage.
76 $start = strpos( $content, '<svg' );
77 $end = strrpos( $content, '</svg>' );
78 if ( false === $start || false === $end ) {
79 return false;
80 }
81
82 $content = substr( $content, $start, ( $end - $start + 6 ) );
83
84 // If the server's PHP version is 8 or up, make sure to Disable the ability to load external entities
85 $php_version_under_eight = version_compare( PHP_VERSION, '8.0.0', '<' );
86 if ( $php_version_under_eight ) {
87 $libxml_disable_entity_loader = libxml_disable_entity_loader( true ); // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated
88 }
89 // Suppress the errors
90 $libxml_use_internal_errors = libxml_use_internal_errors( true );
91
92 // Create DomDocument instance
93 $this->svg_dom = new \DOMDocument();
94 $this->svg_dom->formatOutput = false;
95 $this->svg_dom->preserveWhiteSpace = false;
96 $this->svg_dom->strictErrorChecking = false;
97
98 $open_svg = $this->svg_dom->loadXML( $content );
99 if ( ! $open_svg ) {
100 return false;
101 }
102
103 $this->strip_doctype();
104 $this->sanitize_elements();
105
106 // Export sanitized svg to string
107 // Using documentElement to strip out <?xml version="1.0" encoding="UTF-8"...
108 $sanitized = $this->svg_dom->saveXML( $this->svg_dom->documentElement, LIBXML_NOEMPTYTAG );
109
110 // Restore defaults
111 if ( $php_version_under_eight ) {
112 libxml_disable_entity_loader( $libxml_disable_entity_loader ); // phpcs:ignore Generic.PHP.DeprecatedFunctions.Deprecated
113 }
114 libxml_use_internal_errors( $libxml_use_internal_errors );
115
116 return $sanitized;
117 }
118
119 /**
120 * Is Encoded
121 *
122 * Check if the contents of the SVG file are gzipped
123 *
124 * @see http://www.gzip.org/zlib/rfc-gzip.html#member-format
125 *
126 * @since 3.16.0
127 * @access private
128 *
129 * @param $contents
130 *
131 * @return bool
132 */
133 private function is_encoded( $contents ) {
134 $needle = "\x1f\x8b\x08";
135 if ( function_exists( 'mb_strpos' ) ) {
136 return 0 === mb_strpos( $contents, $needle );
137 } else {
138 return 0 === strpos( $contents, $needle );
139 }
140 }
141
142 /**
143 * Encode SVG
144 *
145 * @since 3.16.0
146 * @access private
147 *
148 * @param $content
149 * @return string
150 */
151 private function encode_svg( $content ) {
152 return gzencode( $content );
153 }
154
155 /**
156 * Decode SVG
157 *
158 * @since 3.16.0
159 * @access private
160 *
161 * @param $content
162 *
163 * @return string
164 */
165 private function decode_svg( $content ) {
166 return gzdecode( $content );
167 }
168
169 /**
170 * Is Allowed Tag
171 *
172 * @since 3.16.0
173 * @access private
174 *
175 * @param $element
176 * @return bool
177 */
178 private function is_allowed_tag( $element ) {
179 static $allowed_tags = false;
180 if ( false === $allowed_tags ) {
181 $allowed_tags = $this->get_allowed_elements();
182 }
183
184 $tag_name = $element->tagName; // phpcs:ignore -- php DomDocument
185
186 if ( ! in_array( strtolower( $tag_name ), $allowed_tags ) ) {
187 $this->remove_element( $element );
188 return false;
189 }
190
191 return true;
192 }
193
194 /**
195 * Remove Element
196 *
197 * Removes the passed element from its DomDocument tree
198 *
199 * @since 3.16.0
200 * @access private
201 *
202 * @param $element
203 */
204 private function remove_element( $element ) {
205 $element->parentNode->removeChild( $element ); // phpcs:ignore -- php DomDocument
206 }
207
208 /**
209 * Is It An Attribute
210 *
211 * @since 3.16.0
212 * @access private
213 *
214 * @param $name
215 * @param $check
216 * @return bool
217 */
218 private function is_a_attribute( $name, $check ) {
219 return 0 === strpos( $name, $check . '-' );
220 }
221
222 /**
223 * Is Remote Value
224 *
225 * @since 3.16.0
226 * @access private
227 *
228 * @param $value
229 * @return string
230 */
231 private function is_remote_value( $value ) {
232 $value = trim( preg_replace( '/[^ -~]/xu', '', $value ) );
233 $wrapped_in_url = preg_match( '~^url\(\s*[\'"]\s*(.*)\s*[\'"]\s*\)$~xi', $value, $match );
234 if ( ! $wrapped_in_url ) {
235 return false;
236 }
237
238 $value = trim( $match[1], '\'"' );
239 return preg_match( '~^((https?|ftp|file):)?//~xi', $value );
240 }
241
242 /**
243 * Has JS Value
244 *
245 * @since 3.16.0
246 * @access private
247 *
248 * @param $value
249 * @return false|int
250 */
251 private function has_js_value( $value ) {
252 return preg_match( '/base64|data|(?:java)?script|alert\(|window\.|document/i', $value );
253 }
254
255 /**
256 * Get Allowed Attributes
257 *
258 * Returns an array of allowed tag attributes in SVG files.
259 *
260 * @since 3.16.0
261 * @access private
262 *
263 * @return array
264 */
265 private function get_allowed_attributes() {
266 $allowed_attributes = [
267 'accent-height',
268 'accumulate',
269 'additivive',
270 'alignment-baseline',
271 'aria-hidden',
272 'aria-controls',
273 'aria-describedby',
274 'aria-description',
275 'aria-expanded',
276 'aria-haspopup',
277 'aria-label',
278 'aria-labelledby',
279 'aria-roledescription',
280 'ascent',
281 'attributename',
282 'attributetype',
283 'azimuth',
284 'basefrequency',
285 'baseline-shift',
286 'begin',
287 'bias',
288 'by',
289 'class',
290 'clip',
291 'clip-path',
292 'clip-rule',
293 'clippathunits',
294 'color',
295 'color-interpolation',
296 'color-interpolation-filters',
297 'color-profile',
298 'color-rendering',
299 'cx',
300 'cy',
301 'd',
302 'dx',
303 'dy',
304 'diffuseconstant',
305 'direction',
306 'display',
307 'divisor',
308 'dominant-baseline',
309 'dur',
310 'edgemode',
311 'elevation',
312 'end',
313 'fill',
314 'fill-opacity',
315 'fill-rule',
316 'filter',
317 'filterres',
318 'filterunits',
319 'flood-color',
320 'flood-opacity',
321 'font-family',
322 'font-size',
323 'font-size-adjust',
324 'font-stretch',
325 'font-style',
326 'font-variant',
327 'font-weight',
328 'fx',
329 'fy',
330 'g1',
331 'g2',
332 'glyph-name',
333 'glyphref',
334 'gradienttransform',
335 'gradientunits',
336 'height',
337 'href',
338 'id',
339 'image-rendering',
340 'in',
341 'in2',
342 'k',
343 'k1',
344 'k2',
345 'k3',
346 'k4',
347 'kerning',
348 'keypoints',
349 'keysplines',
350 'keytimes',
351 'lang',
352 'lengthadjust',
353 'letter-spacing',
354 'kernelmatrix',
355 'kernelunitlength',
356 'lighting-color',
357 'local',
358 'marker-end',
359 'marker-mid',
360 'marker-start',
361 'markerheight',
362 'markerunits',
363 'markerwidth',
364 'mask',
365 'maskcontentunits',
366 'maskunits',
367 'max',
368 'media',
369 'method',
370 'mode',
371 'min',
372 'name',
373 'numoctaves',
374 'offset',
375 'opacity',
376 'operator',
377 'order',
378 'orient',
379 'orientation',
380 'origin',
381 'overflow',
382 'paint-order',
383 'path',
384 'pathlength',
385 'patterncontentunits',
386 'patterntransform',
387 'patternunits',
388 'points',
389 'preservealpha',
390 'preserveaspectratio',
391 'primitiveunits',
392 'r',
393 'rx',
394 'ry',
395 'radius',
396 'refx',
397 'refy',
398 'repeatcount',
399 'repeatdur',
400 'requiredfeatures',
401 'restart',
402 'result',
403 'role',
404 'rotate',
405 'scale',
406 'seed',
407 'shape-rendering',
408 'spacing',
409 'specularconstant',
410 'specularexponent',
411 'spreadmethod',
412 'startoffset',
413 'stddeviation',
414 'stitchtiles',
415 'stop-color',
416 'stop-opacity',
417 'stroke',
418 'stroke-dasharray',
419 'stroke-dashoffset',
420 'stroke-linecap',
421 'stroke-linejoin',
422 'stroke-miterlimit',
423 'stroke-opacity',
424 'stroke-width',
425 'style',
426 'surfacescale',
427 'systemlanguage',
428 'tabindex',
429 'targetx',
430 'targety',
431 'transform',
432 'transform-origin',
433 'text-anchor',
434 'text-decoration',
435 'text-rendering',
436 'textlength',
437 'type',
438 'u1',
439 'u2',
440 'underline-position',
441 'underline-thickness',
442 'unicode',
443 'unicode-bidi',
444 'values',
445 'vector-effect',
446 'vert-adv-y',
447 'vert-origin-x',
448 'vert-origin-y',
449 'viewbox',
450 'visibility',
451 'width',
452 'word-spacing',
453 'wrap',
454 'writing-mode',
455 'x',
456 'x1',
457 'x2',
458 'xchannelselector',
459 'xlink:href',
460 'xlink:title',
461 'xmlns',
462 'xmlns:se',
463 'xmlns:xlink',
464 'xml:lang',
465 'xml:space',
466 'y',
467 'y1',
468 'y2',
469 'ychannelselector',
470 'z',
471 'zoomandpan',
472 ];
473
474 /**
475 * Allowed attributes in SVG file.
476 *
477 * Filters the list of allowed attributes in SVG files.
478 *
479 * Since SVG files can run JS code that may inject malicious code, all attributes
480 * are removed except the allowed attributes.
481 *
482 * This hook can be used to manage allowed SVG attributes. To either add new
483 * attributes or delete existing attributes. To strengthen or weaken site security.
484 *
485 * @param array $allowed_attributes A list of allowed attributes.
486 */
487 $allowed_attributes = apply_filters( 'elementor/files/svg/allowed_attributes', $allowed_attributes );
488
489 return $allowed_attributes;
490 }
491
492 /**
493 * Get Allowed Elements
494 *
495 * Returns an array of allowed element tags to be in SVG files.
496 *
497 * @since 3.16.0
498 * @access private
499 *
500 * @return array
501 */
502 private function get_allowed_elements() {
503 $allowed_elements = [
504 'a',
505 'animate',
506 'animateMotion',
507 'animateTransform',
508 'circle',
509 'clippath',
510 'defs',
511 'desc',
512 'ellipse',
513 'feBlend',
514 'feColorMatrix',
515 'feComponentTransfer',
516 'feComposite',
517 'feConvolveMatrix',
518 'feDiffuseLighting',
519 'feDisplacementMap',
520 'feDistantLight',
521 'feDropShadow',
522 'feFlood',
523 'feFuncA',
524 'feFuncB',
525 'feFuncG',
526 'feFuncR',
527 'feGaussianBlur',
528 'feImage',
529 'feMerge',
530 'feMergeNode',
531 'feMorphology',
532 'feOffset',
533 'fePointLight',
534 'feSpecularLighting',
535 'feSpotLight',
536 'feTile',
537 'feTurbulence',
538 'filter',
539 'foreignobject',
540 'g',
541 'image',
542 'line',
543 'lineargradient',
544 'marker',
545 'mask',
546 'metadata',
547 'mpath',
548 'path',
549 'pattern',
550 'polygon',
551 'polyline',
552 'radialgradient',
553 'rect',
554 'set',
555 'stop',
556 'style',
557 'svg',
558 'switch',
559 'symbol',
560 'text',
561 'textpath',
562 'title',
563 'tspan',
564 'use',
565 'view',
566 ];
567
568 /**
569 * Allowed elements in SVG file.
570 *
571 * Filters the list of allowed elements in SVG files.
572 *
573 * Since SVG files can run JS code that may inject malicious code, all elements
574 * are removed except the allowed elements.
575 *
576 * This hook can be used to manage SVG elements. To either add new elements or
577 * delete existing elements. To strengthen or weaken site security.
578 *
579 * @param array $allowed_elements A list of allowed elements.
580 */
581 $allowed_elements = apply_filters( 'elementor/files/svg/allowed_elements', $allowed_elements );
582
583 return $allowed_elements;
584 }
585
586 /**
587 * Validate Allowed Attributes
588 *
589 * @since 3.16.0
590 * @access private
591 *
592 * @param \DOMElement $element
593 */
594 private function validate_allowed_attributes( $element ) {
595 static $allowed_attributes = false;
596 if ( false === $allowed_attributes ) {
597 $allowed_attributes = $this->get_allowed_attributes();
598 }
599
600 for ( $index = $element->attributes->length - 1; $index >= 0; $index-- ) {
601 // get attribute name
602 $attr_name = $element->attributes->item( $index )->name;
603 $attr_name_lowercase = strtolower( $attr_name );
604 // Remove attribute if not in whitelist
605 if ( ! in_array( $attr_name_lowercase, $allowed_attributes ) && ! $this->is_a_attribute( $attr_name_lowercase, 'aria' ) && ! $this->is_a_attribute( $attr_name_lowercase, 'data' ) ) {
606 $element->removeAttribute( $attr_name );
607 continue;
608 }
609
610 $attr_value = $element->attributes->item( $index )->value;
611
612 // Remove attribute if it has a remote reference or js or data-URI/base64
613 if ( ! empty( $attr_value ) && ( $this->is_remote_value( $attr_value ) || $this->has_js_value( $attr_value ) ) ) {
614 $element->removeAttribute( $attr_name );
615 continue;
616 }
617 }
618 }
619
620 /**
621 * Strip xlinks
622 *
623 * @since 3.16.0
624 * @access private
625 *
626 * @param \DOMElement $element
627 */
628 private function strip_xlinks( $element ) {
629 $xlinks = $element->getAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
630
631 if ( ! $xlinks ) {
632 return;
633 }
634
635 if ( ! $this->is_safe_href( $xlinks ) ) {
636 $element->removeAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
637 }
638 }
639
640 /**
641 * @see https://github.com/darylldoyle/svg-sanitizer/blob/2321a914e/src/Sanitizer.php#L454
642 */
643 private function is_safe_href( $value ) {
644 // Allow empty values.
645 if ( empty( $value ) ) {
646 return true;
647 }
648
649 // Allow fragment identifiers.
650 if ( '#' === substr( $value, 0, 1 ) ) {
651 return true;
652 }
653
654 // Allow relative URIs.
655 if ( '/' === substr( $value, 0, 1 ) ) {
656 return true;
657 }
658
659 // Allow HTTPS domains.
660 if ( 'https://' === substr( $value, 0, 8 ) ) {
661 return true;
662 }
663
664 // Allow HTTP domains.
665 if ( 'http://' === substr( $value, 0, 7 ) ) {
666 return true;
667 }
668
669 // Allow known data URIs.
670 if ( in_array( substr( $value, 0, 14 ), [
671 'data:image/png', // PNG
672 'data:image/gif', // GIF
673 'data:image/jpg', // JPG
674 'data:image/jpe', // JPEG
675 'data:image/pjp', // PJPEG
676 ], true ) ) {
677 return true;
678 }
679
680 // Allow known short data URIs.
681 if ( in_array( substr( $value, 0, 12 ), [
682 'data:img/png', // PNG
683 'data:img/gif', // GIF
684 'data:img/jpg', // JPG
685 'data:img/jpe', // JPEG
686 'data:img/pjp', // PJPEG
687 ], true ) ) {
688 return true;
689 }
690
691 return false;
692 }
693
694 /**
695 * Validate Use Tag
696 *
697 * @since 3.16.0
698 * @access private
699 *
700 * @param $element
701 */
702 private function validate_use_tag( $element ) {
703 $xlinks = $element->getAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
704 if ( $xlinks && '#' !== substr( $xlinks, 0, 1 ) ) {
705 $element->parentNode->removeChild( $element ); // phpcs:ignore -- php DomNode
706 }
707 }
708
709 /**
710 * Strip Doctype
711 *
712 * @since 3.16.0
713 * @access private
714 */
715 private function strip_doctype() {
716 foreach ( $this->svg_dom->childNodes as $child ) {
717 if ( XML_DOCUMENT_TYPE_NODE === $child->nodeType ) { // phpcs:ignore -- php DomDocument
718 $child->parentNode->removeChild( $child ); // phpcs:ignore -- php DomDocument
719 }
720 }
721 }
722
723 /**
724 * Sanitize Elements
725 *
726 * @since 3.16.0
727 * @access private
728 */
729 private function sanitize_elements() {
730 $elements = $this->svg_dom->getElementsByTagName( '*' );
731 // loop through all elements
732 // we do this backwards so we don't skip anything if we delete a node
733 // see comments at: http://php.net/manual/en/class.domnamednodemap.php
734 for ( $index = $elements->length - 1; $index >= 0; $index-- ) {
735 /**
736 * @var \DOMElement $current_element
737 */
738 $current_element = $elements->item( $index );
739 // If the tag isn't in the whitelist, remove it and continue with next iteration
740 if ( ! $this->is_allowed_tag( $current_element ) ) {
741 continue;
742 }
743
744 // validate element attributes
745 $this->validate_allowed_attributes( $current_element );
746
747 $this->strip_xlinks( $current_element );
748
749 if ( 'use' === strtolower( $current_element->tagName ) ) { // phpcs:ignore -- php DomDocument
750 $this->validate_use_tag( $current_element );
751 }
752 }
753 }
754
755 /**
756 * Strip PHP Tags
757 *
758 * @since 3.16.0
759 * @access private
760 *
761 * @param string $content
762 * @return string
763 */
764 private function strip_php_tags( $content ) {
765 $content = preg_replace( '/<\?(=|php)(.+?)\?>/i', '', $content );
766 // Remove XML, ASP, etc.
767 $content = preg_replace( '/<\?(.*)\?>/Us', '', $content );
768 $content = preg_replace( '/<\%(.*)\%>/Us', '', $content );
769
770 if ( ( false !== strpos( $content, '<?' ) ) || ( false !== strpos( $content, '<%' ) ) ) {
771 return '';
772 }
773 return $content;
774 }
775
776 /**
777 * Strip Comments
778 *
779 * @since 3.16.0
780 * @access private
781 *
782 * @param string $content
783 * @return string
784 */
785 private function strip_comments( $content ) {
786 // Remove comments.
787 $content = preg_replace( '/<!--(.*)-->/Us', '', $content );
788 $content = preg_replace( '/\/\*(.*)\*\//Us', '', $content );
789 if ( ( false !== strpos( $content, '<!--' ) ) || ( false !== strpos( $content, '/*' ) ) ) {
790 return '';
791 }
792 return $content;
793 }
794
795 /**
796 * Strip Line Breaks
797 *
798 * @since 3.16.0
799 * @access private
800 *
801 * @param string $content
802 * @return string
803 */
804 private function strip_line_breaks( $content ) {
805 // Remove line breaks.
806 return preg_replace( '/\r|\n/', '', $content );
807 }
808 }
809