Fancy Product Designer
Unauthenticated Arbitrary File Upload
Fancy Product Designer
Unauthenticated SQL Injection
This blog post is about Fancy Product Designer plugin vulnerabilities. If you’re a Fancy Product Designer user, please delete or deactivate the plugin until the patch is released by the vendor.
If you are a Patchstack customer, you are protected from this vulnerability already, and no further action is required from you.
For plugin developers, we have security audit services and Enterprise API for hosting companies.
About the Fancy Product Designer Plugin
The plugin Fancy Product Designer (premium version), which has over 20,000 sales, is one of the more popular premium plugins specifically related to customizing and designing all kinds of products from WooCommerce. This plugin is developed by Radykal.
This plugin will enable users to design and customize any product. Limited only by the user’s imagination it gives users absolute freedom in deciding which products and which parts of the product can be customized.
The security vulnerability
This plugin suffers from Unauthenticated Arbitrary File Upload, where users can upload arbitrary files including PHP files to the server, resulting in a Remote Code Execution (RCE).
The second vulnerability is Unauthenticated SQL Injection which allows any users to execute arbitrary SQL queries in the database of the WordPress site.
The described vulnerabilities are still unpatched in the latest version known (6.4.3) and assigned CVE-2024-51919 and CVE-2024-51818 respectively.
Unauthenticated Arbitrary File Upload
The underlying vulnerable code exists in the save_remote_file and fpd_admin_copy_file functions:
public static function save_remote_file( $remote_file_url ) {
$unique_dir = time().bin2hex(random_bytes(16));
$temp_dir = FPD_ORDER_DIR . 'print_ready_files/' . $unique_dir;
mkdir($temp_dir);
$local_file_path = $temp_dir;
$filename = fpd_admin_copy_file(
$remote_file_url,
$local_file_path
);
return $filename ? $unique_dir . '/' . $filename : null;
}
function fpd_admin_copy_file( $file_url, $destination_dir ) {
if( empty( $file_url ) ) return false;
if( !file_exists($destination_dir) )
wp_mkdir_p( $destination_dir );
$filename = basename( $file_url );
if( function_exists('copy') ) {
return copy( $file_url, $destination_dir . '/' . $filename ) ? $filename : false;
}
else {
$content = file_get_contents( $file_url );
$fp = fopen( $destination_dir . '/' . $filename, 'w' );
$bytes = fwrite( $fp, $content );
fclose( $fp );
return $bytes !== false ? $filename : false;
}
}
The save_remote_file function itself will just accept a remote URL value via $remote_file_url and it will call the fpd_admin_copy_file function to copy or save the files. If we look closely at the fpd_admin_copy_file function, it will either save the file with the filename set to the basename of $file_url using copy or write function. Since there is no proper check on those two functions, if there are any codes that utilize those functions without additional file checks, then we can achieve arbitrary file upload.
We found that the save_remote_file function can be called from the webhook_create_pr_file function:
public function webhook_create_pr_file( $request ) {
$response = array();
if( $request->get_param('print_job_id') && $request->get_param('file_url') ) {
$print_job = new FPD_Print_Job( $request->get_param('print_job_id'), true );
$print_job_details = $print_job->get_details();
//check if print job exists
if( $print_job_details ) {
$remote_file_url = $request->get_param('file_url');
$response['file_url'] = $remote_file_url;
$local_file = FPD_Pro_Export::save_remote_file( $remote_file_url );
------------------ CUT HERE ------------------
The function itself is a handler to a custom REST API endpoint registered on:
public function register_routes() {
register_rest_route( FPD_Pro_Export::ROUTE_NAMESPACE, '/print_job/(?P<id>.+)', array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( &$this, 'webhook_create_pr_file'),
'args' => array(
'id' => array(
'required' => true,
'validate_callback' => function($param, $request, $key) {
return is_string($param);
}
),
),
'permission_callback' => function () {
return true;
}
) );
}
First, the registered endpoint doesn’t have a specific permission check on the permission_callback parameter, resulting in any unauthenticated users being able to access the endpoint. Then, on the webhook_create_pr_file function itself, it will call the save_remote_file function with $remote_file_url supplied as the input parameter which can be fully controlled by the user. With this condition, users can just supply a remote PHP file that the users control and upload the files to the targeted WP server.
Note that this issue requires the AI feature (Genius) to be enabled on the WP site.
Unauthenticated SQL Injection
The underlying vulnerable code exists on the get_products_sql_attrs function:
public function get_products_sql_attrs( $attrs ) {
$where = isset( $attrs['where'] ) ? $attrs['where'] : null;
if( self::user_is_vendor() ) {
$user_ids = array(get_current_user_id());
//add fpd products from user
$fpd_products_user_id = fpd_get_option( 'fpd_wc_dokan_user_global_products' );
//skip if no use is set or on product builder
if( $fpd_products_user_id !== 'none' && !(isset( $_GET['page'] ) && $_GET['page'] === 'fpd_product_builder') )
array_push( $user_ids, $fpd_products_user_id );
$user_ids = join( ",", $user_ids );
$where = empty($where) ? "user_id IN ($user_ids)" : $where." AND user_id IN ($user_ids)";
}
//manage products filter
if( isset($_POST['fpd_filter_users_select']) && $_POST['fpd_filter_users_select'] != "-1" ) {
$where = "user_id=".strip_tags( $_POST['fpd_filter_users_select'] );
}
$attrs['where'] = $where;
return $attrs;
}
The function itself is registered as a handler to the fpd_get_products_sql_attrs filter:
class FPD_WC_Dokan {
public function __construct() {
add_action( 'admin_init', array( &$this, 'init_admin' ) );
add_filter( 'admin_body_class', array(&$this, 'add_body_classes') );
add_action( 'admin_menu', array( &$this, 'remove_menu_pages' ), 100 );
add_action( 'admin_notices', array( &$this, 'display_admin_notices' ) );
//Dokan Dashboard
add_filter( 'woocommerce_admin_order_actions', array( &$this, 'dashboard_orders_actions' ), 20, 2 );
add_filter( 'dokan_get_dashboard_nav', array( &$this, 'dashboard_nav' ) );
//Settings
add_filter( 'fpd_woocommerce_settings', array( &$this, 'add_settings' ) );
add_filter( 'fpd_settings_blocks', array( &$this, 'add_settings_block' ) );
add_action( 'fpd_block_options_end', array(&$this, 'add_block_options') );
//API filters
add_filter( 'fpd_get_products_sql_attrs', array( &$this, 'get_products_sql_attrs' ) );
add_filter( 'fpd_get_categories_sql_attrs', array( &$this, 'get_categories_sql_attrs' ) );
}
The filter itself will be called from the get_products function:
public static function get_products( $attrs = array(), $type = 'catalog' ) {
global $wpdb;
$defaults = array(
'cols' => '*',
'where' => '',
'order_by' => '',
'limit' => null,
'offset' => null
);
$attrs = apply_filters( 'fpd_get_products_sql_attrs', $attrs );
extract( array_merge( $defaults, $attrs ) );
$products = array();
if( fpd_table_exists(FPD_PRODUCTS_TABLE) ) {
$where = empty($where) ? $wpdb->prepare( 'WHERE type="%s"', $type) : $wpdb->prepare( 'WHERE type="%s" AND ', $type ) . $where;
if( !preg_match('/^[a-zA-Z]+\\s(ASC|DESC)$/', $order_by) )
$order_by = '';
$order_by = empty($order_by) ? '' : 'ORDER BY '. $order_by;
$limit = empty($limit) ? '' : $wpdb->prepare( 'LIMIT %d', $limit );
$offset = empty($offset) ? '' : $wpdb->prepare( 'OFFSET %d', $offset );
$products = $wpdb->get_results(
"SELECT $cols FROM ".FPD_PRODUCTS_TABLE." $where $order_by $limit $offset"
);
}
return $products;
}
In short, the function can be triggered by unauthenticated users. If we look at the initial get_products_sql_attrs function, we notice that the $_POST[‘fpd_filter_users_select’] variable is being passed to the $where, for the user_id query with only using strip_tags to sanitize the value. The strip_tags function itself is not enough to prevent SQL Injection in this case, since the function literally only strips HTML, XML, and PHP tags. Additionally, the value is constructed without quotes on the SQL query.
The injected value then will be processed in the get_products function as the $where value will be directly constructed on the $wpdb->get_results() query execution.
The patch
The vulnerability is still unpatched in the latest version known (6.4.3) at the time of publishing this article. We will update the article if the vendor releases a patch.
Conclusion
For file process, always check every process of $_FILES
parameter in the plugin or theme code. Make sure to apply a check on the filename and extension before uploading the file. One of the recommendations to prevent arbitrary file uploads would be applying a whitelist on allowed file extensions.
For the SQL query process, always do a safe escape and format for the user’s input before performing a query. The best practice is always to use a prepared statement and also cast each of the used variables to its intended usage, for example, always cast a variable to an integer if the intended value of the variable should be an integer value.
Want to learn more about finding and fixing vulnerabilities?
Explore our Academy to master the art of finding and patching vulnerabilities within the WordPress ecosystem. Dive deep into detailed guides on various vulnerability types, from discovery tactics for researchers to robust fixes for developers. Join us and contribute to our growing knowledge base.
Timeline
Help us make the Internet a safer place
Making the WordPress ecosystem more secure is a team effort, and we believe that plugin developers and security researchers should work together.
- If you’re a plugin developer, join our mVDP program that makes it easier to report, manage and address vulnerabilities in your software.
- If you’re a security researcher, join Patchstack Alliance to report vulnerabilities & earn rewards.