PluginProbe
Elementor Website Builder – more than just a page builder / 3.28.0-dev1
Elementor Website Builder – more than just a page builder v3.28.0-dev1
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.28.0-dev1, 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 *
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 'class',
268 'clip-path',
269 'clip-rule',
270 'fill',
271 'fill-opacity',
272 'fill-rule',
273 'filter',
274 'id',
275 'mask',
276 'opacity',
277 'stroke',
278 'stroke-dasharray',
279 'stroke-dashoffset',
280 'stroke-linecap',
281 'stroke-linejoin',
282 'stroke-miterlimit',
283 'stroke-opacity',
284 'stroke-width',
285 'style',
286 'systemlanguage',
287 'transform',
288 'href',
289 'xlink:href',
290 'xlink:title',
291 'cx',
292 'cy',
293 'r',
294 'requiredfeatures',
295 'clippathunits',
296 'type',
297 'rx',
298 'ry',
299 'color-interpolation-filters',
300 'stddeviation',
301 'filterres',
302 'filterunits',
303 'height',
304 'primitiveunits',
305 'width',
306 'x',
307 'y',
308 'font-size',
309 'display',
310 'font-family',
311 'font-style',
312 'font-weight',
313 'text-anchor',
314 'marker-end',
315 'marker-mid',
316 'marker-start',
317 'x1',
318 'x2',
319 'y1',
320 'y2',
321 'gradienttransform',
322 'gradientunits',
323 'spreadmethod',
324 'markerheight',
325 'markerunits',
326 'markerwidth',
327 'orient',
328 'preserveaspectratio',
329 'refx',
330 'refy',
331 'viewbox',
332 'maskcontentunits',
333 'maskunits',
334 'd',
335 'patterncontentunits',
336 'patterntransform',
337 'patternunits',
338 'points',
339 'fx',
340 'fy',
341 'offset',
342 'stop-color',
343 'stop-opacity',
344 'xmlns',
345 'xmlns:se',
346 'xmlns:xlink',
347 'xml:space',
348 'method',
349 'spacing',
350 'startoffset',
351 'dx',
352 'dy',
353 'rotate',
354 'textlength',
355 ];
356
357 /**
358 * Allowed attributes in SVG file.
359 *
360 * Filters the list of allowed attributes in SVG files.
361 *
362 * Since SVG files can run JS code that may inject malicious code, all attributes
363 * are removed except the allowed attributes.
364 *
365 * This hook can be used to manage allowed SVG attributes. To either add new
366 * attributes or delete existing attributes. To strengthen or weaken site security.
367 *
368 * @param array $allowed_attributes A list of allowed attributes.
369 */
370 $allowed_attributes = apply_filters( 'elementor/files/svg/allowed_attributes', $allowed_attributes );
371
372 return $allowed_attributes;
373 }
374
375 /**
376 * Get Allowed Elements
377 *
378 * Returns an array of allowed element tags to be in SVG files.
379 *
380 * @since 3.16.0
381 * @access private
382 *
383 * @return array
384 */
385 private function get_allowed_elements() {
386 $allowed_elements = [
387 'a',
388 'circle',
389 'clippath',
390 'defs',
391 'style',
392 'desc',
393 'ellipse',
394 'fegaussianblur',
395 'filter',
396 'foreignobject',
397 'g',
398 'image',
399 'line',
400 'lineargradient',
401 'marker',
402 'mask',
403 'metadata',
404 'path',
405 'pattern',
406 'polygon',
407 'polyline',
408 'radialgradient',
409 'rect',
410 'stop',
411 'svg',
412 'switch',
413 'symbol',
414 'text',
415 'textpath',
416 'title',
417 'tspan',
418 'use',
419 ];
420
421 /**
422 * Allowed elements in SVG file.
423 *
424 * Filters the list of allowed elements in SVG files.
425 *
426 * Since SVG files can run JS code that may inject malicious code, all elements
427 * are removed except the allowed elements.
428 *
429 * This hook can be used to manage SVG elements. To either add new elements or
430 * delete existing elements. To strengthen or weaken site security.
431 *
432 * @param array $allowed_elements A list of allowed elements.
433 */
434 $allowed_elements = apply_filters( 'elementor/files/svg/allowed_elements', $allowed_elements );
435
436 return $allowed_elements;
437 }
438
439 /**
440 * Validate Allowed Attributes
441 *
442 * @since 3.16.0
443 * @access private
444 *
445 * @param \DOMElement $element
446 */
447 private function validate_allowed_attributes( $element ) {
448 static $allowed_attributes = false;
449 if ( false === $allowed_attributes ) {
450 $allowed_attributes = $this->get_allowed_attributes();
451 }
452
453 for ( $index = $element->attributes->length - 1; $index >= 0; $index-- ) {
454 // get attribute name
455 $attr_name = $element->attributes->item( $index )->name;
456 $attr_name_lowercase = strtolower( $attr_name );
457 // Remove attribute if not in whitelist
458 if ( ! in_array( $attr_name_lowercase, $allowed_attributes ) && ! $this->is_a_attribute( $attr_name_lowercase, 'aria' ) && ! $this->is_a_attribute( $attr_name_lowercase, 'data' ) ) {
459 $element->removeAttribute( $attr_name );
460 continue;
461 }
462
463 $attr_value = $element->attributes->item( $index )->value;
464
465 // Remove attribute if it has a remote reference or js or data-URI/base64
466 if ( ! empty( $attr_value ) && ( $this->is_remote_value( $attr_value ) || $this->has_js_value( $attr_value ) ) ) {
467 $element->removeAttribute( $attr_name );
468 continue;
469 }
470 }
471 }
472
473 /**
474 * Strip xlinks
475 *
476 * @since 3.16.0
477 * @access private
478 *
479 * @param \DOMElement $element
480 */
481 private function strip_xlinks( $element ) {
482 $xlinks = $element->getAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
483
484 if ( ! $xlinks ) {
485 return;
486 }
487
488 if ( ! $this->is_safe_href( $xlinks ) ) {
489 $element->removeAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
490 }
491 }
492
493 /**
494 * @see https://github.com/darylldoyle/svg-sanitizer/blob/2321a914e/src/Sanitizer.php#L454
495 */
496 private function is_safe_href( $value ) {
497 // Allow empty values.
498 if ( empty( $value ) ) {
499 return true;
500 }
501
502 // Allow fragment identifiers.
503 if ( '#' === substr( $value, 0, 1 ) ) {
504 return true;
505 }
506
507 // Allow relative URIs.
508 if ( '/' === substr( $value, 0, 1 ) ) {
509 return true;
510 }
511
512 // Allow HTTPS domains.
513 if ( 'https://' === substr( $value, 0, 8 ) ) {
514 return true;
515 }
516
517 // Allow HTTP domains.
518 if ( 'http://' === substr( $value, 0, 7 ) ) {
519 return true;
520 }
521
522 // Allow known data URIs.
523 if ( in_array( substr( $value, 0, 14 ), [
524 'data:image/png', // PNG
525 'data:image/gif', // GIF
526 'data:image/jpg', // JPG
527 'data:image/jpe', // JPEG
528 'data:image/pjp', // PJPEG
529 ], true ) ) {
530 return true;
531 }
532
533 // Allow known short data URIs.
534 if ( in_array( substr( $value, 0, 12 ), [
535 'data:img/png', // PNG
536 'data:img/gif', // GIF
537 'data:img/jpg', // JPG
538 'data:img/jpe', // JPEG
539 'data:img/pjp', // PJPEG
540 ], true ) ) {
541 return true;
542 }
543
544 return false;
545 }
546
547 /**
548 * Validate Use Tag
549 *
550 * @since 3.16.0
551 * @access private
552 *
553 * @param $element
554 */
555 private function validate_use_tag( $element ) {
556 $xlinks = $element->getAttributeNS( 'http://www.w3.org/1999/xlink', 'href' );
557 if ( $xlinks && '#' !== substr( $xlinks, 0, 1 ) ) {
558 $element->parentNode->removeChild( $element ); // phpcs:ignore -- php DomNode
559 }
560 }
561
562 /**
563 * Strip Doctype
564 *
565 * @since 3.16.0
566 * @access private
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