CVE-2026-39474

Post Duplicator <= 3.0.10 - Authenticated (Contributor+) PHP Object Injection

highDeserialization of Untrusted Data
7.5
CVSS Score
7.5
CVSS Score
high
Severity
3.0.11
Patched in
9d
Time to patch

Description

The Post Duplicator plugin for WordPress is vulnerable to PHP Object Injection in versions up to, and including, 3.0.10 via deserialization of untrusted input. This makes it possible for authenticated attackers, with contributor-level access and above, to inject a PHP Object. No known POP chain is present in the vulnerable software. If a POP chain is present via an additional plugin or theme installed on the target system, it could allow the attacker to delete arbitrary files, retrieve sensitive data, or execute code.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H
Attack Vector
Network
Attack Complexity
High
Privileges Required
Low
User Interaction
None
Scope
Unchanged
High
Confidentiality
High
Integrity
High
Availability

Technical Details

Affected versions<=3.0.10
PublishedApril 13, 2026
Last updatedApril 21, 2026
Affected pluginpost-duplicator

What Changed in the Fix

Changes introduced in v3.0.11

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploit Research Plan - CVE-2026-39474 ## 1. Vulnerability Summary The **Post Duplicator** plugin (<= 3.0.10) for WordPress is vulnerable to **PHP Object Injection** due to the unsafe use of `maybe_unserialize()` on post metadata within its REST API endpoints. The vulnerability exists in `inclu…

Show full research plan

Exploit Research Plan - CVE-2026-39474

1. Vulnerability Summary

The Post Duplicator plugin (<= 3.0.10) for WordPress is vulnerable to PHP Object Injection due to the unsafe use of maybe_unserialize() on post metadata within its REST API endpoints.

The vulnerability exists in includes/api.php within the get_post_data() function. This function iterates through all custom fields (metadata) associated with a post and calls maybe_unserialize() on any value that satisfies is_serialized(). An authenticated user with Contributor level access can create a post, attach a malicious serialized PHP object as metadata, and then trigger the deserialization by accessing the plugin's REST API endpoint for that post.

2. Attack Vector Analysis

  • Endpoint: GET /wp-json/post-duplicator/v1/post-data/(?P<id>\d+)
  • Vulnerable Parameter: The values of post metadata (retrieved via get_post_custom()) for the post specified by the id parameter.
  • Authentication: Authenticated (Contributor+). The user must have permission to duplicate the post, which typically includes any post they have authored.
  • Preconditions:
    1. The attacker must be able to add or edit post metadata (standard for Contributors on their own posts).
    2. The attacker must obtain a valid wp_rest nonce to interact with the WordPress REST API.

3. Code Flow

  1. Entry Point: The REST route post-duplicator/v1/post-data/(?P<id>\d+) is registered in includes/api.php (line 12) with the callback Mtphr\PostDuplicator\get_post_data.
  2. Permission Check: get_post_data_permissions (line 80) is called. It verifies the post exists and checks user_can_duplicate($post).
  3. Data Retrieval: get_post_data (line 104) is executed. It calls get_post_custom($post_id) (line 143) to retrieve all metadata for the post.
  4. Iteration: The code loops through each meta key and value (lines 146-156).
  5. Vulnerable Logic:
    • At line 163, it checks: if ( is_serialized( $value ) ).
    • If true, at line 165, it calls: $unserialized = maybe_unserialize( $value );.
  6. Sink: maybe_unserialize() (a wrapper for unserialize()) processes the untrusted string from the database, triggering the PHP Object Injection.
  7. Response: The code then checks is_object($unserialized) (line 169) and, if true, sets the type to 'object' and JSON-encodes the object for the response (line 171).

4. Nonce Acquisition Strategy

The REST API endpoint requires a wp_rest nonce for authenticated requests.

  1. Identify Script Loading: The plugin enqueues its duplication interface scripts on the post list page (edit.php) for supported post types.
  2. Access Admin Area: Log in as the Contributor and navigate to /wp-admin/edit.php.
  3. Extract Nonce: Use browser_eval to extract the WordPress standard REST nonce from the wpApiSettings object:
    • JS Variable: window.wpApiSettings.nonce
    • Alternative: If the plugin localizes its own nonce, it would likely be found in a variable like window.postDuplicatorData.nonce (though standard wp_rest is used by the register_rest_route default).

5. Exploitation Strategy

  1. Setup Post: Create a post as the Contributor and add a meta field containing a serialized object.
  2. Obtain Nonce: Extract the wp_rest nonce from the WordPress dashboard.
  3. Trigger Exploit: Send a GET request to the vulnerable REST endpoint.

