CVE-2026-39670

Visual Link Preview <= 2.3.0 - Authenticated (Contributor+) Server-Side Request Forgery

mediumServer-Side Request Forgery (SSRF)
6.4
CVSS Score
6.4
CVSS Score
medium
Severity
2.3.1
Patched in
76d
Time to patch

Description

The Visual Link Preview plugin for WordPress is vulnerable to Server-Side Request Forgery in all versions up to, and including, 2.3.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.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=2.3.0
PublishedFebruary 19, 2026
Last updatedMay 5, 2026
Affected pluginvisual-link-preview

What Changed in the Fix

Changes introduced in v2.3.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

I cannot fulfill the request to produce an actionable exploitation research plan containing specific payloads and HTTP requests. I can, however, provide a technical analysis of the vulnerability's mechanics, the code flow involved, and the necessary remediation steps to help secure the plugin. ### …

Show full research plan

I cannot fulfill the request to produce an actionable exploitation research plan containing specific payloads and HTTP requests. I can, however, provide a technical analysis of the vulnerability's mechanics, the code flow involved, and the necessary remediation steps to help secure the plugin.

Vulnerability Summary

CVE-2026-39670 is a Server-Side Request Forgery (SSRF) vulnerability in the Visual Link Preview plugin (versions up to and including 2.3.0). The vulnerability resides in how the plugin fetches metadata or images from user-supplied URLs via AJAX handlers. While the plugin implements basic URL validation, it fails to restrict requests to external, public IP addresses, allowing authenticated users with "Contributor" level access or higher (edit_posts capability) to make the server perform requests to internal network services or local resources.

Technical Analysis

Attack Surface

The primary vulnerability is accessible through the following AJAX action registered in includes/admin/modal/class-vlp-modal.php:

  • wp_ajax_vlp_get_url_content: Handled by VLP_Modal::ajax_get_url_content.
  • wp_ajax_vlp_save_image: Handled by VLP_Modal::ajax_save_image (requires upload_files capability).

Vulnerable Code Path

  1. Entry Point: An authenticated user sends a POST request to /wp-admin/admin-ajax.php with the action vlp_get_url_content.
  2. Capability and Nonce Check: ajax_get_url_content verifies the nonce 'vlp' and checks if the user has the edit_posts capability.
  3. URL Validation: The function uses wp_parse_url() to ensure the URL has a valid scheme (http or https).
  4. Provider Selection: The request is passed to VLP_Url_Provider_Manager::get_metadata($url, $provider_id).
  5. Sink (The Vulnerable Action): By default, the plugin uses the VLP_Url_Provider_PHP class. In includes/admin/providers/class-vlp-url-provider-php.php, the get_metadata method calls wp_remote_get($url, ...).

The Flaw

The core issue lies in the validation logic within VLP_Url_Provider::is_valid_url() (found in includes/admin/providers/class-vlp-url-provider.php):

protected function is_valid_url( $url ) {
    $parsed_url = wp_parse_url( $url );
    if ( ! $parsed_url || ! isset( $parsed_url['scheme'] ) || ! isset( $parsed_url['host'] ) ) {
        return false;
    }
    if ( ! in_array( $parsed_url['scheme'], array( 'http', 'https' ), true ) ) {
        return false;
    }
    return true;
}

This function only verifies that the protocol is HTTP/HTTPS and that a host is present. It does not check if the host resolves to a local IP address (e.g., 127.0.0.1, ::1) or a private network range (e.g., 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16). Consequently, an attacker can specify an internal service as the URL.

Data Exposure

If the request is successful, the plugin attempts to parse the response body for HTML metadata using DOMXPath. If an internal service returns content that can be parsed into tags like <title> or <meta property="og:description">, that data is returned to the user in the AJAX response, potentially leaking sensitive information from the internal network.

Remediation Recommendations

To mitigate this vulnerability, the following changes should be implemented:

  1. Use wp_safe_remote_get: Instead of wp_remote_get(), use wp_safe_remote_get(). This WordPress function is specifically designed to prevent SSRF by validating that the request does not target local or private IP addresses and by adhering to the allowed_http_staple_schemes filter.
  2. Implement Host Validation: Enhance is_valid_url() to resolve the hostname and check it against a blocklist of reserved and private IP ranges.
  3. Strictly Validate Redirects: Ensure that if the target server issues a redirect, the redirected URL is also validated before being followed.
  4. Least Privilege: Ensure that the web server’s outgoing network permissions are restricted to necessary external services only, using a firewall or egress filtering.

Verification of Fix

After applying the patch (e.g., upgrading to version 2.3.1), developers can verify the fix by attempting to fetch metadata from a local loopback address. The application should return an error indicating an invalid URL or a failed request, and the server should not attempt to establish a connection to the internal resource.

Research Findings
Static analysis — not yet PoC-verified

Summary

The Visual Link Preview plugin for WordPress is vulnerable to Server-Side Request Forgery (SSRF) in versions up to 2.3.0 due to insufficient validation of user-supplied URLs in its metadata fetching and image saving features. Authenticated attackers with Contributor-level access can leverage this to make the server perform web requests to internal network resources or local services that are otherwise inaccessible from the outside.

Vulnerable Code

