[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$ffxYAV6H6TqiYcWSLXtxVh1Mdq-z2CtcxJE7_P6fHqdE":3},{"id":4,"url_slug":5,"title":6,"description":7,"plugin_slug":8,"theme_slug":9,"affected_versions":10,"patched_in_version":11,"severity":12,"cvss_score":13,"cvss_vector":14,"vuln_type":15,"published_date":16,"updated_date":17,"references":18,"days_to_patch":20,"patch_diff_files":21,"patch_trac_url":9,"research_status":25,"research_verified":26,"research_rounds_completed":27,"research_plan":28,"research_summary":29,"research_vulnerable_code":30,"research_fix_diff":31,"research_exploit_outline":32,"research_model_used":33,"research_started_at":34,"research_completed_at":35,"research_error":9,"poc_status":9,"poc_video_id":9,"poc_summary":9,"poc_steps":9,"poc_tested_at":9,"poc_wp_version":9,"poc_php_version":9,"poc_playwright_script":9,"poc_exploit_code":9,"poc_has_trace":26,"poc_model_used":9,"poc_verification_depth":9,"poc_exploit_code_gated":26,"source_links":36},"CVE-2026-32421","post-timeline-missing-authorization","Post Timeline \u003C= 2.4.1 - Missing Authorization","The Post Timeline plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in versions up to, and including, 2.4.1. This makes it possible for unauthenticated attackers to perform an unauthorized action.","post-timeline",null,"\u003C=2.4.1","2.4.2","medium",5.3,"CVSS:3.1\u002FAV:N\u002FAC:L\u002FPR:N\u002FUI:N\u002FS:U\u002FC:N\u002FI:L\u002FA:N","Missing Authorization","2026-02-26 00:00:00","2026-04-15 21:01:44",[19],"https:\u002F\u002Fwww.wordfence.com\u002Fthreat-intel\u002Fvulnerabilities\u002Fid\u002F54f7cdb7-4e92-4793-86bb-cbd612e7ec13?source=api-prod",49,[22,23,24],"includes\u002Ffrontend\u002Fapp.php","post-timeline.php","readme.txt","researched",false,3,"This analysis covers **CVE-2026-32421**, a Missing Authorization vulnerability in the **Post Timeline** plugin (\u003C= 2.4.1). The vulnerability allows unauthenticated attackers to perform unauthorized actions, likely modifying plugin settings or data via AJAX.\n\n### 1. Vulnerability Summary\nThe **Post Timeline** plugin fails to implement proper capability checks and nonce verification on one or more AJAX handlers registered via `wp_ajax_nopriv_`. In versions up to and including 2.4.1, certain sensitive functions (most likely `ptl_save_settings` or `ptl_update_order`) are accessible to unauthenticated users. This allows an attacker to modify the plugin's global configuration or manipulate timeline data.\n\n### 2. Attack Vector Analysis\n*   **Endpoint:** `\u002Fwp-admin\u002Fadmin-ajax.php`\n*   **Action:** `ptl_save_settings` (inferred based on plugin architecture and CVSS) or `ptl_update_post_order`.\n*   **Authentication:** None required (Unauthenticated).\n*   **Parameter:** `action`, `nonce`, and the data payload (e.g., `ptl_settings[]`).\n*   **Preconditions:** The plugin must be active. A valid nonce may be required if the handler calls `check_ajax_referer`, but because the nonce is typically exposed on the frontend for timeline functionality, it can be easily retrieved.\n\n### 3. Code Flow\n1.  **Entry Point:** The attacker sends a POST request to `admin-ajax.php` with the `action` parameter set to a vulnerable hook (e.g., `ptl_save_settings`).\n2.  **Hook Registration:** In the plugin's initialization (likely `includes\u002Fplugin.php`), a hook is registered:\n    `add_action('wp_ajax_nopriv_ptl_save_settings', 'save_settings_callback');`\n3.  **Vulnerable Callback:** The callback function (e.g., `save_settings_callback`) is executed. \n4.  **Missing Check:** The function lacks a `current_user_can('manage_options')` check. If a nonce check exists, it likely uses a nonce that is also available to unauthenticated users on the frontend.\n5.  **Sink:** The function eventually calls `update_option('post_timeline_global_settings', ...)` or performs a database modification using user-supplied data from `$_POST`.\n\n### 4. Nonce Acquisition Strategy\nThe plugin enqueues scripts for the frontend timeline which often include a nonce for AJAX operations (like \"Load More\"). \n\n1.  **Identify Shortcode:** The main shortcode is `[post-timeline]`, as seen in `includes\u002Ffrontend\u002Fapp.php`.\n2.  **Create Trigger Page:** Create a public page containing this shortcode.\n    ```bash\n    wp post create --post_type=page --post_title=\"Timeline Test\" --post_status=publish --post_content='[post-timeline]'\n    ```\n3.  **Retrieve Nonce:** Navigate to the page and extract the nonce from the localized JavaScript object. Based on the script registration in `includes\u002Ffrontend\u002Fapp.php`, the handle is `post-timeline-public-script`. The object name is likely `ptl_vars`.\n    *   **Action:** `browser_navigate` to the new page.\n    *   **Action:** `browser_eval(\"window.ptl_vars?.nonce || window.ptl_ajax_obj?.nonce\")`.\n\n### 5. Exploitation Strategy\nWe will attempt to modify the `post_timeline_global_settings` option, specifically targeting a benign setting like `ptl-font-family` to demonstrate the vulnerability without breaking the site.\n\n1.  **Step 1: Discover Action and Nonce Key**\n    Search the plugin code for the AJAX registration:\n    `grep -rn \"wp_ajax_nopriv_\" .`\n    Identify the action that looks like a settings update or data modification.\n2.  **Step 2: Get Nonce**\n    Follow the strategy in Section 4 to obtain the nonce from the frontend.\n3.  **Step 3: Execute Unauthorized Action**\n    Send the malicious request via `http_request`.\n    *   **URL:** `http:\u002F\u002Flocalhost:8080\u002Fwp-admin\u002Fadmin-ajax.php`\n    *   **Method:** `POST`\n    *   **Body:**\n        ```\n        action=ptl_save_settings&nonce=[EXTRACTED_NONCE]&ptl_settings[ptl-font-family]=ExploitText\n        ```\n    *   *Note:* If the action name differs (e.g., `ptl_update_settings`), adjust accordingly.\n4.  **Step 4: Verify Success**\n    Check the WordPress database for the modified option.\n\n### 6. Test Data Setup\n1.  **Install\u002FActivate:** Ensure `post-timeline` version 2.4.1 is active.\n2.  **Create Content:** Create at least one post and assign it to a category so the `[post-timeline]` shortcode renders content.\n    ```bash\n    wp post create --post_type=post --post_title=\"Sample Post\" --post_status=publish\n    ```\n3.  **Create Page:**\n    ```bash\n    wp post create --post_type=page --post_title=\"Exploit Page\" --post_status=publish --post_content='[post-timeline]'\n    ```\n\n### 7. Expected Results\n*   The `admin-ajax.php` request should return a successful status code (e.g., `200 OK` or a JSON `{\"success\":true}`).\n*   The `post_timeline_global_settings` option in the database should be updated with the attacker-controlled value.\n\n### 8. Verification Steps\nAfter the HTTP request, use WP-CLI to confirm the change:\n```bash\n# Get the current settings\nwp option get post_timeline_global_settings --format=json\n```\nVerify that the key (e.g., `ptl-font-family`) matches the payload value (`ExploitText`).\n\n### 9. Alternative Approaches\nIf `ptl_save_settings` is not the vulnerable action:\n1.  **Check for Term Creation:** Look for an AJAX action like `ptl_add_tag`. If found, attempt to create a new tag in the `post_timeline_tags` taxonomy.\n    *   Payload: `action=ptl_add_tag&tag_name=HackedTag&nonce=[NONCE]`\n    *   Verification: `wp term list post_timeline_tags`\n2.  **Check for Order Manipulation:** Look for `ptl_update_post_order`.\n    *   Payload: `action=ptl_","The Post Timeline plugin for WordPress is vulnerable to unauthorized data disclosure due to a missing capability check and post status verification in the ptl_popup_gallery AJAX handler. This allows unauthenticated attackers to retrieve gallery content and metadata from posts that may be private, in draft status, or password-protected by supplying the target post ID.","\u002F\u002F includes\u002Ffrontend\u002Fapp.php around line 352\n    public function ptl_popup_gallery()\n    {\n        $response          = new \\stdclass();\n        $response->success = false;\n        $html              = '';\n        $post_id           = isset($_POST['post_id']) ? sanitize_text_field(wp_unslash($_POST['post_id'])) : '';\n        $post_thumbnail    = get_post_thumbnail_id($post_id);\n        \u002F\u002F\tGet the Custom Meta\n        $post_meta \t     = get_post_custom($post_id);","--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fpost-timeline\u002F2.4.1\u002Fincludes\u002Ffrontend\u002Fapp.php\t2025-12-01 10:57:22.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fpost-timeline\u002F2.4.2\u002Fincludes\u002Ffrontend\u002Fapp.php\t2026-02-26 06:53:04.000000000 +0000\n@@ -354,7 +354,17 @@\n         $response          = new \\stdclass();\n         $response->success = false;\n         $html              = '';\n-        $post_id           = isset($_POST['post_id']) ? sanitize_text_field(wp_unslash($_POST['post_id'])) : '';\n+        $post_id           = isset($_POST['post_id']) ? absint(wp_unslash($_POST['post_id'])) : 0;\n+        $post              = ($post_id > 0) ? get_post($post_id) : null;\n+\n+        if (\n+            !$post ||\n+            ('publish' !== $post->post_status && !current_user_can('read_post', $post_id)) ||\n+            post_password_required($post)\n+        ) {\n+            wp_send_json($response);\n+        }\n+\n         $post_thumbnail    = get_post_thumbnail_id($post_id);\n         \u002F\u002F\tGet the Custom Meta\n         $post_meta \t     = get_post_custom($post_id);\n@@ -372,16 +382,16 @@\n             $gallery = explode(',', $gallery);\n             $gallery = array_filter($gallery);\n             if (!empty($gallery)) {\n-                $html = '\u003Cdiv class=\"owl-carousel owl-theme ptl-media-gallery-' . $post_id . '  ptl-media-post-gallery-popup\">';\n+                $html = '\u003Cdiv class=\"owl-carousel owl-theme ptl-media-gallery-' . esc_attr($post_id) . '  ptl-media-post-gallery-popup\">';\n \n                 if ($img) {\n-                    $img_src = 'src=\"' . $img . '\"';\n+                    $img_src = 'src=\"' . esc_url($img) . '\"';\n \n                     if ($this->settings['ptl-lazy-load'] == 'on') {\n-                        $img_src = 'src=\"' . $placeholder_img . '\" data-src=\"' . $img . '\"';\n+                        $img_src = 'src=\"' . esc_url($placeholder_img) . '\" data-src=\"' . esc_url($img) . '\"';\n                     }\n \n-                    $html .= '\u003Cdiv>\u003Cimg ' . $img_src . ' alt=\"' . $image_alt . '\"\u002F>\u003C\u002Fdiv>';\n+                    $html .= '\u003Cdiv>\u003Cimg ' . $img_src . ' alt=\"' . esc_attr($image_alt) . '\"\u002F>\u003C\u002Fdiv>';\n                 }\n \n                 foreach ($gallery as $image) {\n@@ -389,34 +399,33 @@\n                         $url       = wp_get_attachment_image_src($image, 'full')[0];\n                         $image_alt = (!empty(get_post_meta($image, '_wp_attachment_image_alt', true))) ? get_post_meta($image, '_wp_attachment_image_alt', true) : get_the_title($image);\n \n-                        $img_src = 'src=\"' . $url . '\"';\n+                        $img_src = 'src=\"' . esc_url($url) . '\"';\n \n                         if ($this->settings['ptl-lazy-load'] == 'on') {\n-                            $img_src = 'src=\"' . $placeholder_img . '\" data-src=\"' . $url . '\"';\n+                            $img_src = 'src=\"' . esc_url($placeholder_img) . '\" data-src=\"' . esc_url($url) . '\"';\n                         }\n \n-                        $html .= '\u003Cdiv>\u003Cimg ' . $img_src . '  alt=\"' . $image_alt . '\" \u002F>\u003C\u002Fdiv>';\n+                        $html .= '\u003Cdiv>\u003Cimg ' . $img_src . '  alt=\"' . esc_attr($image_alt) . '\" \u002F>\u003C\u002Fdiv>';\n                     }\n                 }\n \n                 $html .= '\u003C\u002Fdiv>';\n             } else {\n                 if ($img) {\n-                    $img_src = 'src=\"' . $img . '\"';\n+                    $img_src = 'src=\"' . esc_url($img) . '\"';\n \n                     if ($this->settings['ptl-lazy-load'] == 'on') {\n-                        $img_src = 'src=\"' . $placeholder_img . '\" data-src=\"' . $img . '\"';\n+                        $img_src = 'src=\"' . esc_url($placeholder_img) . '\" data-src=\"' . esc_url($img) . '\"';\n                     }\n \n-                    $html .= '\u003Cimg ' . $img_src . '  alt=\"' . $image_alt . '\" \u002F>';\n+                    $html .= '\u003Cimg ' . $img_src . '  alt=\"' . esc_attr($image_alt) . '\" \u002F>';\n                 }\n             }\n             $response->gallery = $html;\n             $response->success = true;\n         }\n \n-        echo json_encode($response);\n-        die;\n+        wp_send_json($response);\n     }","1. Identify a target Post ID that is either private, a draft, or password-protected.\n2. Access the WordPress AJAX endpoint at `\u002Fwp-admin\u002Fadmin-ajax.php`.\n3. Send a POST request with the `action` parameter set to `ptl_popup_gallery` and the `post_id` parameter set to the target ID.\n4. No authentication is required for this request as the plugin registers the action with `wp_ajax_nopriv_`.\n5. The server will respond with a JSON object containing the rendered HTML of the gallery and images associated with the restricted post, effectively bypassing intended access controls.","gemini-3-flash-preview","2026-04-18 23:31:06","2026-04-18 23:32:01",{"type":37,"vulnerable_version":38,"fixed_version":11,"vulnerable_browse":39,"vulnerable_zip":40,"fixed_browse":41,"fixed_zip":42,"all_tags":43},"plugin","2.4.1","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fpost-timeline\u002Ftags\u002F2.4.1","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fpost-timeline.2.4.1.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fpost-timeline\u002Ftags\u002F2.4.2","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fpost-timeline.2.4.2.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fpost-timeline\u002Ftags"]