PluginProbe
Elementor Website Builder – more than just a page builder / 3.24.3
Elementor Website Builder – more than just a page builder v3.24.3
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 3.24.3, at core/utils/svg/svg-sanitizer.php

662 lines 14.3 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 * @see http://www.gzip.org/zlib/rfc-gzip.html#member-format
124 *
125 * @since 3.16.0
126 * @access private
127 *
128 * @param $contents
129 *
130 * @return bool
131 */
132 private function is_encoded( $contents ) {
133 $needle = "\x1f\x8b\x08";
134 if ( function_exists( 'mb_strpos' ) ) {
135 return 0 === mb_strpos( $contents, $needle );
136 } else {
137 return 0 === strpos( $contents, $needle );
138 }
139 }
140
141 /**
142 * Encode SVG
143 *
144 * @since 3.16.0
145 * @access private
146 *
147 * @param $content
148 * @return string
149 */
150 private function encode_svg( $content ) {
151 return gzencode( $content );
152 }
153
154 /**
155 * Decode SVG
156 *
157 * @since 3.16.0
158 * @access private
159 *
160 * @param $content
161 *
162 * @return string
163 */
164 private function decode_svg( $content ) {
165 return gzdecode( $content );
166 }
167
168 /**
169 * Is Allowed Tag
170 *
171 * @since 3.16.0
172 * @access private
173 *
174 * @param $element
175 * @return bool
176 */
177 private function is_allowed_tag( $element ) {
178 static $allowed_tags = false;
179 if ( false === $allowed_tags ) {
180 $allowed_tags = $this->get_allowed_elements();
181 }
182
183 $tag_name = $element->tagName; // phpcs:ignore -- php DomDocument
184
185 if ( ! in_array( strtolower( $tag_name ), $allowed_tags ) ) {
186 $this->remove_element( $element );
187 return false;
188 }
189
190 return true;
191 }
192
193 /**
194 * Remove Element
195 *
196 * Removes the passed element from its DomDocument tree
197 *
198 * @since 3.16.0
199 * @access private
200 *
201 * @param $element
202 */
203 private function remove_element( $element ) {
204 $element->parentNode->removeChild( $element ); // phpcs:ignore -- php DomDocument
205 }
206
207 /**
208 * Is It An Attribute
209 *
210 * @since 3.16.0
211 * @access private
212 *
213 * @param $name
214 * @param $check
215 * @return bool
216 */
217 private function is_a_attribute( $name, $check ) {
218 return 0 === strpos( $name, $check . '-' );
219 }
220
221 /**
222 * Is Remote Value
223 *
224 * @since 3.16.0
225 * @access private
226 *
227 * @param $value
228 * @return string
229 */
230 private function is_remote_value( $value ) {
231 $value = trim( preg_replace( '/[^ -~]/xu', '', $value ) );
232 $wrapped_in_url = preg_match( '~^url\(\s*[\'"]\s*(.*)\s*[\'"]\s*\)$~xi', $value, $match );
233 if ( ! $wrapped_in_url ) {
234 return false;
235 }
236
237 $value = trim( $match[1], '\'"' );
238 return preg_match( '~^((https?|ftp|file):)?//~xi', $value );
239 }
240
241 /**
242 * Has JS Value
243 *
244 * @since 3.16.0
245 * @access private
246 *
247 * @param $value
248 * @return false|int
249 */
250 private function has_js_value( $value ) {
251 return preg_match( '/base64|data|(?:java)?script|alert\(|window\.|document/i', $value );
252 }
253
254 /**
255 * Get Allowed Attributes
256 *
257 * Returns an array of allowed tag attributes in SVG files.
258 *
259 * @since 3.16.0
260 * @access private
261 *
262 * @return array
263 */
264 private function get_allowed_attributes() {
265 $allowed_attributes = [
266 'class',
267 'clip-path',
268 'clip-rule',
269 'fill',
270 'fill-opacity',
271 'fill-rule',
272 'filter',
273 'id',
274 'mask',
275 'opacity',
276 'stroke',
277 'stroke-dasharray',
278 'stroke-dashoffset',
279 'stroke-linecap',
280 'stroke-linejoin',
281 'stroke-miterlimit',
282 'stroke-opacity',
283 'stroke-width',
284 'style',
285 'systemlanguage',
286 'transform',
287 'href',
288 'xlink:href',
289 'xlink:title',
290 'cx',
291 'cy',
292 'r',
293 'requiredfeatures',
294 'clippathunits',
295 'type',
296 'rx',
297 'ry',
298 'color-interpolation-filters',
299 'stddeviation',
300 'filterres',
301 'filterunits',
302 'height',
303 'primitiveunits',
304 'width',
305 'x',
306 'y',
307 'font-size',
308 'display',
309 'font-family',
310 'font-style',
311 'font-weight',
312 'text-anchor',
313 'marker-end',
314 'marker-mid',
315 'marker-start',
316 'x1',
317 'x2',
318 'y1',
319 'y2',
320 'gradienttransform',
321 'gradientunits',
322 'spreadmethod',
323 'markerheight',
324 'markerunits',
325 'markerwidth',
326 'orient',
327 'preserveaspectratio',
328 'refx',
329 'refy',
330 'viewbox',
331 'maskcontentunits',
332 'maskunits',
333 'd',
334 'patterncontentunits',
335 'patterntransform',
336 'patternunits',
337 'points',
338 'fx',
339 'fy',
340 'offset',
341 'stop-color',
342 'stop-opacity',
343 'xmlns',
344 'xmlns:se',
345 'xmlns:xlink',
346 'xml:space',
347 'method',
348 'spacing',
349 'startoffset',
350 'dx',
351 'dy',
352 'rotate',
353 'textlength',
354 ];
355
356 /**
357 * Allowed attributes in SVG file.
358 *
359 * Filters the list of allowed attributes in SVG files.
360 *
361 * Since SVG files can run JS code that may inject malicious code, all attributes
362 * are removed except the allowed attributes.
363 *
364 * This hook can be used to manage allowed SVG attributes. To either add new
365 * attributes or delete existing attributes. To strengthen or weaken site security.
366 *
367 * @param array $allowed_attributes A list of allowed attributes.
368 */
369 $allowed_attributes = apply_filters( 'elementor/files/svg/allowed_attributes', $allowed_attributes );
370
371 return $allowed_attributes;
372 }
373
374 /**
375 * Get Allowed Elements
376 *
377 * Returns an array of allowed element tags to be in SVG files.
378 *
379 * @since 3.16.0
380 * @access private
381 *
382 * @return array
383 */
384 private function get_allowed_elements() {
385 $allowed_elements = [
386 'a',
387 'circle',
388 'clippath',
389 'defs',
390 'style',
391 'desc',
392 'ellipse',
393 'fegaussianblur',
394 'filter',
395 'foreignobject',
396 'g',
397 'image',
398 'line',
399 'lineargradient',
400 'marker',
401 'mask',
402 'metadata',
403 'path',
404 'pattern',
405 'polygon',
406 'polyline',
407 'radialgradient',
408 'rect',
409 'stop',
410 'svg',
411 'switch',
412 'symbol',
413 'text',
414 'textpath',
415 'title',
416 'tspan',
417 'use',
418 ];
419
420 /**
421 * Allowed elements in SVG file.
422 *
423 * Filters the list of allowed elements in SVG files.
424 *
425 * Since SVG files can run JS code that may inject malicious code, all elements
426 * are removed except the allowed elements.
427 *
428 * This hook can be used to manage SVG elements. To either add new elements or
429 * delete existing elements. To strengthen or weaken site security.
430 *
431 * @param array $allowed_elements A list of allowed elements.
432 */
433 $allowed_elements = apply_filters( 'elementor/files/svg/allowed_elements', $allowed_elements );
434
435 return $allowed_elements;
436 }
437
438 /**
439 * Validate Allowed Attributes
440 *
441 * @since 3.16.0
442 * @access private
443 *
444 * @param \DOMElement $element
445 */
446 private function validate_allowed_attributes( $element ) {
447 static $allowed_attributes = false;
448 if ( false === $allowed_attributes ) {
449 $allowed_attributes = $this->get_allowed_attributes();
450 }
451
452 for ( $index = $element->attributes->length - 1; $index >= 0; $index-- ) {
453 // get attribute name
454 $attr_name = $element->attributes->item( $index )->name;
455 $attr_name_lowercase = strtolower( $attr_name );
456 // Remove attribute if not in whitelist
457 if ( ! in_array( $attr_name_lowercase, $allowed_attributes ) && ! $this->is_a_attribute( $attr_name_lowercase, 'aria' ) && ! $this->is_a_attribute( $attr_name_lowercase, 'data' ) ) {
458 $element->removeAttribute( $attr_name );
459 continue;
460 }
461
462 $attr_value = $element->attributes->item( $index )->value;
463
464 // Remove attribute if it has a remote reference or js or data-URI/base64
465 if ( ! empty( $attr_value ) && ( $this->is_remote_value( $attr_value ) || $this->has_js_value( $attr_value ) ) ) {
466 $element->removeAttribute( $attr_name );
467 continue;
468 }
469 }
470 }
471
472 /**
473 * Strip xlinks
474 *
475 * @since 3.16.0
476 * @access private
477 *
478 * @param \DOMElement $element
479 */
480 private function strip_xlinks( $element ) {
481 $xlinks = $element->getAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
482
483 if ( ! $xlinks ) {
484 return;
485 }
486
487 if ( ! $this->is_safe_href( $xlinks ) ) {
488 $element->removeAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
489 }
490 }
491
492 /**
493 * @see https://github.com/darylldoyle/svg-sanitizer/blob/2321a914e/src/Sanitizer.php#L454
494 */
495 private function is_safe_href( $value ) {
496 // Allow empty values.
497 if ( empty( $value ) ) {
498 return true;
499 }
500
501 // Allow fragment identifiers.
502 if ( '#' === substr( $value, 0, 1 ) ) {
503 return true;
504 }
505
506 // Allow relative URIs.
507 if ( '/' === substr( $value, 0, 1 ) ) {
508 return true;
509 }
510
511 // Allow HTTPS domains.
512 if ( 'https://' === substr( $value, 0, 8 ) ) {
513 return true;
514 }
515
516 // Allow HTTP domains.
517 if ( 'http://' === substr( $value, 0, 7 ) ) {
518 return true;
519 }
520
521 // Allow known data URIs.
522 if ( in_array( substr( $value, 0, 14 ), [
523 'data:image/png', // PNG
524 'data:image/gif', // GIF
525 'data:image/jpg', // JPG
526 'data:image/jpe', // JPEG
527 'data:image/pjp', // PJPEG
528 ], true ) ) {
529 return true;
530 }
531
532 // Allow known short data URIs.
533 if ( in_array( substr( $value, 0, 12 ), [
534 'data:img/png', // PNG
535 'data:img/gif', // GIF
536 'data:img/jpg', // JPG
537 'data:img/jpe', // JPEG
538 'data:img/pjp', // PJPEG
539 ], true ) ) {
540 return true;
541 }
542
543 return false;
544 }
545
546 /**
547 * Validate Use Tag
548 *
549 * @since 3.16.0
550 * @access private
551 *
552 * @param $element
553 */
554 private function validate_use_tag( $element ) {
555 $xlinks = $element->getAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
556 if ( $xlinks && '#' !== substr( $xlinks, 0, 1 ) ) {
557 $element->parentNode->removeChild( $element ); // phpcs:ignore -- php DomNode
558 }
559 }
560
561 /**
562 * Strip Doctype
563 *
564 * @since 3.16.0
565 * @access private
566 *
567 */
568 private function strip_doctype() {
569 foreach ( $this->svg_dom->childNodes as $child ) {
570 if ( XML_DOCUMENT_TYPE_NODE === $child->nodeType ) { // phpcs:ignore -- php DomDocument
571 $child->parentNode->removeChild( $child ); // phpcs:ignore -- php DomDocument
572 }
573 }
574 }
575
576 /**
577 * Sanitize Elements
578 *
579 * @since 3.16.0
580 * @access private
581 */
582 private function sanitize_elements() {
583 $elements = $this->svg_dom->getElementsByTagName( '*' );
584 // loop through all elements
585 // we do this backwards so we don't skip anything if we delete a node
586 // see comments at: http://php.net/manual/en/class.domnamednodemap.php
587 for ( $index = $elements->length - 1; $index >= 0; $index-- ) {
588 /**
589 * @var \DOMElement $current_element
590 */
591 $current_element = $elements->item( $index );
592 // If the tag isn't in the whitelist, remove it and continue with next iteration
593 if ( ! $this->is_allowed_tag( $current_element ) ) {
594 continue;
595 }
596
597 //validate element attributes
598 $this->validate_allowed_attributes( $current_element );
599
600 $this->strip_xlinks( $current_element );
601
602 if ( 'use' === strtolower( $current_element->tagName ) ) { // phpcs:ignore -- php DomDocument
603 $this->validate_use_tag( $current_element );
604 }
605 }
606 }
607
608 /**
609 * Strip PHP Tags
610 *
611 * @since 3.16.0
612 * @access private
613 *
614 * @param $string
615 * @return string
616 */
617 private function strip_php_tags( $string ) {
618 $string = preg_replace( '/<\?(=|php)(.+?)\?>/i', '', $string );
619 // Remove XML, ASP, etc.
620 $string = preg_replace( '/<\?(.*)\?>/Us', '', $string );
621 $string = preg_replace( '/<\%(.*)\%>/Us', '', $string );
622
623 if ( ( false !== strpos( $string, '<?' ) ) || ( false !== strpos( $string, '<%' ) ) ) {
624 return '';
625 }
626 return $string;
627 }
628
629 /**
630 * Strip Comments
631 *
632 * @since 3.16.0
633 * @access private
634 *
635 * @param $string
636 * @return string
637 */
638 private function strip_comments( $string ) {
639 // Remove comments.
640 $string = preg_replace( '/<!--(.*)-->/Us', '', $string );
641 $string = preg_replace( '/\/\*(.*)\*\//Us', '', $string );
642 if ( ( false !== strpos( $string, '<!--' ) ) || ( false !== strpos( $string, '/*' ) ) ) {
643 return '';
644 }
645 return $string;
646 }
647
648 /**
649 * Strip Line Breaks
650 *
651 * @since 3.16.0
652 * @access private
653 *
654 * @param $string
655 * @return string
656 */
657 private function strip_line_breaks( $string ) {
658 // Remove line breaks.
659 return preg_replace( '/\r|\n/', '', $string );
660 }
661 }
662