WP Quick Post Duplicator <= 2.1 - Missing Authorization
Description
The WP Quick Post Duplicator plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in all versions up to, and including, 2.1. This makes it possible for authenticated attackers, with Contributor-level access and above, to perform an unauthorized action.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:NTechnical Details
<=2.1What Changed in the Fix
Changes introduced in v2.2
Source Code
WordPress.org SVN# Exploitation Research Plan - CVE-2026-24387 ## 1. Vulnerability Summary The **WP Quick Post Duplicator** plugin (<= 2.1) is vulnerable to **Missing Authorization**. While the plugin provides a feature to duplicate posts and pages, the processing function `apj_duplicate_post_as_a_draft()` fails to…
Show full research plan
Exploitation Research Plan - CVE-2026-24387
1. Vulnerability Summary
The WP Quick Post Duplicator plugin (<= 2.1) is vulnerable to Missing Authorization. While the plugin provides a feature to duplicate posts and pages, the processing function apj_duplicate_post_as_a_draft() fails to perform a granular capability check (e.g., current_user_can('edit_post', $post_id)). It only verifies a global edit_posts capability and a standard WordPress nonce.
Crucially, the nonce used for the duplication action is not bound to a specific Post ID. This allows an authenticated user with "Contributor" permissions (who has the edit_posts capability) to obtain a valid nonce from one of their own posts and reuse it to duplicate any other post or page on the site, including private or draft content created by administrators.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin.php - Action:
apj_duplicate_post_as_a_draft - Parameters:
action:apj_duplicate_post_as_a_draft(Required)post: The ID of the target post/page to duplicate (Required)_wpnonce: A valid nonce for the actionwqpd_clone_post_page_nonce(Required)
- Authentication: Authenticated, Contributor-level access (
edit_posts) or higher. - Preconditions: At least one post must be accessible/editable by the attacker to obtain the initial nonce.
3. Code Flow
- Entry Point: The function
apj_duplicate_post_as_a_draftis registered to theadmin_action_apj_duplicate_post_as_a_drafthook inwp-quick-post-duplicator.php. - Authorization Check: Line 102 checks
if(!current_user_can( 'edit_posts' )). This allows any Contributor+ to proceed. - Nonce Verification: Line 106 calls
check_admin_referer('wqpd_clone_post_page_nonce'). This verifies the_wpnonceparameter against the specific action string. - Input Processing: Lines 113-117 retrieve the
postID from$_GET['post'](or$_POST['post']). - Retrieval: Line 119 calls
get_post($post_id)without verifying if the current user has permission to view or edit this specific$post_id. - Sinks:
wp_insert_post($args)(Line 146): Creates a new post with the original content and the attacker as thepost_author.$wpdb->query($main_sql_query)(Line 169): Duplicates all post metadata.
4. Nonce Acquisition Strategy
The nonce is generated in the apj_duplicate_post_link function and displayed in the "Row Actions" (the hover menu) on the wp-admin/edit.php page.
- Login: Authenticate as a Contributor.
- Navigate: Go to
edit.php(Posts list). - Locate Link: Find a post owned by the Contributor.
- Extract Nonce: The "Duplicate This Item" link contains the nonce in the
_wpnoncequery parameter. - Execution Tool: Use
browser_evalto extract the link attribute from the DOM.- Selector:
a[href*="action=apj_duplicate_post_as_a_draft"] - JS Logic:
document.querySelector('a[href*="action=apj_duplicate_post_as_a_draft"]').href
- Selector:
5. Exploitation Strategy
- Preparation:
- Identify the ID of a "Secret" page/post (e.g., ID
1) that the Contributor cannot normally edit. - Login as a Contributor and obtain a valid nonce from the URL of the "Duplicate" link on one of the Contributor's own posts.
- Identify the ID of a "Secret" page/post (e.g., ID
- Execution: Send an authenticated
GETrequest toadmin.phptargeting the secret post. - HTTP Request (via
http_requesttool):GET /wp-admin/admin.php?action=apj_duplicate_post_as_a_draft&post=[TARGET_ID]&_wpnonce=[NONCE] HTTP/1.1 Cookie: [Contributor_Cookies] - Cleanup/Observation: The plugin will redirect to
edit.php. A new draft will now exist with the content of the target post, but owned by the Contributor.
6. Test Data Setup
- Administrator Actions:
- Create a Page titled "Admin Secret Page" with content "FLAG_SENSITIVE_DATA".
- Note the ID of this page (e.g.,
5).
- Contributor Actions:
- Create a Post titled "Contributor Post".
- Ensure the Contributor can see the "Duplicate This Item" link for their own post in
wp-admin/edit.php.
7. Expected Results
- The
http_requestshould return a302 Foundredirecting back toedit.php. - A new post of type
pageorpost(matching the target) will appear in the database. - The
post_authorof the new record will be the Contributor's ID. - The
post_contentwill be "FLAG_SENSITIVE_DATA".
8. Verification Steps
- WP-CLI Check: List the posts owned by the contributor:
wp post list --author=[CONTRIBUTOR_ID] --post_status=draft - Content Check: Verify the content of the newly created post:
wp post get [NEW_ID] --field=post_content - Meta Check: Verify metadata was also duplicated:
wp post meta list [NEW_ID]
9. Alternative Approaches
- POST Method: The plugin code checks
$_POST['post'](Line 115). If a WAF blocks long query strings, the exploit can be converted to a POST request toadmin.php, thoughcheck_admin_refererstill expects the nonce in the request (usually_wpnonce). - Direct Meta Inspection: If the "Secret" post has sensitive custom fields, check if they were successfully copied to the new draft via
wp post meta list.
Summary
The WP Quick Post Duplicator plugin (<= 2.1) is vulnerable to missing authorization because it fails to perform post-level capability checks and uses a generic nonce not bound to a specific post ID. This allows authenticated users with Contributor-level access to duplicate any post or page on the site, including private or restricted content, by reusing a nonce obtained from their own posts.
Vulnerable Code
// wp-quick-post-duplicator.php lines 68-107 in version 2.1 function apj_duplicate_post_as_a_draft() { global $wpdb; //check current user have a preveligies to edit post or page if(!current_user_can( 'edit_posts' )){ wp_die("you don't have a permission"); } // check user from right place check_admin_referer('wqpd_clone_post_page_nonce'); if (!(isset($_GET['post']) || isset($_POST['post']) || (isset($_REQUEST['action']) && 'apj_duplicate_post_as_a_draft' == $_REQUEST['action']))) { wp_die('No post to duplicate has been supplied!'); } $apjvalue1 = intval($_GET['post']); $apjvalue2 = intval($_POST['post']); $post_id = (isset($apjvalue1) ? $apjvalue1 : $apjvalue2); $post = get_post($post_id); --- // wp-quick-post-duplicator.php lines 42-55 in version 2.1 function apj_duplicate_post_link($actions, $post) { if (current_user_can('edit_posts')) { if ( $post->post_type == "post" || $post->post_type == "page" ) { $url = admin_url( 'admin.php' ); if ( current_user_can( 'edit_post', $post->ID ) ) { // adding a nonce in this link $copy_link = wp_nonce_url( add_query_arg( array( 'action' => 'apj_duplicate_post_as_a_draft','post'=>$post->ID ), $url ), 'wqpd_clone_post_page_nonce' ); $actions['duplicate'] = '<a href="' . $copy_link . '" rel="permalink">Duplicate This Item</a>'; } } } return $actions; }
Security Fix
@@ -5,7 +5,7 @@ * Donate link: https://paypal.me/arulprasadj?locale.x=en_GB * Requires at least: 3.0 * Tested up to: 6.8 -* Stable tag: 2.1 +* Stable tag: 2.2 * License: GPLv2 or later * License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -43,6 +43,10 @@ == Changelog == += 2.2 = +* Security fix: Prevent unauthorized users from duplicating private or restricted posts. +* Security: Added post-level capability checks and improved nonce validation. + = 2.1 = * Security improvements. @@ -4,17 +4,16 @@ * * * @package WP Quick Post Duplicator - * @version 2.1 + * @version 2.2 * @since 1.0 */ /** - /** * Plugin Name: WP Quick Post Duplicator * Plugin URI: https://wordpress.org/plugins/wp-quick-post-duplicator/ * Description: Copy or Duplicate any post types, including pages, taxonomies & custom fields with a single click. * Author: Arul Prasad J * Author URI: https://profiles.wordpress.org/arulprasadj/ - * Version: 2.1 + * Version: 2.2 * Text Domain: wp-quick-post-duplicator * Domain Path: /languages * License: GPLv2 or later (license.txt) @@ -36,110 +35,153 @@ * - apj_duplicate_post_as_a_draft() * Classes list: */ -function apj_duplicate_post_link($actions, $post) -{ - if (current_user_can('edit_posts')) - { - if ( $post->post_type == "post" || $post->post_type == "page" ) { - $url = admin_url( 'admin.php' ); - if ( current_user_can( 'edit_post', $post->ID ) ) { - // adding a nonce in this link - $copy_link = wp_nonce_url( add_query_arg( array( 'action' => 'apj_duplicate_post_as_a_draft','post'=>$post->ID ), $url ), 'wqpd_clone_post_page_nonce' ); +/** + * Add duplicate link to post/page row actions + */ +function apj_duplicate_post_link( $actions, $post ) { - $actions['duplicate'] = '<a href="' . $copy_link . '" rel="permalink">Duplicate This Item</a>'; - } - } + if ( ! current_user_can( 'edit_posts' ) ) { + return $actions; + } + + if ( $post->post_type !== 'post' && $post->post_type !== 'page' ) { + return $actions; } + + // Only show link if user can edit THIS post + if ( ! current_user_can( 'edit_post', $post->ID ) ) { + return $actions; + } + + $url = admin_url( 'admin.php' ); + + // 🔒 Nonce bound to post ID + $copy_link = wp_nonce_url( + add_query_arg( + array( + 'action' => 'apj_duplicate_post_as_a_draft', + 'post' => $post->ID, + ), + $url + ), + 'wqpd_clone_post_' . $post->ID + ); + + $actions['duplicate'] = '<a href="' . esc_url( $copy_link ) . '" rel="permalink">Duplicate This Item</a>'; + return $actions; } -$post_types = get_post_types('', 'names'); - -foreach ($post_types as $post_type) -{ - add_filter($post_type . '_row_actions', 'apj_duplicate_post_link', 10, 2); +/** + * Attach duplicate link to all post types + */ +$post_types = get_post_types( array(), 'names' ); +foreach ( $post_types as $post_type ) { + add_filter( $post_type . '_row_actions', 'apj_duplicate_post_link', 10, 2 ); } -function apj_duplicate_post_as_a_draft() -{ +/** + * Duplicate post handler (SECURE) + */ +function apj_duplicate_post_as_a_draft() { global $wpdb; - //check current user have a preveligies to edit post or page - if(!current_user_can( 'edit_posts' )){ - wp_die("you don't have a permission"); - } - // check user from right place - check_admin_referer('wqpd_clone_post_page_nonce'); - - if (!(isset($_GET['post']) || isset($_POST['post']) || (isset($_REQUEST['action']) && 'apj_duplicate_post_as_a_draft' == $_REQUEST['action']))) - { - wp_die('No post to duplicate has been supplied!'); - } - - $apjvalue1 = intval($_GET['post']); - - $apjvalue2 = intval($_POST['post']); - - $post_id = (isset($apjvalue1) ? $apjvalue1 : $apjvalue2); - - $post = get_post($post_id); - - $current_user = wp_get_current_user(); - $new_post_author = $current_user->ID; - - if (isset($post) && $post != null) - { - - $args = array( - 'comment_status' => $post->comment_status, - 'ping_status' => $post->ping_status, - 'post_author' => $new_post_author, - 'post_content' => $post->post_content, - 'post_excerpt' => $post->post_excerpt, - 'post_name' => $post->post_name, - 'post_parent' => $post->post_parent, - 'post_password' => $post->post_password, - 'post_status' => 'draft', - 'post_title' => $post->post_title, - 'post_type' => $post->post_type, - 'to_ping' => $post->to_ping, - 'menu_order' => $post->menu_order - ); - - $new_post_id = wp_insert_post($args); - - $taxonomies = get_object_taxonomies($post->post_type); - foreach ($taxonomies as $taxonomy) - { - $post_terms = wp_get_object_terms($post_id, $taxonomy, array( - 'fields' => 'slugs' - )); - wp_set_object_terms($new_post_id, $post_terms, $taxonomy, false); - } - $post_meta_infos = $wpdb->get_results("SELECT meta_key, meta_value FROM $wpdb->postmeta WHERE post_id=$post_id"); - if (count($post_meta_infos) != 0) - { - $main_sql_query = "INSERT INTO $wpdb->postmeta (post_id, meta_key, meta_value) "; - foreach ($post_meta_infos as $meta_info) - { - $meta_key = $meta_info->meta_key; - $meta_value = addslashes($meta_info->meta_value); - $sql_query_select[] = "SELECT $new_post_id, '$meta_key', '$meta_value'"; - } - $main_sql_query .= implode(" UNION ALL ", $sql_query_select); - $wpdb->query($main_sql_query); - } + // Must be logged in and able to edit posts + if ( ! current_user_can( 'edit_posts' ) ) { + wp_die( "You don't have permission." ); + } + + // Validate request + if ( + ! isset( $_GET['post'], $_GET['_wpnonce'] ) || + ! isset( $_REQUEST['action'] ) || + $_REQUEST['action'] !== 'apj_duplicate_post_as_a_draft' + ) { + wp_die( 'Invalid request.' ); + } + + $post_id = absint( $_GET['post'] ); + if ( ! $post_id ) { + wp_die( 'Invalid post ID.' ); + } + + // 🔒 Verify nonce bound to post + check_admin_referer( 'wqpd_clone_post_' . $post_id ); + + $post = get_post( $post_id ); + if ( ! $post ) { + wp_die( 'Post not found.' ); + } + + /** + * 🔒 CRITICAL FIX + * Ensure user is allowed to READ this post + */ + if ( ! current_user_can( 'read_post', $post_id ) ) { + wp_die( 'You are not allowed to duplicate this post.' ); + } - wp_redirect(admin_url('edit.php?post_type=' . $post->post_type)); - exit; + /** + * Extra protection for private posts + */ + if ( $post->post_status === 'private' && ! current_user_can( 'edit_post', $post_id ) ) { + wp_die( 'You are not allowed to duplicate private posts.' ); } - else - { - wp_die('Post creation failed, could not find original post: ' . $post_id); + + $current_user = wp_get_current_user(); + + $args = array( + 'comment_status' => $post->comment_status, + 'ping_status' => $post->ping_status, + 'post_author' => $current_user->ID, + 'post_content' => $post->post_content, + 'post_excerpt' => $post->post_excerpt, + 'post_name' => $post->post_name, + 'post_parent' => $post->post_parent, + 'post_password' => $post->post_password, + 'post_status' => 'draft', + 'post_title' => $post->post_title, + 'post_type' => $post->post_type, + 'to_ping' => $post->to_ping, + 'menu_order' => $post->menu_order, + ); + + $new_post_id = wp_insert_post( $args ); + + if ( is_wp_error( $new_post_id ) ) { + wp_die( 'Failed to create duplicate post.' ); + } + + // Copy taxonomies + $taxonomies = get_object_taxonomies( $post->post_type ); + foreach ( $taxonomies as $taxonomy ) { + $terms = wp_get_object_terms( $post_id, $taxonomy, array( 'fields' => 'slugs' ) ); + wp_set_object_terms( $new_post_id, $terms, $taxonomy, false ); } + + // Copy post meta (safe method) + $post_meta_infos = $wpdb->get_results( + $wpdb->prepare( + "SELECT meta_key, meta_value FROM $wpdb->postmeta WHERE post_id = %d", + $post_id + ) + ); + + if ( $post_meta_infos ) { + foreach ( $post_meta_infos as $meta_info ) { + add_post_meta( + $new_post_id, + $meta_info->meta_key, + maybe_unserialize( $meta_info->meta_value ) + ); + } + } + + wp_redirect( admin_url( 'edit.php?post_type=' . $post->post_type ) ); + exit; } -add_action('admin_action_apj_duplicate_post_as_a_draft', 'apj_duplicate_post_as_a_draft'); +add_action( 'admin_action_apj_duplicate_post_as_a_draft', 'apj_duplicate_post_as_a_draft' );
Exploit Outline
The exploit involves an authenticated attacker with Contributor-level permissions obtaining a valid nonce from a post they already have permission to duplicate. 1. Log in to the WordPress dashboard as a Contributor. 2. Access the Posts or Pages list (`edit.php`) and locate a post owned by the current user. 3. Extract the `_wpnonce` value from the "Duplicate This Item" link in the row actions (this nonce is associated with the `wqpd_clone_post_page_nonce` action string). 4. Identify the ID of a target post or page (e.g., an administrator's private page) that the Contributor cannot normally edit or see. 5. Send a GET request to `/wp-admin/admin.php` with the following parameters: `action=apj_duplicate_post_as_a_draft`, `post=[TARGET_ID]`, and `_wpnonce=[EXTRACTED_NONCE]`. 6. The plugin will create a new draft containing the content and metadata of the target post, but with the Contributor as the owner, allowing them to view and further edit the stolen content.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.