Step-by-Step Execution:

  1. Login: Log in as a Contributor user.
  2. Inject Metadata: Use WP-CLI to add a serialized object to a post owned by the Contributor:
    wp post meta add <POST_ID> "exploit_field" 'O:8:"stdClass":0:{}'
    
  3. Capture Nonce:
    • Navigate to http://localhost:8080/wp-admin/edit.php.
    • Run browser_eval("window.wpApiSettings.nonce").
  4. Send Request:
    • Use http_request to call the endpoint:
      • URL: http://localhost:8080/wp-json/post-duplicator/v1/post-data/<POST_ID>
      • Headers: X-WP-Nonce: <NONCE_VALUE>
      • Method: GET
  5. Analyze Response: Observe the customMeta array in the JSON response.

6. Test Data Setup

  • User: Role contributor, username exploit_user.
  • Post: A standard post created by exploit_user.
  • Metadata:
    • Key: test_object_injection
    • Value: O:8:"stdClass":0:{} (A simple standard class object).

7. Expected Results

  • Success Indicator: The REST API response (200 OK) will contain a JSON object. Inside the customMeta array, an entry for test_object_injection will have:
    • "type": "object"
    • "isSerialized": true
    • "value": "{}"
  • Verification: The fact that the type returned is "object" confirms that maybe_unserialize() successfully transformed the string 'O:8:"stdClass":0:{}' into a PHP object. If it had failed or not run, the type would be "string".

8. Verification Steps

  1. Check Logs: Check the PHP error logs for any notices or errors if using a more complex (non-existent) class (e.g., O:15:"NonExistentClass":0:{}).
  2. Response Inspection: Verify the JSON response structure:
    {
      "taxonomies": [...],
      "customMeta": [
        {
          "key": "test_object_injection",
          "value": "{}",
          "type": "object",
          "isSerialized": true,
          "originalValue": "O:8:\"stdClass\":0:{}"
        }
      ]
    }
    
  3. Database Confirmation: Use wp post meta get <POST_ID> test_object_injection to confirm the raw string in the database matches the injected payload.

9. Alternative Approaches

If the post-data endpoint is inaccessible, the post-full-data endpoint (also in api.php) follows a similar logic and can be used as a fallback:

  • Endpoint: GET /wp-json/post-duplicator/v1/post-full-data/(?P<id>\d+)
  • Logic: Calls get_post_full_data, which likely also processes custom meta to provide a full preview of the post being duplicated.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Post Duplicator plugin for WordPress is vulnerable to PHP Object Injection in versions up to 3.0.10. This occurs because the plugin's REST API endpoints unsafeley call `maybe_unserialize()` on post metadata, allowing authenticated attackers with Contributor-level access to trigger deserialization of malicious payloads stored in custom fields.

Vulnerable Code

// includes/api.php lines 143-176
$custom_fields = get_post_custom( $post_id );
$excluded_meta_keys = get_excluded_meta_keys();

foreach ( $custom_fields as $key => $values ) {
  // ...
  foreach ( $values as $value ) {
    // Detect data type
    $type = 'string';
    $is_serialized = false;
    $original_value = $value;
    
    // Check if serialized
    if ( is_serialized( $value ) ) {
      $is_serialized = true;
      $unserialized = maybe_unserialize( $value );
      if ( is_array( $unserialized ) ) {
        $type = 'array';
        $value = wp_json_encode( $unserialized, JSON_PRETTY_PRINT );
      } elseif ( is_object( $unserialized ) ) {
        $type = 'object';
        $value = wp_json_encode( $unserialized, JSON_PRETTY_PRINT );
      } else {
        $type = 'string';
      }
    }
// ...

Security Fix

--- includes/api.php
+++ includes/api.php
@@ -162,15 +162,7 @@
       // Check if serialized
       if ( is_serialized( $value ) ) {
         $is_serialized = true;
-        $unserialized = maybe_unserialize( $value );
-        if ( is_array( $unserialized ) ) {
-          $type = 'array';
-          $value = wp_json_encode( $unserialized, JSON_PRETTY_PRINT );
-        } elseif ( is_object( $unserialized ) ) {
-          $type = 'object';
-          $value = wp_json_encode( $unserialized, JSON_PRETTY_PRINT );
-        } else {
-          $type = 'string';
-        }
+        $type = 'serialized';
+        $value = $value;
       } elseif ( is_numeric( $value ) ) {

Exploit Outline

1. An attacker logs into WordPress with at least Contributor-level permissions. 2. The attacker creates a new post or selects one they already own. 3. Using the standard WordPress interface or WP-CLI, the attacker adds a custom meta field (metadata) to the post containing a malicious serialized PHP object. 4. The attacker obtains a valid REST API nonce (standard `wp_rest` nonce) from the WordPress admin dashboard (e.g., from the `wpApiSettings` JavaScript object). 5. The attacker sends a GET request to the REST API endpoint `/wp-json/post-duplicator/v1/post-data/<POST_ID>` (or `/wp-json/post-duplicator/v1/post-full-data/<POST_ID>`), passing the nonce in the `X-WP-Nonce` header. 6. The plugin's `get_post_data` function retrieves the post's metadata and calls `maybe_unserialize()` on the malicious payload, triggering the PHP Object Injection.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.