PluginProbe
Elementor Website Builder – more than just a page builder / 3.20.0-dev3
Elementor Website Builder – more than just a page builder v3.20.0-dev3
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
← All changes | core/files/uploads-manager.php +68 -105 4.1.13.20.0-dev3 View file →
@@ -6,11 +6,9 @@
6 6 use Elementor\Core\Files\File_Types\Base as File_Type_Base;
7 7 use Elementor\Core\Files\File_Types\Json;
8 8 use Elementor\Core\Files\File_Types\Svg;
9 9 use Elementor\Core\Files\File_Types\Zip;
10 -use Elementor\Core\Files\Fonts\Google_Font;
11 10 use Elementor\Core\Utils\Exceptions;
12 -use Elementor\Fonts;
13 11 use Elementor\User;
14 12
15 13 if ( ! defined( 'ABSPATH' ) ) {
16 14 exit; // Exit if accessed directly.
@@ -26,9 +24,8 @@
26 24 class Uploads_Manager extends Base_Object {
27 25
28 26 const UNFILTERED_FILE_UPLOADS_KEY = 'elementor_unfiltered_files_upload';
29 27 const INVALID_FILE_CONTENT = 'Invalid Content In File';
30 - const ELEMENTOR_UPLOAD_DIR = 'elementor';
31 28
32 29 /**
33 30 * @var File_Type_Base[]
34 31 */
@@ -46,8 +43,13 @@
46 43 */
47 44 private $temp_dir;
48 45
49 46 /**
47 + * @var array - Array of temp directories that were created during the upload process.
48 + */
49 + private $temp_unique_dirs = [];
50 +
51 + /**
50 52 * Register File Types
51 53 *
52 54 * To Add a new file type to Elementor, with its own handling logic, you need to add it to the $file_types array here.
53 55 *
@@ -75,9 +77,9 @@
75 77 * @since 3.3.0
76 78 * @access public
77 79 *
78 80 * @param string $file_path
79 - * @param array $allowed_file_types
81 + * @param array $allowed_file_types
80 82 * @return array|\WP_Error
81 83 */
82 84 public function extract_and_validate_zip( $file_path, $allowed_file_types = null ) {
83 85 $result = [];
@@ -91,8 +93,10 @@
91 93 if ( is_wp_error( $extracted ) ) {
92 94 return $extracted;
93 95 }
94 96
97 + $this->temp_unique_dirs[] = realpath( $extracted['extraction_directory'] );
98 +
95 99 // If there are no extracted file names, no files passed the extraction validation.
96 100 if ( empty( $extracted['files'] ) ) {
97 101 // TODO: Decide what to do if no files passed the extraction validation
98 102 return new \WP_Error( 'file_error', self::INVALID_FILE_CONTENT );
@@ -122,53 +126,39 @@
122 126 *
123 127 * @since 3.3.0
124 128 * @access public
125 129 *
126 - * @param array $data
127 - * @param array $allowed_file_extensions Optional. an array of file types that are allowed to pass validation for each
128 - * upload.
130 + * @param array $data {
131 + * @type string 'fileName'
132 + * @type string 'fileData'
133 + * }
134 + * @param array $allowed_file_extensions Optional. Array of file types, allowed to pass validation for each upload.
135 + *
129 136 * @return array|\WP_Error
130 137 */
131 138 public function handle_elementor_upload( array $data, $allowed_file_extensions = null ) {
139 + $normalized_data = [
140 + 'fileName' => basename( $data['fileName'] ?? '' ),
141 + 'fileData' => $data['fileData'] ?? null,
142 + ];
143 +
132 144 // If $file['fileData'] is set, it signals that the passed file is a Base64 string that needs to be decoded and
133 145 // saved to a temporary file.
134 - if ( isset( $data['fileData'] ) ) {
135 - $data = $this->save_base64_to_tmp_file( $data, $allowed_file_extensions );
146 + if ( isset( $normalized_data['fileData'] ) ) {
147 + $normalized_data = $this->save_base64_to_tmp_file( $normalized_data, $allowed_file_extensions );
136 148 }
137 149
138 - if ( is_wp_error( $data ) ) {
139 - return $data;
140 - }
150 + $validation_result = $this->validate_file( $normalized_data, $allowed_file_extensions );
141 151
142 - if ( ! isset( $data['fileData'] ) ) {
143 - if ( empty( $data['tmp_name'] ) ) {
144 - return new \WP_Error( 'file_error', esc_html__( 'Invalid temporary file path.', 'elementor' ) );
145 - }
146 -
147 - // Path validation only applies to direct calls (e.g. import_template) where
148 - // tmp_name originates from user input. When is_elementor_upload is true, this
149 - // method is used as a WordPress filter (wp_handle_sideload_prefilter) and
150 - // tmp_name is set by WordPress core.
151 - if ( ! $this->is_elementor_upload && ! $this->is_path_in_allowed_dir( $data['tmp_name'] ) ) {
152 - return new \WP_Error( 'file_error', esc_html__( 'Invalid temporary file path.', 'elementor' ) );
153 - }
154 - }
155 -
156 - $validation_result = $this->validate_file( $data, $allowed_file_extensions );
157 -
158 152 if ( is_wp_error( $validation_result ) ) {
159 - if ( ! empty( $data['tmp_name'] ) ) {
160 - $this->remove_file_or_dir( dirname( $data['tmp_name'] ) );
161 - }
162 -
163 153 return $validation_result;
164 154 }
165 155
166 - return $data;
156 + return $normalized_data;
167 157 }
168 158
169 159 /**
170 - * Is Unfiltered Uploads Enabled
160 + * are Unfiltered Uploads Enabled
171 161 *
172 162 * @since 3.5.0
173 163 * @access public
174 164 *
@@ -174,9 +164,9 @@
174 164 *
175 165 * @return bool
176 166 */
177 167 final public static function are_unfiltered_uploads_enabled() {
178 - $enabled = (bool) get_option( self::UNFILTERED_FILE_UPLOADS_KEY )
168 + $enabled = ! ! get_option( self::UNFILTERED_FILE_UPLOADS_KEY )
179 169 && Svg::file_sanitizer_can_run()
180 170 && User::is_current_user_can_upload_json();
181 171
182 172 /**
@@ -272,74 +262,55 @@
272 262 return $data;
273 263 }
274 264
275 265 /**
276 - * Check if path is within the allowed Elementor uploads directory.
266 + * Remove File Or Directory
277 267 *
278 - * Prevents path traversal and arbitrary directory deletion by ensuring the path
279 - * resolves under wp-content/uploads/elementor/ or under the configured temp dir
280 - * (elementor/files/temp-dir filter), so that cleanup works when temp dir is customized.
268 + * Directory is deleted recursively with all of its contents (subdirectories and files).
281 269 *
282 - * @since 3.35.4
283 - * @access private
270 + * @since 3.3.0
271 + * @access public
284 272 *
285 273 * @param string $path
286 - * @return bool
287 274 */
288 - private function is_path_in_allowed_dir( $path ) {
289 - if ( ! is_string( $path ) || '' === $path ) {
290 - return false;
275 + public function remove_file_or_dir( $path ) {
276 + if ( is_dir( $path ) ) {
277 + $this->remove_directory_with_files( $path );
278 + } elseif ( is_file( $path ) ) {
279 + unlink( $path );
291 280 }
292 -
293 - $real_path = realpath( $path );
294 -
295 - if ( false === $real_path ) {
296 - $real_path = realpath( dirname( $path ) );
297 - if ( false === $real_path ) {
298 - return false;
299 - }
300 - }
301 -
302 - $wp_upload_dir = wp_upload_dir();
303 - $elementor_base = realpath( $wp_upload_dir['basedir'] . DIRECTORY_SEPARATOR . self::ELEMENTOR_UPLOAD_DIR );
304 -
305 - if ( false !== $elementor_base ) {
306 - $allowed = $real_path === $elementor_base || 0 === strpos( $real_path, $elementor_base . DIRECTORY_SEPARATOR );
307 - if ( $allowed ) {
308 - return true;
309 - }
310 - }
311 -
312 - $temp_dir = realpath( $this->get_temp_dir() );
313 - if ( false !== $temp_dir ) {
314 - $temp_dir = rtrim( $temp_dir, DIRECTORY_SEPARATOR ) . DIRECTORY_SEPARATOR;
315 - return 0 === strpos( $real_path, $temp_dir );
316 - }
317 -
318 - return false;
319 281 }
320 282
321 283 /**
322 - * Remove File Or Directory
284 + * Safely removes a file or directory if it resides within the designated temporary folder.
323 285 *
324 - * Directory is deleted recursively with all of its contents (subdirectories and files).
325 - * Only paths under wp-content/uploads/elementor/ are allowed (security: prevents arbitrary directory deletion).
286 + * This method validates that the provided file path is located within the temporary directory
287 + * before proceeding with the removal. If the path is outside the temporary directory,
288 + * no action is taken to prevent unintended deletions.
326 289 *
327 - * @since 3.3.0
290 + * @since 3.19.0
328 291 * @access public
329 292 *
330 293 * @param string $path
294 + *
331 295 */
332 - public function remove_file_or_dir( $path ) {
333 - if ( ! $this->is_path_in_allowed_dir( $path ) ) {
296 + public function remove_temp_file_or_dir( $path ) {
297 + $realpath = realpath( $path );
298 + if ( false === $realpath ) {
334 299 return;
335 300 }
336 301
337 - if ( is_dir( $path ) ) {
338 - $this->remove_directory_with_files( $path );
339 - } elseif ( is_file( $path ) ) {
340 - unlink( $path );
302 + if ( is_uploaded_file( $path ) ) {
303 + $this->remove_file_or_dir( $path );
304 + return;
341 305 }
306 +
307 + foreach ( $this->temp_unique_dirs as $temp_dir ) {
308 + if ( strpos( $realpath, $temp_dir ) === 0 ) {
309 + $this->remove_file_or_dir( $path );
310 + break;
311 + }
312 + }
342 313 }
343 314
344 315 /**
345 316 * Create Temp File
@@ -391,9 +362,9 @@
391 362 public function get_temp_dir() {
392 363 if ( ! $this->temp_dir ) {
393 364 $wp_upload_dir = wp_upload_dir();
394 365
395 - $temp_dir = implode( DIRECTORY_SEPARATOR, [ $wp_upload_dir['basedir'], self::ELEMENTOR_UPLOAD_DIR, 'tmp' ] ) . DIRECTORY_SEPARATOR;
366 + $temp_dir = implode( DIRECTORY_SEPARATOR, [ $wp_upload_dir['basedir'], 'elementor', 'tmp' ] ) . DIRECTORY_SEPARATOR;
396 367
397 368 /**
398 369 * Temp File Path
399 370 *
@@ -427,8 +398,11 @@
427 398 $unique_dir_path = $this->get_temp_dir() . uniqid() . DIRECTORY_SEPARATOR;
428 399
429 400 wp_mkdir_p( $unique_dir_path );
430 401
402 + // Store uniqid and unique_dir_path pair
403 + $this->temp_unique_dirs[] = realpath( $unique_dir_path );
404 +
431 405 return $unique_dir_path;
432 406 }
433 407
434 408 /**
@@ -443,9 +417,8 @@
443 417 * @param Ajax $ajax
444 418 */
445 419 public function register_ajax_actions( Ajax $ajax ) {
446 420 $ajax->register_ajax_action( 'enable_unfiltered_files_upload', [ $this, 'enable_unfiltered_files_upload' ] );
447 - $ajax->register_ajax_action( 'enqueue_google_fonts', [ $this, 'ajax_enqueue_google_fonts' ] );
448 421 }
449 422
450 423 /**
451 424 * Set Unfiltered Files Upload
@@ -460,24 +433,8 @@
460 433
461 434 update_option( self::UNFILTERED_FILE_UPLOADS_KEY, 1 );
462 435 }
463 436
464 - public function ajax_enqueue_google_fonts( $data ): bool {
465 - if ( empty( $data['font_name'] ) ) {
466 - return false;
467 - }
468 -
469 - $font_type = Fonts::get_font_type( $data['font_name'] );
470 -
471 - if ( Fonts::GOOGLE !== $font_type ) {
472 - return false;
473 - }
474 -
475 - Google_Font::enqueue( $data['font_name'] );
476 -
477 - return true;
478 - }
479 -
480 437 /**
481 438 * Support Unfiltered File Uploads
482 439 *
483 440 * When uploading a file within Elementor, this method adds the registered
@@ -589,12 +546,8 @@
589 546 *
590 547 * @return array|\WP_Error
591 548 */
592 549 private function save_base64_to_tmp_file( $file, $allowed_file_extensions = null ) {
593 - if ( empty( $file['fileName'] ) || empty( $file['fileData'] ) ) {
594 - return new \WP_Error( 'file_error', self::INVALID_FILE_CONTENT );
595 - }
596 -
597 550 $file_extension = pathinfo( $file['fileName'], PATHINFO_EXTENSION );
598 551 $is_file_type_allowed = $this->is_file_type_allowed( $file_extension, $allowed_file_extensions );
599 552
600 553 if ( is_wp_error( $is_file_type_allowed ) ) {
@@ -632,8 +585,18 @@
632 585 * @param array $file_extensions Optional
633 586 * @return bool|\WP_Error
634 587 */
635 588 private function validate_file( array $file, $file_extensions = [] ) {
589 + $is_name_valid = empty( $file['name'] ) || basename( $file['name'] ) === $file['name'];
590 + $is_tmp_name_valid = empty( $file['tmp_name'] ) || realpath( $file['tmp_name'] ) !== false;
591 +
592 + if ( ( empty( $file['name'] ) && empty( $file['tmp_name'] ) ) || ! $is_name_valid || ! $is_tmp_name_valid ) {
593 + return new \WP_Error(
594 + Exceptions::FORBIDDEN,
595 + esc_html__( 'This file is not allowed for security reasons.', 'elementor' )
596 + );
597 + }
598 +
636 599 $uploaded_file_name = isset( $file['name'] ) ? $file['name'] : $file['tmp_name'];
637 600
638 601 $file_extension = pathinfo( $uploaded_file_name, PATHINFO_EXTENSION );
639 602
@@ -655,9 +618,9 @@
655 618 // If there is a File Type Handler for the uploaded file, it means it is a non-standard file type. In this case,
656 619 // we check if unfiltered file uploads are enabled or not before allowing it.
657 620 if ( ! self::are_unfiltered_uploads_enabled() ) {
658 621 $error = 'json' === $file_extension
659 - ? esc_html__( 'You do not have permission to upload JSON files.', 'elementor' )
622 + ? esc_html__( 'You don\'t have permission to upload JSON files. Contact the administrator.', 'elementor' )
660 623 : esc_html__( 'This file is not allowed for security reasons.', 'elementor' );
661 624 return new \WP_Error( Exceptions::FORBIDDEN, $error );
662 625 }
663 626