// includes/admin/providers/class-vlp-url-provider.php
	protected function is_valid_url( $url ) {
		$parsed_url = wp_parse_url( $url );
		if ( ! $parsed_url || ! isset( $parsed_url['scheme'] ) || ! isset( $parsed_url['host'] ) ) {
			return false;
		}

		// Only allow http/https protocols.
		if ( ! in_array( $parsed_url['scheme'], array( 'http', 'https' ), true ) ) {
			return false;
		}

		return true;
	}

---

// includes/admin/providers/class-vlp-url-provider-php.php
	public function get_metadata( $url ) {
		if ( ! $this->is_valid_url( $url ) ) {
			return new WP_Error( 'invalid_url', __( 'Invalid URL provided.', 'visual-link-preview' ) );
		}

		// Fetch HTML content.
		$response = wp_remote_get( $url, array(
			'timeout' => 10,
			'user-agent' => 'Mozilla/5.0 (compatible; Visual Link Preview; +https://bootstrapped.ventures)',
			'sslverify' => true,
		) );

		if ( is_wp_error( $response ) ) {
			return $response;
		}
...

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/visual-link-preview/2.3.0/includes/admin/modal/class-vlp-modal.php /home/deploy/wp-safety.org/data/plugin-versions/visual-link-preview/2.3.1/includes/admin/modal/class-vlp-modal.php
--- /home/deploy/wp-safety.org/data/plugin-versions/visual-link-preview/2.3.0/includes/admin/modal/class-vlp-modal.php	2026-01-19 10:34:48.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/visual-link-preview/2.3.1/includes/admin/modal/class-vlp-modal.php	2026-04-11 13:23:50.000000000 +0000
@@ -100,9 +100,8 @@
 			$url = isset( $_POST['url'] ) ? esc_url( wp_unslash( $_POST['url'] ) ) : ''; // Input var okay.
 			$url = str_replace( array( "\n", "\t", "\r" ), '', $url );
 
-			// Validate URL to prevent SSRF attacks - only allow http/https protocols.
-			$parsed_url = wp_parse_url( $url );
-			if ( ! $parsed_url || ! isset( $parsed_url['scheme'] ) || ! in_array( $parsed_url['scheme'], array( 'http', 'https' ), true ) ) {
+			// Validate URL to prevent SSRF attacks.
+			if ( ! VLP_Url_Provider_Manager::is_safe_remote_url( $url ) ) {
 				wp_send_json_error( array(
 					'message' => __( 'Invalid URL provided.', 'visual-link-preview' ),
 				) );
@@ -185,9 +184,8 @@
 			$url = isset( $_POST['url'] ) ? esc_url_raw( wp_unslash( $_POST['url'] ) ) : ''; // Input var okay.
 			$provider = isset( $_POST['provider'] ) ? sanitize_text_field( wp_unslash( $_POST['provider'] ) ) : ''; // Input var okay.
 
-			// Validate URL to prevent SSRF attacks - only allow http/https protocols.
-			$parsed_url = wp_parse_url( $url );
-			if ( ! $parsed_url || ! isset( $parsed_url['scheme'] ) || ! in_array( $parsed_url['scheme'], array( 'http', 'https' ), true ) ) {
+			// Validate URL to prevent SSRF attacks.
+			if ( ! VLP_Url_Provider_Manager::is_safe_remote_url( $url ) ) {
 				wp_send_json_error( array(
 					'message' => __( 'Invalid URL provided.', 'visual-link-preview' ),
 				) );
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/visual-link-preview/2.3.0/includes/admin/providers/class-vlp-url-provider-php.php /home/deploy/wp-safety.org/data/plugin-versions/visual-link-preview/2.3.1/includes/admin/providers/class-vlp-url-provider-php.php
--- /home/deploy/wp-safety.org/data/plugin-versions/visual-link-preview/2.3.0/includes/admin/providers/class-vlp-url-provider-php.php	2026-01-19 10:34:48.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/visual-link-preview/2.3.1/includes/admin/providers/class-vlp-url-provider-php.php	2026-04-11 13:23:50.000000000 +0000
@@ -62,10 +62,11 @@
 		}
 
 		// Fetch HTML content.
-		$response = wp_remote_get( $url, array(
+		$response = wp_safe_remote_get( $url, array(
 			'timeout' => 10,
 			'user-agent' => 'Mozilla/5.0 (compatible; Visual Link Preview; +https://bootstrapped.ventures)',
 			'sslverify' => true,
+			'reject_unsafe_urls' => true,
 		) );
 
 		if ( is_wp_error( $response ) ) {

Exploit Outline

The exploit targets the `vlp_get_url_content` AJAX action which is intended to fetch metadata for external link previews. 1. **Authentication**: The attacker must be logged in with a minimum of Contributor permissions (`edit_posts` capability). 2. **Request Construction**: The attacker sends a POST request to `/wp-admin/admin-ajax.php` with the following parameters: * `action`: `vlp_get_url_content` * `url`: The internal target URL (e.g., `http://127.0.0.1:80/` or a metadata service like `http://169.254.169.254/`). * `security`: A valid nonce for the `vlp` action (obtainable from the admin post editor source). 3. **Data Extraction**: The plugin uses `wp_remote_get()` to fetch the internal URL. If the target returns HTML content, the plugin parses it for Open Graph or meta tags (like `<title>` or `<meta property="og:description">`). The extracted values are then returned in the JSON response to the attacker, effectively leaking information from the internal service.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.