| Server IP : 208.122.213.31 / Your IP : 216.73.216.45 Web Server : Apache System : Linux msd6191.mjhst.com 4.18.0-553.137.1.el8_10.x86_64 #1 SMP Wed Jun 24 11:40:24 UTC 2026 x86_64 User : WHMCS_MIA_382 ( 1001) PHP Version : 7.4.33 Disable Function : NONE MySQL : ON | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /home/httpd/html/baberankings.com/wp-content/plugins/w3-total-cache/ |
Upload File : |
<?php
/**
* File: Util_Url.php
*
* @package W3TC
*
* @since 2.10.0
*/
namespace W3TC;
/**
* Class Util_Url
*
* Outbound-URL guard: resolves a URL's host and rejects any address
* that targets a private / loopback / link-local / reserved range.
*
* Every outbound `wp_remote_*` call where the URL is admin-influenced
* (license API override, AlwaysCached queue, recursive sitemap
* fetcher, varnish purge, forums-api tab id, etc.) MUST run through
* {@see self::is_public_host()} before issuing the request. Without
* this check, a `wp_remote_get( $admin_url )` whose `$admin_url`
* carries an internal-range value would target internal services:
* AWS instance metadata at `169.254.169.254`, a Redis at
* `127.0.0.1:6379`, an RFC1918 neighbour, the WordPress front-end
* (`http://0.0.0.0/`), etc.
*
* **Residual risk: DNS rebinding.** `gethostbynamel()` resolves once
* at check time; a hostile resolver can return a public IP for the
* check and a private IP for the subsequent fetch. Closing that gap
* requires `CURLOPT_RESOLVE` (pinning the IP at the cURL layer) or a
* single end-to-end resolve. WordPress's `wp_remote_*` doesn't expose
* `CURLOPT_RESOLVE`, so the documented operator-level posture is an
* egress firewall / outbound policy that blocks RFC1918 destinations
* from the application tier. This class documents the gap rather
* than hiding it.
*
* @since 2.10.0
*/
class Util_Url {
/**
* Returns true when the URL parses, has an http/https scheme, and
* every IP its hostname resolves to is a public-range address.
*
* False is returned for any of:
*
* - non-string / empty input
* - malformed URL (no scheme, no host)
* - non-http/https scheme (`file://`, `gopher://`, `ftp://`, etc.)
* - hostname that fails to resolve
* - any resolved IP that is loopback, RFC1918, link-local,
* reserved, or an IPv4-mapped IPv6 address pointing at the
* above (e.g. `::ffff:127.0.0.1`)
* - the host parsed as a bare IP literal that hits any of the
* above checks directly (so `http://127.0.0.1/`, `http://[::1]/`,
* and `http://10.0.0.5/` are refused without a DNS lookup)
*
* @since 2.10.0
*
* @param string $w3tc_url Candidate URL.
*
* @return bool
*/
public static function is_public_host( $w3tc_url ) {
if ( ! \is_string( $w3tc_url ) || '' === $w3tc_url ) {
return false;
}
if ( ! self::is_valid_http_scheme( $w3tc_url ) ) {
return false;
}
$host = \wp_parse_url( $w3tc_url, PHP_URL_HOST );
if ( ! \is_string( $host ) || '' === $host ) {
return false;
}
/**
* Strip the surrounding brackets from an IPv6 literal so the
* validator can recognise the address shape.
*/
if ( '[' === \substr( $host, 0, 1 ) && ']' === \substr( $host, -1 ) ) {
$host = \substr( $host, 1, -1 );
}
/**
* If the host is already a literal IP, validate it directly —
* no DNS lookup needed, and the wp_remote_* layer would have
* used the literal anyway.
*/
if ( false !== \filter_var( $host, FILTER_VALIDATE_IP ) ) {
return self::is_public_ip( $host );
}
/**
* Resolve A records (v4). `gethostbynamel()` returns the full v4
* resolution set; we accept the URL only when every result is
* public. (One CNAME -> mixed result set is exotic but possible.)
* An empty / failed result here is NOT fatal on its own — an
* IPv6-only host has no A records but legitimately resolves via
* AAAA. We treat A-failure as "consult AAAA" rather than refuse.
*/
$ipv4_set = @\gethostbynamel( $host );
if ( false === $ipv4_set ) {
$ipv4_set = array();
}
foreach ( $ipv4_set as $ip ) {
if ( ! \is_string( $ip ) || ! self::is_public_ip( $ip ) ) {
return false;
}
}
/**
* Resolve AAAA records (v6). `dns_get_record()` is PHP-builtin
* but disabled on some hardened hosts, so its absence isn't an
* error — we just lose the v6 leg of the check.
*/
$ipv6_set = array();
if ( \function_exists( 'dns_get_record' ) ) {
$aaaa = @\dns_get_record( $host, DNS_AAAA );
if ( \is_array( $aaaa ) ) {
foreach ( $aaaa as $rec ) {
if ( isset( $rec['ipv6'] ) && \is_string( $rec['ipv6'] ) ) {
$ipv6_set[] = $rec['ipv6'];
}
}
}
}
foreach ( $ipv6_set as $ip ) {
if ( ! self::is_public_ip( $ip ) ) {
return false;
}
}
/**
* Refuse hosts that have NO public address at all — i.e. neither
* a v4 resolution nor a v6 resolution. This covers both the
* genuinely-unresolvable case and the "dns_get_record disabled
* and gethostbynamel returned nothing" edge.
*/
if ( empty( $ipv4_set ) && empty( $ipv6_set ) ) {
return false;
}
return true;
}
/**
* Returns true when the URL has an http/https scheme and its host
* matches this installation's own home or site host.
*
* The comparison is a case-insensitive host-string match — no DNS
* lookup. Requests the plugin issues to its own front-end (e.g.
* priming this site's own minify cache files) must keep working on
* installs whose hostname resolves to an internal address
* (intranet, staging, split-horizon DNS), where a resolution-based
* check like {@see self::is_public_host()} would refuse them.
*
* @since 2.10.1
*
* @param string $w3tc_url Candidate URL.
*
* @return bool
*/
public static function is_self_host_url( $w3tc_url ) {
if ( ! self::is_valid_http_scheme( $w3tc_url ) ) {
return false;
}
$host = \wp_parse_url( $w3tc_url, PHP_URL_HOST );
if ( ! \is_string( $host ) || '' === $host ) {
return false;
}
$self_hosts = array(
\wp_parse_url( \home_url(), PHP_URL_HOST ),
\wp_parse_url( \site_url(), PHP_URL_HOST ),
);
foreach ( $self_hosts as $self_host ) {
if ( \is_string( $self_host ) && '' !== $self_host && 0 === \strcasecmp( $host, $self_host ) ) {
return true;
}
}
return false;
}
/**
* Returns true for an http or https URL; refuses every other
* scheme. `gopher`, `file`, `php`, `data`, etc. each open
* out-of-band fetch paths in their own right and have no place
* in a `wp_remote_*` call.
*
* @since 2.10.0
*
* @param string $w3tc_url Candidate URL.
*
* @return bool
*/
public static function is_valid_http_scheme( $w3tc_url ) {
if ( ! \is_string( $w3tc_url ) || '' === $w3tc_url ) {
return false;
}
$scheme = \wp_parse_url( $w3tc_url, PHP_URL_SCHEME );
return 'http' === $scheme || 'https' === $scheme;
}
/**
* Returns true when the given IP literal is a public-range address.
*
* Wraps `filter_var` with `FILTER_FLAG_NO_PRIV_RANGE` (rejects
* 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7) and
* `FILTER_FLAG_NO_RES_RANGE` (rejects 0.0.0.0/8, 127.0.0.0/8,
* 169.254.0.0/16 / link-local, 224.0.0.0/4 multicast, 240.0.0.0/4
* reserved-future, ::1, fe80::/10, fc00::/7, ff00::/8).
*
* Adds an explicit IPv4-mapped-IPv6 check (`::ffff:127.0.0.1`) —
* PHP's filter sometimes lets these through depending on version,
* and unwrapping the embedded v4 lets us re-run the v4 check.
*
* @since 2.10.0
*
* @param string $ip IP literal (v4 or v6).
*
* @return bool
*/
public static function is_public_ip( $ip ) {
if ( ! \is_string( $ip ) || '' === $ip ) {
return false;
}
/**
* IPv4-mapped IPv6: ::ffff:127.0.0.1 — pull out the embedded v4
* and re-run the check (otherwise FILTER_VALIDATE_IP would treat
* the whole thing as a "public v6 address" with no privacy
* markers, even though it routes to a v4 loopback).
*/
if ( false !== \stripos( $ip, '::ffff:' ) ) {
$tail = \substr( $ip, \stripos( $ip, '::ffff:' ) + 7 );
if ( false !== \filter_var( $tail, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
return self::is_public_ip( $tail );
}
}
return false !== \filter_var(
$ip,
FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
);
}
/**
* Returns true for an IP that is safe as a target for outbound
* internal-services traffic — i.e. RFC1918 / private-range hosts
* are accepted (a Varnish or memcached server on `10.x` or
* `192.168.x` is a normal deployment) BUT loopback, link-local
* (incl. AWS instance-metadata `169.254.169.254`), broadcast, and
* reserved-future ranges are still refused.
*
* Uses `FILTER_FLAG_NO_RES_RANGE` only — without `NO_PRIV_RANGE`.
* Per the PHP docs, `NO_RES_RANGE` blocks `0.0.0.0/8`,
* `127.0.0.0/8`, `169.254.0.0/16`, `240.0.0.0/4`, `::1/128`,
* `::/128`, `::ffff:0:0/96`, and `fe80::/10` — exactly the
* dangerous set for SSRF — while leaving RFC1918 ranges valid.
*
* Use this (not {@see self::is_public_ip()}) at sinks like
* Varnish purge where the legitimate destination is intentionally
* a private-network host but a forged config value pointing at
* cloud metadata or `127.0.0.1:6379` must be refused.
*
* @since 2.10.0
*
* @param string $ip IP literal (v4 or v6).
*
* @return bool
*/
public static function is_safe_internal_ip( $ip ) {
if ( ! \is_string( $ip ) || '' === $ip ) {
return false;
}
// IPv4-mapped IPv6 — pull out and re-check the embedded v4.
if ( false !== \stripos( $ip, '::ffff:' ) ) {
$tail = \substr( $ip, \stripos( $ip, '::ffff:' ) + 7 );
if ( false !== \filter_var( $tail, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
return self::is_safe_internal_ip( $tail );
}
}
return false !== \filter_var(
$ip,
FILTER_VALIDATE_IP,
FILTER_FLAG_NO_RES_RANGE
);
}
/**
* Returns true when `$host` is a hostname or IP literal that
* resolves only to safe outbound destinations — i.e. {@see
* self::is_safe_internal_ip()} returns true for every IP the host
* resolves to.
*
* Mirrors {@see self::is_public_host()}'s resolution shape (literal-
* IP fast path, gethostbynamel for A records, dns_get_record for
* AAAA, refuse when neither resolves) but with the looser
* RFC1918-allowing IP policy.
*
* The caller is responsible for any scheme / port / path validation
* separately — this method takes a hostname or literal, not a URL.
*
* @since 2.10.0
*
* @param string $host Hostname or IP literal (no scheme, no port).
*
* @return bool
*/
public static function host_resolves_safe_internal( $host ) {
if ( ! \is_string( $host ) || '' === $host ) {
return false;
}
// Strip surrounding brackets from an IPv6 literal.
if ( '[' === \substr( $host, 0, 1 ) && ']' === \substr( $host, -1 ) ) {
$host = \substr( $host, 1, -1 );
}
// Literal IP — validate directly.
if ( false !== \filter_var( $host, FILTER_VALIDATE_IP ) ) {
return self::is_safe_internal_ip( $host );
}
$ipv4_set = @\gethostbynamel( $host );
if ( false === $ipv4_set ) {
$ipv4_set = array();
}
foreach ( $ipv4_set as $ip ) {
if ( ! \is_string( $ip ) || ! self::is_safe_internal_ip( $ip ) ) {
return false;
}
}
$ipv6_set = array();
if ( \function_exists( 'dns_get_record' ) ) {
$aaaa = @\dns_get_record( $host, DNS_AAAA );
if ( \is_array( $aaaa ) ) {
foreach ( $aaaa as $rec ) {
if ( isset( $rec['ipv6'] ) && \is_string( $rec['ipv6'] ) ) {
$ipv6_set[] = $rec['ipv6'];
}
}
}
}
foreach ( $ipv6_set as $ip ) {
if ( ! self::is_safe_internal_ip( $ip ) ) {
return false;
}
}
// Refuse hosts with no resolved address at all.
return ! empty( $ipv4_set ) || ! empty( $ipv6_set );
}
/**
* Returns true when the URL parses, has an https scheme, every
* resolved IP is public (per {@see self::is_public_host()}), AND
* the host ends with one of the entries in `$allowed_suffixes`
* (matched case-insensitively against the FQDN's tail).
*
* The suffix list MUST be authoritative-host patterns such as
* `.rackspacecloud.com` — leading dot included so a literal
* `rackspacecloud.com` (without subdomain) is NOT incorrectly
* matched by a `rackspacecloud.com` suffix on an attacker-owned
* `foorackspacecloud.com.evil.example`.
*
* @since 2.10.0
*
* @param string $w3tc_url Candidate URL.
* @param array $allowed_suffixes Host suffixes such as
* `array( '.rackspacecloud.com',
* '.rackcdn.com' )`.
*
* @return bool
*/
public static function is_https_public_host_with_suffix( $w3tc_url, array $allowed_suffixes ) {
if ( ! \is_string( $w3tc_url ) || '' === $w3tc_url ) {
return false;
}
if ( 'https' !== \wp_parse_url( $w3tc_url, PHP_URL_SCHEME ) ) {
return false;
}
if ( ! self::is_public_host( $w3tc_url ) ) {
return false;
}
$host = \wp_parse_url( $w3tc_url, PHP_URL_HOST );
if ( ! \is_string( $host ) || '' === $host ) {
return false;
}
$host_lc = \strtolower( $host );
foreach ( $allowed_suffixes as $suffix ) {
if ( ! \is_string( $suffix ) || '' === $suffix ) {
continue;
}
$suffix_lc = \strtolower( $suffix );
$slen = \strlen( $suffix_lc );
$hlen = \strlen( $host_lc );
if ( $hlen >= $slen && \substr( $host_lc, -$slen ) === $suffix_lc ) {
return true;
}
}
return false;
}
}