This blog post is about the Porto Theme's plugin vulnerability. If you're a Porto Theme user, please update the plugin to at least version 2.12.1.
✌️ Our users are protected from this vulnerability. Are yours?
Automatically mitigate vulnerabilities in real-time without changing code.
See pricingIdentify vulnerabilities in your plugins and get recommendations for fixes.
Request auditProtect your users, improve server health and earn additional revenue.
Patchstack for hostsAbout the Porto Theme's Plugin
The plugin Porto Theme - Functionality (premium version) is a required plugin for the Porto theme. The theme itself is estimated to have more than 95,000 currently active installations. The Porto theme is known as the more popular premium multipurpose & WooCommerce Theme. This plugin is developed by p-themes.

Porto theme is an ultimate business & WooCommerce WordPress theme that is suitable for any business and WooCommerce site. Porto provides plenty of elements and powerful features that can configure all we want. Porto provides ultimate WooCommerce features with exclusive skins, layouts, and features.
The security vulnerability
This plugin suffers from an unauthenticated SQL injection vulnerability. This vulnerability allows any unauthenticated user to perform SQL injection. The described vulnerability was fixed in version 2.12.1 and assigned CVE-2023-48738.
Check this vulnerability in the Patchstack vulnerability database.
Unauthenticated SQL Injection
The underlying vulnerable code exists in the bulk_delete_critical
function:
/**
* Bulk delete the critical CSS.
*
* @since 2.3.0
*/
public function bulk_delete_critical() {
if ( ! isset( $_GET['post'] ) ) {
$this->redirect_critical_wizard();
}
$page_ids = wp_unslash( $_GET['post'] );
foreach ( $page_ids as $key => $value ) {
if ( 'homepage' == $value ) {
unset( $page_ids[ $key ] );
update_option( 'homepage_critical', '' );
break;
}
}
// Delete critical css
global $wpdb;
$page_ids = sanitize_text_field( implode( ',', $page_ids ) );
$wpdb->query( $wpdb->prepare( 'UPDATE ' . $wpdb->postmeta . " SET meta_value = '' WHERE meta_id IN ($page_ids)" ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$this->redirect_critical_wizard();
}
This function handles the process of deletion of critical CSS, which is a feature that helps the user reduce the rendering time of the CSS file. The function can be called from the table_actions
function:
/**
* The Table Actions
*
* @since 2.3.0
*/
public function table_actions() {
$action = '';
if ( isset( $_REQUEST['action'] ) ) {
if ( -1 !== $_REQUEST['action'] && '-1' !== $_REQUEST['action'] ) {
$action = sanitize_text_field( wp_unslash( $_REQUEST['action'] ) );
}
}
if ( isset( $_REQUEST['action2'] ) ) {
if ( -1 !== $_REQUEST['action2'] && '-1' !== $_REQUEST['action2'] ) {
$action = sanitize_text_field( wp_unslash( $_REQUEST['action2'] ) );
}
}
if ( ! empty( $action ) ) {
if ( 'porto_bulk_delete_critical' == $action ) {
$this->bulk_delete_critical();
} elseif ( 'delete_css' == $action ) {
$this->delete_css();
}
return false;
}
if ( ( isset( $_REQUEST['page'] ) && false !== strpos( $_REQUEST['page'], 'porto' ) ) && ( isset( $_REQUEST['action2'] ) || isset( $_REQUEST['action'] ) ) ) {
$referer = wp_get_referer();
if ( $referer ) {
wp_safe_redirect( $referer );
die;
}
}
return false;
}
The table_actions
is just a function that handles some process on the critical CSS feature. The function actually is called from the init
function:
/**
* The init function.
*
* @since 2.3.0
*/
public function init() {
global $porto_settings_optimize;
if ( defined( 'PORTO_VERSION' ) && ! empty( $porto_settings_optimize['critical_css'] ) && is_admin() ) {
add_action( 'admin_menu', array( $this, 'add_admin_menus' ) );
$this->table_actions();
}
}
This init
function is hooked to the WordPress built-in init
hook which is just a hook that runs after WordPress has finished loading but before any headers are sent. Given the condition, any unauthenticated user is able to execute the init
function and eventually call bulk_delete_critical
through the table_actions
function. This is possible because there is no permission and nonce validation on the table_actions
or bulk_delete_critical
functions.
Notice that in the bulk_delete_critical
function we can inject our SQL injection payload via the $page_ids
variable.
Note that this vulnerability can be reproduced with an Unauthenticated user with the condition that the Critical CSS feature is enabled on the plugin settings.
The patch
The patch includes an implementation of permission and nonce validation on the table_actions
function. For the vulnerable variable, force mapping the $page_ids
variable to an integer value should be enough to prevent SQL injection. The patch can be seen below:

Conclusion
Always secure the SQL process in plugins or themes with proper function and implementation. Both are important since the usage of proper functions to prevent SQL Injection like esc_sql()
and $wpdb->prepare
alone are not enough to prevent SQL Injection if the usage implementation is not proper.
For a value that is intended to only contain an integer value, we recommend to implement intval
to the variable so that it contains only valid integer values.
Timeline
1 August, 2023
We found the vulnerability and reached out to the vendor.
23 November, 2023
Published the vulnerabilities to the Patchstack vulnerability database (No reply from vendor).
25 November, 2023
Porto Theme - Functionality plugin version 2.12.1 released to patch the reported issue.
20 December, 2023
Security advisory article publicly released.
🤝 You can help us make the Internet a safer place
Streamline your disclosure process to fix vulnerabilities faster and comply with CRA.
Get started for freeProtect your users too! Improve server health and earn added revenue with proactive security.
Patchstack for hostsReport vulnerabilities to our gamified bug bounty program to earn monthly cash rewards.
Learn more