Visual Link Preview <= 2.3.0 - Authenticated (Contributor+) Server-Side Request Forgery
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:NTechnical Details
<=2.3.0What Changed in the Fix
Changes introduced in v2.3.1
Source Code
WordPress.org SVNI 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 byVLP_Modal::ajax_get_url_content.wp_ajax_vlp_save_image: Handled byVLP_Modal::ajax_save_image(requiresupload_filescapability).
Vulnerable Code Path
- Entry Point: An authenticated user sends a POST request to
/wp-admin/admin-ajax.phpwith the actionvlp_get_url_content. - Capability and Nonce Check:
ajax_get_url_contentverifies the nonce'vlp'and checks if the user has theedit_postscapability. - URL Validation: The function uses
wp_parse_url()to ensure the URL has a valid scheme (httporhttps). - Provider Selection: The request is passed to
VLP_Url_Provider_Manager::get_metadata($url, $provider_id). - Sink (The Vulnerable Action): By default, the plugin uses the
VLP_Url_Provider_PHPclass. Inincludes/admin/providers/class-vlp-url-provider-php.php, theget_metadatamethod callswp_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:
- Use
wp_safe_remote_get: Instead ofwp_remote_get(), usewp_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 theallowed_http_staple_schemesfilter. - Implement Host Validation: Enhance
is_valid_url()to resolve the hostname and check it against a blocklist of reserved and private IP ranges. - Strictly Validate Redirects: Ensure that if the target server issues a redirect, the redirected URL is also validated before being followed.
- 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.
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
@@ -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' ), ) ); @@ -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.