CVE-2026-46697
Description
Fediverse Embeds WordPress plugin prior to 1.5.8 had an unauthenticated SSRF via its media-proxy REST endpoint, allowing arbitrary internal requests.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Fediverse Embeds WordPress plugin prior to 1.5.8 had an unauthenticated SSRF via its media-proxy REST endpoint, allowing arbitrary internal requests.
Vulnerability
The Fediverse Embeds plugin for WordPress versions prior to 1.5.8 registers an unauthenticated REST route ftf/media-proxy in includes/Media_Proxy.php with permission_callback => '__return_true' [1][2]. The endpoint accepts a base64-encoded URL parameter and passes it directly to wp_remote_get() without any domain allowlist validation. Although the source code contains a comment acknowledging the need for validation, the implementation in version 1.5.7 only sets a local $can_download_media flag that is never checked [2]. This results in a full-read Server-Side Request Forgery (SSRF) vulnerability.
Exploitation
An unauthenticated attacker can exploit this by sending a GET request to the REST endpoint with a base64-encoded URL pointing to an internal or external resource. For example, encoding http://internal-listener:9090/ and sending it as the url parameter triggers the plugin to fetch and return the response body [2]. No authentication or user interaction is required; the attacker only needs network access to the WordPress site.
Impact
Successful exploitation allows an attacker to read the full response body of any HTTP(S) resource reachable from the WordPress server, including internal services (e.g., cloud metadata endpoints, databases, or other internal applications). This constitutes a high-severity information disclosure (CWE-918) with a CVSS v3 score of 7.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N) [2]. The attacker gains the ability to probe internal networks and exfiltrate sensitive data.
Mitigation
The vulnerability is patched in version 1.5.8 of the plugin, released on 2026-05-15 [1][2]. The fix introduces Helpers::is_safe_url() and Helpers::is_safe_host() functions that validate the target host against private and reserved IP ranges using filter_var with FILTER_FLAG_NO_PRIV_RANGE and FILTER_FLAG_NO_RES_RANGE, as well as DNS resolution checks for hostnames [2]. Users should update to version 1.5.8 or later immediately. No workaround is available for unpatched versions.
AI Insight generated on Jun 11, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2- Range: <1.5.8
Patches
12f021faf4dadFixed unauthenticated SSRF vulnerability in media proxy endpoint.
1 file changed · +218 −169
includes/Media_Proxy.php+218 −169 modified@@ -1,169 +1,218 @@ -<?php - -namespace FTF_Fediverse_Embeds; -// require_once __DIR__ . "/../vendor/autoload.php"; -if (!class_exists("simple_html_dom_node")) { - require_once __DIR__ . "/../vendor/simplehtmldom/simplehtmldom/simple_html_dom.php"; -} - -use FTF_Fediverse_Embeds\Helpers; - -class Media_Proxy -{ - protected $archival_mode; - - function __construct() - { - // $this->archival_mode = get_option("ftf_fediverse_embeds_archival_mode") === "on" ? true : false; - $this->archival_mode = true; - add_action("rest_api_init", array($this, "register_media_proxy_endpoint")); - add_action("wp_ajax_nopriv_ftf_media_proxy", array($this, "media_proxy"), 1000); - } - - public function register_media_proxy_endpoint(/* $_REQUEST */) - { - register_rest_route("ftf", "media-proxy", array( - "methods" => \WP_REST_Server::READABLE, - "permission_callback" => "__return_true", - "callback" => array($this, "proxy_media"), - )); - } - - public function proxy_media(\WP_REST_Request $request) - { - /* - At minimum, we need to make sure that the media file is being requested from a fediverse server, - otherwise you could load arbitrary files from anywhere. Some fediverse servers may use a different subdomain - for hosting their files, or a CDN. - - The current solution relies heavily on keeping track of allowed domains and converting URLs. - - This needs to be more robust. - */ - - $url = $request["url"]; - $folder_name = "media"; - - if ($this->archival_mode) { - $url = base64_decode($url); - $dir = plugin_dir_path(__FILE__) . "../$folder_name"; - $file_name = basename($url); - $file_extension = pathinfo($file_name, PATHINFO_EXTENSION); - - $file_name = md5($url) . "." . $file_extension; - $file_path = "$dir/$file_name"; - - $file_name_hashed = Helpers::generate_random_string(12) . time(); - $file_path_hashed = "$dir/$file_name_hashed"; - - if (!is_dir($dir)) { - mkdir($dir); - } - - if (!file_exists("$dir/index.html")) { - file_put_contents("$dir/index.html", ""); - } - } - - if ($this->archival_mode && file_exists($file_path)) { - $image_info = getimagesize($file_path); - header("Content-type: {$image_info['mime']}"); - echo file_get_contents($file_path); - } else { - if (!empty($url)) { - global $wp_version; - $parse = parse_url($url); - $domain = $parse["host"]; - - $allowed_domains = array( - "cdn.masto.host", - "pool.jortage.com", - "social-cdn.vivaldi.net" - ); - - $can_download_media = false; - - if (str_ends_with($domain, ".files.fedi.monster")) { - $can_download_media = true; - } elseif (in_array($domain, $allowed_domains)) { - $can_download_media = true; - } else { - // Converting files.domain.social and media.domain.social to domain.social - - $domain = str_replace(array( - "cdn.", - "files.", - "media.", - "pool.", - "s3.", - ), "", $domain); - - $remote_response = wp_remote_get("https://$domain/.well-known/nodeinfo", array( - "user-agent" => "FTF: Fediverse Embeds; WordPress/" . $wp_version . "; " . get_bloginfo("url"), - )); - // Check if this is a fediverse server. - if (!is_wp_error($remote_response) && $remote_response["response"] && $remote_response["response"]["code"] && $remote_response["response"]["code"] === 200) { - $can_download_media = true; - } - } - - $remote_response = wp_remote_get($url, array( - "user-agent" => "FTF: Fediverse Embeds; WordPress/" . $wp_version . "; " . get_bloginfo("url"), - )); - - - $content_type = wp_remote_retrieve_header($remote_response, "content-type"); - $mime_types_safe = array( - "image/apng", - "image/avif", - "image/bmp", - "image/gif", - "image/vnd.microsoft.icon", - "image/jpeg", - "image/png", - "image/svg+xml", - "image/tiff", - "image/webp", - "video/x-msvideo", - "video/mp4", - "video/mpeg", - "video/ogg", - "video/mp2t", - "video/webm", - "video/3gpp", - "audio/3gpp", - "video/3gpp2", - "audio/3gpp2", - "audio/aac", - "audio/midi, audio/x-midi", - "audio/mpeg", - "audio/ogg", - "audio/wav", - "audio/webm", - ); - - if (!in_array($content_type, $mime_types_safe)) { - $can_download_media = false; - } - - if ($can_download_media) { - if ($this->archival_mode) { - // file_put_contents($file_path, $remote_response["body"]); - file_put_contents($file_path_hashed, $remote_response["body"]); - $validate = wp_check_filetype_and_ext($file_path, $file_name); - - if (!in_array($validate["type"], $mime_types_safe)){ - unlink($file_path_hashed); - } else { - rename($file_path_hashed, $file_path); - } - } - } - - header("Content-Type: " . $remote_response["headers"]["content-type"]); - echo $remote_response["body"]; - } - } - exit(); - } -} +<?php + +namespace FTF_Fediverse_Embeds; +// require_once __DIR__ . "/../vendor/autoload.php"; +if (!class_exists("simple_html_dom_node")) { + require_once __DIR__ . "/../vendor/simplehtmldom/simplehtmldom/simple_html_dom.php"; +} + +use FTF_Fediverse_Embeds\Helpers; + +class Media_Proxy +{ + protected $archival_mode; + + function __construct() + { + // $this->archival_mode = get_option("ftf_fediverse_embeds_archival_mode") === "on" ? true : false; + $this->archival_mode = true; + add_action("rest_api_init", array($this, "register_media_proxy_endpoint")); + add_action("wp_ajax_nopriv_ftf_media_proxy", array($this, "media_proxy"), 1000); + } + + public function register_media_proxy_endpoint(/* $_REQUEST */) + { + register_rest_route("ftf", "media-proxy", array( + "methods" => \WP_REST_Server::READABLE, + "permission_callback" => "__return_true", + "callback" => array($this, "proxy_media"), + )); + } + + private function is_safe_host(string $host): bool + { + $host = trim($host, "[]"); + + if (filter_var($host, FILTER_VALIDATE_IP)) { + return (bool) filter_var( + $host, + FILTER_VALIDATE_IP, + FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE + ); + } + + $a_records = dns_get_record($host, DNS_A) ?: []; + $aaaa_records = dns_get_record($host, DNS_AAAA) ?: []; + $all_records = array_merge($a_records, $aaaa_records); + + if (empty($all_records)) { + return false; + } + + foreach ($all_records as $record) { + $ip = $record["ip"] ?? $record["ipv6"] ?? null; + if ($ip === null) { + return false; + } + if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { + return false; + } + } + + return true; + } + + private function is_safe_url(string $url): bool + { + $parsed = parse_url($url); + + if (!in_array($parsed["scheme"] ?? "", ["http", "https"], true)) { + return false; + } + + $host = $parsed["host"] ?? ""; + if ($host === "") { + return false; + } + + return $this->is_safe_host($host); + } + + public function proxy_media(\WP_REST_Request $request) + { + $url = $request["url"]; + $folder_name = "media"; + + if ($this->archival_mode) { + $url = base64_decode($url); + $dir = plugin_dir_path(__FILE__) . "../$folder_name"; + $file_name = basename($url); + $file_extension = pathinfo($file_name, PATHINFO_EXTENSION); + + $file_name = md5($url) . "." . $file_extension; + $file_path = "$dir/$file_name"; + + $file_name_hashed = Helpers::generate_random_string(12) . time(); + $file_path_hashed = "$dir/$file_name_hashed"; + + if (!is_dir($dir)) { + mkdir($dir); + } + + if (!file_exists("$dir/index.html")) { + file_put_contents("$dir/index.html", ""); + } + } + + if (empty($url) || !$this->is_safe_url($url)) { + status_header(403); + exit(); + } + + if ($this->archival_mode && file_exists($file_path)) { + $image_info = getimagesize($file_path); + header("Content-type: {$image_info['mime']}"); + echo file_get_contents($file_path); + } else { + if (!empty($url)) { + global $wp_version; + $parse = parse_url($url); + $domain = $parse["host"]; + + $allowed_domains = array( + "cdn.masto.host", + "pool.jortage.com", + "social-cdn.vivaldi.net" + ); + + $can_download_media = false; + + if (str_ends_with($domain, ".files.fedi.monster")) { + $can_download_media = true; + } elseif (in_array($domain, $allowed_domains)) { + $can_download_media = true; + } else { + // Converting files.domain.social and media.domain.social to domain.social + + $stripped_domain = str_replace(array( + "cdn.", + "files.", + "media.", + "pool.", + "s3.", + ), "", $domain); + + if ($this->is_safe_host($stripped_domain)) { + $remote_response = wp_remote_get("https://$stripped_domain/.well-known/nodeinfo", array( + "user-agent" => "FTF: Fediverse Embeds; WordPress/" . $wp_version . "; " . get_bloginfo("url"), + )); + // Check if this is a fediverse server. + if (!is_wp_error($remote_response) && $remote_response["response"] && $remote_response["response"]["code"] && $remote_response["response"]["code"] === 200) { + $can_download_media = true; + } + } + } + + if (!$can_download_media) { + status_header(403); + exit(); + } + + $remote_response = wp_remote_get($url, array( + "user-agent" => "FTF: Fediverse Embeds; WordPress/" . $wp_version . "; " . get_bloginfo("url"), + )); + + $content_type = wp_remote_retrieve_header($remote_response, "content-type"); + $mime_types_safe = array( + "image/apng", + "image/avif", + "image/bmp", + "image/gif", + "image/vnd.microsoft.icon", + "image/jpeg", + "image/png", + "image/svg+xml", + "image/tiff", + "image/webp", + "video/x-msvideo", + "video/mp4", + "video/mpeg", + "video/ogg", + "video/mp2t", + "video/webm", + "video/3gpp", + "audio/3gpp", + "video/3gpp2", + "audio/3gpp2", + "audio/aac", + "audio/midi, audio/x-midi", + "audio/mpeg", + "audio/ogg", + "audio/wav", + "audio/webm", + ); + + if (!in_array($content_type, $mime_types_safe)) { + status_header(403); + exit(); + } + + if ($this->archival_mode) { + // file_put_contents($file_path, $remote_response["body"]); + file_put_contents($file_path_hashed, $remote_response["body"]); + $validate = wp_check_filetype_and_ext($file_path, $file_name); + + if (!in_array($validate["type"], $mime_types_safe)) { + unlink($file_path_hashed); + } else { + rename($file_path_hashed, $file_path); + } + } + + header("Content-Type: " . $remote_response["headers"]["content-type"]); + echo $remote_response["body"]; + } + } + exit(); + } +}
Vulnerability mechanics
Root cause
"The `$can_download_media` validation flag is set but never checked before the response body is echoed, allowing arbitrary URL fetching without any allowlist enforcement."
Attack vector
An unauthenticated attacker sends a GET request to the WordPress REST endpoint `/ftf/media-proxy` with a `url` parameter containing a base64-encoded target URL. The plugin decodes the URL and passes it directly to `wp_remote_get()` without enforcing any allowlist or checking the `$can_download_media` flag [ref_id=2]. This allows the attacker to make arbitrary HTTP requests from the server and read the full response body, enabling internal network reconnaissance and data exfiltration.
Affected code
The vulnerability resides in `includes/Media_Proxy.php` in the `proxy_media()` method. The REST route `ftf/media-proxy` is registered with `permission_callback => "__return_true"`, making it accessible to any unauthenticated visitor. The `$can_download_media` flag is set during domain validation but is never checked before the response body is echoed, so the `wp_remote_get($url)` call at the end of the method always executes and its full body is returned to the attacker.
What the fix does
The patch adds two new private methods `is_safe_host()` and `is_safe_url()` to `Media_Proxy.php` [patch_id=5619880]. `is_safe_url()` validates that the scheme is `http` or `https` and that the host passes `is_safe_host()`, which uses `filter_var()` with `FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE` to reject private and reserved IPs, and for hostnames it resolves DNS records and rejects any that resolve to a private/reserved IP. The fix also adds early-exit `status_header(403)` calls that block the request before `wp_remote_get()` is reached if the URL is unsafe or if `$can_download_media` is false.
Preconditions
- configThe WordPress site must have the Fediverse Embeds plugin version 1.5.7 installed and active.
- authNo authentication is required; the endpoint is publicly accessible.
- networkThe attacker must be able to send HTTP requests to the WordPress REST API endpoint.
- inputThe attacker provides a base64-encoded URL in the `url` parameter.
Generated on Jun 11, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
2News mentions
0No linked articles in our index yet.