Critical SQL Injection Found in Porto Theme's Plugin

Published 20 December 2023
Rafie Muhammad
Security Researcher at Patchstack
Table of Contents

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.

You can sign up for the Patchstack Community plan to be notified about vulnerabilities as soon as they become disclosed.

For plugin developers, we have security audit services and Threat Intelligence Feed API for hosting companies.

About 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'] ) ) {

    $page_ids = wp_unslash( $_GET['post'] );

    foreach ( $page_ids as $key => $value ) {
        if ( 'homepage' == $value ) {
            unset( $page_ids[ $key ] );
            update_option( 'homepage_critical', '' );

    // 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 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 ) {
        } elseif ( 'delete_css' == $action ) {
        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 );
    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 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:


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.


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.

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.

The latest in Security Advisories

Looks like your browser is blocking our support chat widget. Turn off adblockers and reload the page.