The Exploit
An authenticated WordPress Author can forge requests to arbitrary internal hosts by poisoning the $_SERVER['HTTP_HOST'] header through a crafted AJAX call.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: vulnerable-site.com
Content-Type: application/x-www-form-urlencoded
Cookie: wordpress_logged_in_[hash]=[author_session]
action=rl_query_media&nonce=[valid_nonce]&remote_url=http://169.254.169.254/latest/meta-data/&host_override=169.254.169.254
The attacker observes the AJAX response containing JSON data from the AWS metadata service or internal service, revealing credentials, API tokens, or other sensitive configuration. The request originates from the web server's own IP address, bypassing firewall rules that restrict external outbound connections.
What the Patch Did
Before
$remote_url = isset( $data['remote_url'] ) ? esc_url( $data['remote_url'] ) : '';
// validate hostname to prevent SSRF
$parsed_url = wp_parse_args( parse_url( $remote_url ) );
if ( strpos( $parsed_url['host'], 'pexels.com' ) !== false || strpos( $parsed_url['host'], 'pixabay.com' ) !== false ) {
// allow request
$response = wp_remote_get( $remote_url );
} else {
wp_send_json_error( 'Host not whitelisted' );
}
After
$remote_url = isset( $data['remote_url'] ) ? esc_url( $data['remote_url'] ) : '';
// validate hostname to prevent SSRF using strict comparison
$parsed_url = wp_parse_args( parse_url( $remote_url ) );
$allowed_hosts = array( 'pexels.com', 'pixabay.com' );
$is_allowed = false;
foreach ( $allowed_hosts as $allowed_host ) {
if ( $parsed_url['host'] === $allowed_host ||
( strpos( $parsed_url['host'], '.' . $allowed_host ) !== false &&
substr( $parsed_url['host'], -strlen( $allowed_host ) - 1 ) === '.' . $allowed_host ) ) {
$is_allowed = true;
break;
}
}
if ( ! $is_allowed ) {
wp_send_json_error( 'Host not whitelisted' );
}
The patch replaced a weak substring-based check (strpos()) with strict string comparison (===) combined with subdomain validation. The critical addition is the equality operator and the negative check that blocks any hostname not explicitly whitelisted, including those containing the allowed domain as an infix (e.g., pexels.com.attacker.com).
Root Cause
CWE-918: Server-Side Request Forgery (SSRF)
The vulnerability stems from insufficient hostname validation in the ajax_query_media() function within includes/class-remote-library.php. The attacker-controlled remote_url parameter (transmitted via $_POST) is parsed by parse_url(), and the resulting hostname is validated using strpos() to check for substring matches. This trust boundary violation allows an attacker to bypass the allowlist: strpos( 'pexels.com.attacker.com', 'pexels.com' ) returns 0 (truthy), permitting the forged request. The attacker's URL then reaches wp_remote_get(), which performs the HTTP request from the server's network context, accessing internal services unreachable from the internet.
Why It Works
The load-bearing line is the strict equality check: $parsed_url['host'] === $allowed_host. Without it, strpos() remains vulnerable to bypass. The subdomain validation loop (the second condition in the if statement) distinguishes legitimate subdomains (api.pexels.com) from domain-suffix attacks (pexels.com.attacker.com) by confirming the allowed domain appears at the end of the hostname after a dot boundary. The engineer added the positive $is_allowed flag and explicit negation (if ( ! $is_allowed )) to enforce allowlist logic—rejecting anything not provably safe—rather than relying on the absence of a block. This defense-in-depth approach ensures that even if the substring check accidentally triggers, the strict comparison and boundary validation will catch the bypass.
Hardening Checklist
- Use
wp_safe_remote_get()andwp_safe_remote_post()instead ofwp_remote_get()andwp_remote_post()to block requests to private IP ranges (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) by default. - Implement strict hostname allowlist comparison with
===, never substring checks likestrpos()orin_array()without strict type coercion (in_array( $host, $allowed, true )). - Validate parsed URL components independently: check
parse_url( $url )['host']against allowlist before passing towp_remote_*(), and reject URLs with no host or mismatched schemes. - Add a rate limiter on remote fetch endpoints using transient-based counters (
get_transient(),set_transient()) to limit queries per user per minute, reducing reconnaissance surface. - Log SSRF-relevant rejections to
error_log()with the attempted URL and user ID, enabling audit trails and anomaly detection.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-2479