REPORT / 01
Analysis Report · Folder Analysis cache/post-expirator_4.9.3 → cache/post-expirator_4.9.4 — CVE-2025-14718
Shared security patch analysis results
02 ·
Lifecycle actions
cancel · resume · skip · regenerate
03 ·
Share this analysis
copy link · embed report
03 ·
CVE Security Analysis & Writeups
ai-generated · per cve
Comprehensive security analysis generated by AI for each confirmed CVE match. Click on a CVE to view the detailed writeup including vulnerability background, technical details, patch analysis, and PoC guide.
CVE-2025-14718
NVD
AI-Generated Analysis
05 ·
Findings
filter · search · paginate
Showing 0 to 0 of 0 results
src/Modules/Workflows/Rest/RestApiV1.php
AI: 2 vulnerabilities
2 true positives
CVE-2025-14718
--- cache/post-expirator_4.9.3/src/Modules/Workflows/Rest/RestApiV1.php 2026-01-10 00:37:55.365416817 +0000+++ cache/post-expirator_4.9.4/src/Modules/Workflows/Rest/RestApiV1.php 2026-01-10 00:40:11.865819824 +0000@@ -11,6 +11,8 @@ use PublishPress\Future\Modules\Workflows\Models\PostModel; use PublishPress\Future\Modules\Workflows\Models\WorkflowModel; use PublishPress\Future\Modules\Workflows\Models\WorkflowsModel;+use PublishPress\Future\Modules\Workflows\CapabilitiesAbstract;+use PublishPress\Future\Framework\WordPress\Utils\WorkflowSanitizationUtil; // TODO: Move this to a controller on the workflows module. class RestApiV1 implements RestApiManagerInterface@@ -19,22 +21,31 @@ public const BASE_PATH = RestApiManager::API_BASE . '/v1'; - public const PERMISSION_READ = 'edit_posts';+ public const PERMISSION_READ = CapabilitiesAbstract::EDIT_WORKFLOWS; - public const PERMISSION_CREATE = 'edit_posts';+ public const PERMISSION_CREATE = CapabilitiesAbstract::EDIT_WORKFLOWS; - public const PERMISSION_UPDATE = 'edit_posts';+ public const PERMISSION_UPDATE = CapabilitiesAbstract::EDIT_WORKFLOWS; - public const PERMISSION_DELETE = 'edit_posts';+ public const PERMISSION_DELETE = CapabilitiesAbstract::EDIT_WORKFLOWS;++ public const PERMISSION_PUBLISH = CapabilitiesAbstract::PUBLISH_WORKFLOWS;++ public const PERMISSION_UNPUBLISH = CapabilitiesAbstract::UNPUBLISH_WORKFLOWS; /** * @var HookableInterface */ private HookableInterface $hooks; - public function __construct(HookableInterface $hooks)- {+ private WorkflowSanitizationUtil $workflowSanitization;++ public function __construct(+ HookableInterface $hooks,+ WorkflowSanitizationUtil $workflowSanitization+ ) { $this->hooks = $hooks;+ $this->workflowSanitization = $workflowSanitization; } public function register()@@ -46,7 +57,10 @@ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [$this, 'getWorkflow'],- 'permission_callback' => [$this, 'checkUserCanCallApi'],+ 'permission_callback' => function ($request) {+ return $this->hasValidNonce($request)+ && current_user_can(self::PERMISSION_READ);+ }, 'args' => [ 'id' => [ 'description' => __('The ID of the workflow', 'post-expirator'),@@ -67,7 +81,10 @@ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [$this, 'getWorkflowsWithManualTrigger'],- 'permission_callback' => [$this, 'checkUserCanCallApi'],+ 'permission_callback' => function ($request) {+ return $this->hasValidNonce($request)+ && current_user_can(self::PERMISSION_READ);+ }, 'args' => [ 'postType' => [ 'description' => __('The post type', 'post-expirator'),@@ -87,7 +104,10 @@ [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [$this, 'createWorkflow'],- 'permission_callback' => [$this, 'checkUserCanCallApi'],+ 'permission_callback' => function ($request) {+ return $this->hasValidNonce($request)+ && current_user_can(self::PERMISSION_CREATE);+ }, 'show_in_index' => false, 'show_in_rest' => true, ]@@ -100,7 +120,10 @@ [ 'methods' => WP_REST_Server::EDITABLE, 'callback' => [$this, 'updateWorkflow'],- 'permission_callback' => [$this, 'checkUserCanCallApi'],+ 'permission_callback' => function ($request) {+ return $this->hasValidNonce($request)+ && current_user_can(self::PERMISSION_UPDATE);+ }, 'args' => [ 'id' => [ 'description' => __('The ID of the workflow', 'post-expirator'),@@ -120,7 +143,10 @@ [ 'methods' => WP_REST_Server::DELETABLE, 'callback' => [$this, 'deleteWorkflow'],- 'permission_callback' => [$this, 'checkUserCanCallApi'],+ 'permission_callback' => function ($request) {+ return $this->hasValidNonce($request)+ && current_user_can(self::PERMISSION_DELETE);+ }, 'args' => [ 'id' => [ 'description' => __('The ID of the workflow', 'post-expirator'),@@ -140,7 +166,10 @@ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [$this, 'getTaxonomyTerms'],- 'permission_callback' => [$this, 'checkUserCanCallApi'],+ 'permission_callback' => function ($request) {+ return $this->hasValidNonce($request)+ && current_user_can(self::PERMISSION_READ);+ }, 'args' => [ 'taxonomy' => [ 'description' => __('The taxonomy name', 'post-expirator'),@@ -160,7 +189,10 @@ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [$this, 'getPostWorkflowSettings'],- 'permission_callback' => [$this, 'checkUserCanCallApi'],+ 'permission_callback' => function ($request) {+ return $this->hasValidNonce($request)+ && current_user_can(self::PERMISSION_READ);+ }, 'args' => [ 'post' => [ 'description' => __('The post ID', 'post-expirator'),@@ -180,7 +212,10 @@ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [$this, 'getAuthors'],- 'permission_callback' => [$this, 'checkUserCanCallApi'],+ 'permission_callback' => function ($request) {+ return $this->hasValidNonce($request)+ && current_user_can(self::PERMISSION_READ);+ }, 'show_in_index' => false, 'show_in_rest' => true, ]@@ -377,9 +412,26 @@ ]); } - public function checkUserCanCallApi($request)+ /**+ * Validate the nonce for the request+ *+ * @param WP_REST_Request $request The request object+ *+ * @return bool True if the nonce is valid, false otherwise+ */+ private function hasValidNonce($request) {- return current_user_can(self::PERMISSION_READ);+ $nonce = $request->get_header('X-WP-Nonce');+ if (!$nonce || !wp_verify_nonce($nonce, 'wp_rest')) {+ return false;+ }++ $workflowNonce = $request->get_header('X-PP-Workflow-Nonce');+ if (!$workflowNonce || !wp_verify_nonce($workflowNonce, 'pp_workflow_action')) {+ return false;+ }++ return true; } public function getAuthors($request)@@ -438,68 +490,6 @@ private function sanitizeWorkflowData($data) {- if (is_array($data)) {- $sanitized = [];- foreach ($data as $key => $value) {- $sanitizedKey = $this->sanitizeWorkflowKey($key);-- if (is_array($value)) {- $sanitized[$sanitizedKey] = $this->sanitizeWorkflowData($value);- } elseif (is_string($value)) {- $sanitized[$sanitizedKey] = sanitize_text_field($value);- } else {- // Preserve booleans, numbers, null as-is- $sanitized[$sanitizedKey] = $value;- }- }- return $sanitized;- }-- return is_string($data) ? sanitize_text_field($data) : $data;- }-- private function sanitizeWorkflowKey($key)- {- /**- * Sanitize keys while preserving JSON Logic operators and camelCase keys.- *- * JSON Logic uses operators like ==, !=, >, <, >=, <= as keys in the data structure.- * These are data, not executable code, so we need to preserve them while removing- * truly dangerous characters that could be used for injection attacks.- *- * - If the key is exactly a valid JSON Logic operator, return it as-is- * - Otherwise, proceed with normal sanitization (removes dangerous chars including operators)- */-- // Whitelist of valid JSON Logic operators that contain =, !, >, or <- $validJsonLogicOperators = [- '==', '===', '!=', '!==', // Equality operators- '>', '<', '>=', '<=', // Comparison operators- '!', '!!', // Logical operators- ];-- // If the key is exactly a valid JSON Logic operator, return it early- if (in_array($key, $validJsonLogicOperators, true)) {- return $key;- }-- // Proceed with normal sanitization- // For valid camelCase keys (postId, postType, etc.), str_replace and preg_replace- // will return the original string quickly when no dangerous characters are found.- // Remove dangerous characters that could be used for code injection:- // - Quotes (single and double) - could break out of JSON/string contexts- // - Backslashes - escape sequences- // - Forward slashes - path traversal- // - Semicolons - command injection- // - Parentheses - function calls- // - Dollar signs - variable references, potential code injection- // - Operator characters (=, !, >, <) if not part of a valid operator- $dangerous = ['"', "'", '\\', '/', ';', '(', ')', '$', '=', '!', '>', '<'];- $sanitized = str_replace($dangerous, '', $key);-- // Remove control characters and null bytes for additional safety- $sanitized = preg_replace('/[\x00-\x1F\x7F]/', '', $sanitized);-- return $sanitized;+ return $this->workflowSanitization->sanitizeWorkflowData($data); } }
AI Analysis
I'll analyze this code diff for security fixes following the specified format.
---
**Vulnerability 1: Missing CSRF Protection (Nonce Validation)**
Vulnerability Existed: yes
TRUE POSITIVE
Cross-Site Request Forgery (CSRF) - CWE-352 - src/Modules/Workflows/Rest/RestApiV1.php - Lines 379-400 (old), 415-435 (new)
Old Code:
```php
public function checkUserCanCallApi($request)
{
return current_user_can(self::PERMISSION_READ);
}
```
Fixed Code:
```php
private function hasValidNonce($request)
{
$nonce = $request->get_header('X-WP-Nonce');
if (!$nonce || !wp_verify_nonce($nonce, 'wp_rest')) {
return false;
}
$workflowNonce = $request->get_header('X-PP-Workflow-Nonce');
if (!$workflowNonce || !wp_verify_nonce($workflowNonce, 'pp_workflow_action')) {
return false;
}
return true;
}
```
Explanation:
The old code only checked user capabilities without validating CSRF tokens (nonces). All REST API endpoints were vulnerable to CSRF attacks where an attacker could trick an authenticated user into performing unwanted actions. The fix implements dual nonce validation: standard WordPress REST nonce (`X-WP-Nonce`) and a custom workflow-specific nonce (`X-PP-Workflow-Nonce`), which are now required in addition to capability checks on all endpoints (lines 59-62, 84-87, 107-110, 125-128, 148-151, 171-174, 194-197, 215-218).
---
**Vulnerability 2: Improper Authorization (Overly Permissive Capabilities)**
Vulnerability Existed: yes
TRUE POSITIVE
Improper Access Control / Privilege Escalation - CWE-269, CWE-276 - src/Modules/Workflows/Rest/RestApiV1.php - Lines 24-27 (old), 24-32 (new)
Old Code:
```php
public const PERMISSION_READ = 'edit_posts';
public const PERMISSION_CREATE = 'edit_posts';
public const PERMISSION_UPDATE = 'edit_posts';
public const PERMISSION_DELETE = 'edit_posts';
```
Fixed Code:
```php
public const PERMISSION_READ = CapabilitiesAbstract::EDIT_WORKFLOWS;
public const PERMISSION_CREATE = CapabilitiesAbstract::EDIT_WORKFLOWS;
public const PERMISSION_UPDATE = CapabilitiesAbstract::EDIT_WORKFLOWS;
public const PERMISSION_DELETE = CapabilitiesAbstract::EDIT_WORKFLOWS;
public const PERMISSION_PUBLISH = CapabilitiesAbstract::PUBLISH_WORKFLOWS;
public const PERMISSION_UNPUBLISH = CapabilitiesAbstract::UNPUBLISH_WORKFLOWS;
```
Explanation:
The old code used generic WordPress capabilities (`edit_posts`) for all workflow operations, allowing any user with post editing permissions to manage workflows. The fix implements granular, workflow-specific capabilities (`EDIT_WORKFLOWS`, `PUBLISH_WORKFLOWS`, `UNPUBLISH_WORKFLOWS`) defined in `CapabilitiesAbstract`, enforcing principle of least privilege and enabling fine-grained permission control specific to workflow management.
---
**Vulnerability 3: Input Validation and Sanitization**
Vulnerability Existed: not sure
Improper Input Validation - CWE-20 - src/Modules/Workflows/Rest/RestApiV1.php - Lines 490-562 (old), 492-494 (new)
Old Code:
```php
private function sanitizeWorkflowData($data)
{
if (is_array($data)) {
$sanitized = [];
foreach ($data as $key => $value) {
$sanitizedKey = $this->sanitizeWorkflowKey($key);
if (is_array($value)) {
$sanitized[$sanitizedKey] = $this->sanitizeWorkflowData($value);
} elseif (is_string($value)) {
$sanitized[$sanitizedKey] = sanitize_text_field($value);
} else {
$sanitized[$sanitizedKey] = $value;
}
}
return $sanitized;
}
return is_string($data) ? sanitize_text_field($data) : $data;
}
private function sanitizeWorkflowKey($key) { ... }
```
Fixed Code:
```php
private function sanitizeWorkflowData($data)
{
return $this->workflowSanitization->sanitizeWorkflowData($data);
}
```
Explanation:
The old code implemented inline sanitization with complex logic for handling JSON Logic operators and special characters. The fix delegates sanitization to an external `WorkflowSanitizationUtil` class. This is a refactoring that improves code maintainability and consistency. Without examining the `WorkflowSanitizationUtil` implementation, it's uncertain whether the new sanitization is more or less secure. The inline implementation explicitly preserved JSON Logic operators (==, !=, >, <, etc.) while removing dangerous characters. The effectiveness of the fix depends on whether the utility class maintains equivalent or better security standards.
CVE Analysis Results:
CVE-2025-14718: Yes
View CVE 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.
Showing 1 to 1 of 1 results