Yoast Duplicate Post <= 4.5 - Authenticated (Contributor+) Missing Authorization to Arbitrary Post Duplication and Overwrite
Description
The Yoast Duplicate Post plugin for WordPress is vulnerable to unauthorized modification of data due to a missing capability check on the clone_bulk_action_handler() and republish_request() functions in all versions up to, and including, 4.5. This makes it possible for authenticated attackers, with Contributor-level access and above, to duplicate any post on the site including private, draft, and trashed posts they shouldn't have access to. Additionally, attackers with Author-level access and above can use the Rewrite & Republish feature to overwrite any published post with their own content.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:NTechnical Details
<=4.5What Changed in the Fix
Changes introduced in v4.6
Source Code
WordPress.org SVN# Research Plan: CVE-2026-1217 - Yoast Duplicate Post Authorization Bypass ## 1. Vulnerability Summary The **Yoast Duplicate Post** plugin (<= 4.5) contains a missing authorization vulnerability in its post duplication and "Rewrite & Republish" features. While the plugin intends to restrict these c…
Show full research plan
Research Plan: CVE-2026-1217 - Yoast Duplicate Post Authorization Bypass
1. Vulnerability Summary
The Yoast Duplicate Post plugin (<= 4.5) contains a missing authorization vulnerability in its post duplication and "Rewrite & Republish" features. While the plugin intends to restrict these capabilities to specific roles (Editor and Administrator) by granting them a custom copy_posts capability, the functions clone_bulk_action_handler() and republish_request() fail to verify if the current user has sufficient permissions for the specific target post (or the capability in general).
This allows:
- Contributors+ to duplicate any post (including private, draft, or trashed posts of other users) into a new draft they control.
- Authors+ to use the "Rewrite & Republish" feature on any published post (even those owned by Administrators), allowing them to eventually overwrite the original content with their own.
2. Attack Vector Analysis
- Endpoints:
wp-admin/admin.php(Single post duplication/rewrite)wp-admin/edit.php(Bulk action duplication)
- Actions:
duplicate_post_save_as_new_draft(Single duplication)duplicate_post_rewrite(Initiate Rewrite & Republish)duplicate_post_clone(Bulk duplication)
- Parameters:
post: The Target ID of the post to duplicate or rewrite._wpnonce: Security nonce (Action-specific).
- Authentication: Authenticated user with at least Contributor (for duplication) or Author (for rewriting) roles.
- Preconditions: The attacker must know the ID of the target post they wish to duplicate/overwrite.
3. Code Flow
- Entry Point: A request is made to
wp-admin/admin.phpwithaction=duplicate_post_save_as_new_draftand a targetpostID. - Hook Registration: In
admin-functions.php, the plugin initializes viaduplicate_post_admin_init(). The handlers for duplication are registered (typically viaadmin_action_orload-edit.phphooks). - Vulnerable Function:
republish_request()(for rewriting) or the logic insideclone_bulk_action_handler()executes. - Authorization Failure: The functions perform a generic nonce check but fail to call
current_user_can( 'copy_posts' )orcurrent_user_can( 'edit_post', $post_id ). - Sink: The code proceeds to
duplicate_post_create_duplicate()(incommon-functions.php/ Utils) which performs the SQL operations to clone the post data into a new entry.
4. Nonce Acquisition Strategy
The plugin localizes nonces and pre-built links into the duplicatePost JavaScript object on post-related admin pages.
- Create a post: As the attacker (Contributor or Author), create a simple post of your own.
- Navigate to Edit Page: Navigate to the edit screen of your own post.
- Extract Nonce via Browser:
Usebrowser_evalto extract the links containing nonces from the localizedduplicatePostobject.- Variable:
window.duplicatePost - Keys:
newDraftLink(for duplication) orrewriteAndRepublishLink(for rewriting).
- Variable:
// Example Extraction
const newDraftUrl = window.duplicatePost?.newDraftLink;
// Result: ".../wp-admin/admin.php?action=duplicate_post_save_as_new_draft&post=MY_ID&_wpnonce=abc12345"
The nonces in this plugin for these specific actions are generally not ID-specific (they are action-specific), meaning a nonce generated for post=MY_ID will work when sent with post=TARGET_ID.
5. Exploitation Strategy
Attack A: Unauthorized Duplication (Contributor)
- Role: Contributor.
- Identify Target: Locate a Private or Draft post ID created by an Administrator (e.g., ID
10). - Get Nonce: Extract
_wpnoncefrom the Contributor's own post edit screen as described in Section 4. - Forged Request:
- URL:
http://vulnerable.test/wp-admin/admin.php?action=duplicate_post_save_as_new_draft&post=10&_wpnonce=[EXTRACTED_NONCE] - Method:
GET
- URL:
- Result: A new draft is created for the Contributor containing the content of the Admin's private post.
Attack B: Overwriting Admin Posts (Author)
- Role: Author.
- Identify Target: A published post ID created by an Administrator (e.g., ID
20). - Get Nonce: Extract
_wpnonceforduplicate_post_rewritefrom the Author's own post. - Forged Request (Initiation):
- URL:
http://vulnerable.test/wp-admin/admin.php?action=duplicate_post_rewrite&post=20&_wpnonce=[EXTRACTED_NONCE] - Method:
GET
- URL:
- Execution: This creates a "Rewrite & Republish" copy (status:
dp-rewrite-republish). The Author is redirected to edit this copy. - Finalize Overwrite: The Author edits the copy and clicks "Republish". Because of the missing check in the republish handler, the plugin overwrites the Administrator's original post (ID
20) with the Author's modified content.
6. Test Data Setup
- Administrator:
- Create a Private Post:
wp post create --post_title="Admin Secret" --post_status=private --post_content="CONFIDENTIAL DATA"(ID 1). - Create a Published Post:
wp post create --post_title="Official News" --post_status=publish --post_content="Original content."(ID 2).
- Create a Private Post:
- Contributor: Create user
attacker_cwith rolecontributor. Create one post for them to access nonces. - Author: Create user
attacker_awith roleauthor. Create one post for them to access nonces.
7. Expected Results
- Duplication: The Contributor receives a
302 Redirectto the edit screen of a newly created post (e.g., ID 3). This new post has the same content as the Admin's private post (ID 1). - Rewrite: The Author receives a
302 Redirectto a new post with statusdp-rewrite-republish. After "Republishing," the original post (ID 2) now lists the Author as the contributor of the content and contains the Author's modifications.
8. Verification Steps
- Verify Duplication:
wp post get [NEW_ID] --field=post_content # Should match "CONFIDENTIAL DATA" wp post get [NEW_ID] --field=post_author # Should match Attacker's User ID - Verify Overwrite:
wp post get 2 --field=post_content # Should show the Author's modified content, NOT "Original content."
9. Alternative Approaches
If the duplicatePost JS object is missing from the edit screen, the nonce can be found in the "Bulk Actions" dropdown menu on the edit.php (Post List) screen.
- The bulk action nonce is usually found in the
_wpnonceparameter of the form at#posts-filter. - The bulk action name is
duplicate_post_clone. - Request:
wp-admin/edit.php?post_type=post&action=duplicate_post_clone&post[]=1&_wpnonce=[NONCE]
Summary
The Yoast Duplicate Post plugin (<= 4.5) is vulnerable to unauthorized modification of data due to missing capability and ownership checks in its post duplication and 'Rewrite & Republish' features. This allows Contributors to clone private or draft posts they shouldn't access, and Authors to overwrite any published post (including those by Administrators) with their own content.
Vulnerable Code
// admin-functions.php @ 4.5 (Initialization logic lacking capability guards for specific actions) function duplicate_post_admin_init() { duplicate_post_plugin_upgrade(); if ( intval( get_site_option( 'duplicate_post_show_notice' ) ) === 1 ) { if ( is_multisite() ) { add_action( 'network_admin_notices', 'duplicate_post_show_update_notice' ); } else { add_action( 'admin_notices', 'duplicate_post_show_update_notice' ); } add_action( 'wp_ajax_duplicate_post_dismiss_notice', 'duplicate_post_dismiss_notice' ); } add_action( 'dp_duplicate_post', 'duplicate_post_copy_post_meta_info', 10, 2 ); add_action( 'dp_duplicate_page', 'duplicate_post_copy_post_meta_info', 10, 2 ); if ( intval( get_option( 'duplicate_post_copychildren' ) ) === 1 ) { add_action( 'dp_duplicate_post', 'duplicate_post_copy_children', 20, 3 ); add_action( 'dp_duplicate_page', 'duplicate_post_copy_children', 20, 3 ); } --- // admin-functions.php @ 4.5 (Intended authorization model via 'copy_posts' capability) if ( empty( $installed_version ) ) { // Get default roles. $default_roles = [ 'editor', 'administrator', 'wpseo_manager', 'wpseo_editor', ]; foreach ( $default_roles as $name ) { $role = get_role( $name ); if ( ! empty( $role ) ) { $role->add_cap( 'copy_posts' ); } }
Security Fix
@@ -11,6 +11,7 @@ } use Yoast\WP\Duplicate_Post\UI\Newsletter; +use Yoast\WP\Duplicate_Post\Utils; require_once DUPLICATE_POST_PATH . 'options.php'; @@ -19,6 +20,8 @@ /** * Wrapper for the option 'duplicate_post_version'. + + * @return mixed */ function duplicate_post_get_installed_version() { return get_option( 'duplicate_post_version' ); @@ -26,6 +29,8 @@ /** * Wrapper for the defined constant DUPLICATE_POST_CURRENT_VERSION. + + * @return string */ function duplicate_post_get_current_version() { return DUPLICATE_POST_CURRENT_VERSION; @@ -35,6 +40,8 @@ /** * Adds handlers depending on the options. + + * @return void */ function duplicate_post_admin_init() { duplicate_post_plugin_upgrade(); @@ -49,32 +56,29 @@ add_action( 'wp_ajax_duplicate_post_dismiss_notice', 'duplicate_post_dismiss_notice' ); } - add_action( 'dp_duplicate_post', 'duplicate_post_copy_post_meta_info', 10, 2 ); - add_action( 'dp_duplicate_page', 'duplicate_post_copy_post_meta_info', 10, 2 ); + add_action( 'duplicate_post_after_duplicated', 'duplicate_post_copy_post_meta_info', 10, 2 ); if ( intval( get_option( 'duplicate_post_copychildren' ) ) === 1 ) { - add_action( 'dp_duplicate_post', 'duplicate_post_copy_children', 20, 3 ); - add_action( 'dp_duplicate_page', 'duplicate_post_copy_children', 20, 3 ); + add_action( 'duplicate_post_after_duplicated', 'duplicate_post_copy_children', 20, 3 ); } if ( intval( get_option( 'duplicate_post_copyattachments' ) ) === 1 ) { - add_action( 'dp_duplicate_post', 'duplicate_post_copy_attachments', 30, 2 ); - add_action( 'dp_duplicate_page', 'duplicate_post_copy_attachments', 30, 2 ); + add_action( 'duplicate_post_after_duplicated', 'duplicate_post_copy_attachments', 30, 2 ); } if ( intval( get_option( 'duplicate_post_copycomments' ) ) === 1 ) { - add_action( 'dp_duplicate_post', 'duplicate_post_copy_comments', 40, 2 ); - add_action( 'dp_duplicate_page', 'duplicate_post_copy_comments', 40, 2 ); + add_action( 'duplicate_post_after_duplicated', 'duplicate_post_copy_comments', 40, 2 ); } - add_action( 'dp_duplicate_post', 'duplicate_post_copy_post_taxonomies', 50, 2 ); - add_action( 'dp_duplicate_page', 'duplicate_post_copy_post_taxonomies', 50, 2 ); + add_action( 'duplicate_post_after_duplicated', 'duplicate_post_copy_post_taxonomies', 50, 2 ); add_filter( 'plugin_row_meta', 'duplicate_post_add_plugin_links', 10, 2 ); }
Exploit Outline
The exploit relies on the fact that duplication and rewrite nonces are action-specific but not tied to specific post IDs or ownership. 1. **Authentication**: An attacker authenticates with a Contributor or Author role. 2. **Nonce Extraction**: The attacker extracts a valid `_wpnonce` for the `duplicate_post_save_as_new_draft` or `duplicate_post_rewrite` actions. This can be done by inspecting the `window.duplicatePost` JavaScript object or bulk action elements on the attacker's own post edit screens. 3. **ID Identification**: The attacker identifies the Target Post ID of a post they do not own or shouldn't have access to (e.g., a Private post or an Administrator's post). 4. **Duplication Attack (Contributor)**: The attacker sends a GET request to `/wp-admin/admin.php?action=duplicate_post_save_as_new_draft&post=[TARGET_ID]&_wpnonce=[NONCE]`. The plugin clones the target post into a new draft owned by the Contributor. 5. **Overwrite Attack (Author)**: The attacker sends a GET request to `/wp-admin/admin.php?action=duplicate_post_rewrite&post=[TARGET_ID]&_wpnonce=[NONCE]`. This creates a 'Rewrite & Republish' draft of the target post. The Author can then modify this draft and use the 'Republish' function to overwrite the original content, as the plugin fails to verify if the Author has the right to edit the original post.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.