2048 ) { return new \WP_Error( 'siteaudithub_url_invalid', __( 'Invalid or overly long URL.', 'siteaudithub' ) ); } $parts = wp_parse_url( $url ); if ( ! is_array( $parts ) || empty( $parts['scheme'] ) || empty( $parts['host'] ) ) { return new \WP_Error( 'siteaudithub_url_invalid', __( 'URL is missing a scheme or host.', 'siteaudithub' ) ); } $scheme = strtolower( $parts['scheme'] ); if ( 'http' !== $scheme && 'https' !== $scheme ) { return new \WP_Error( 'siteaudithub_url_scheme', __( 'Only http and https URLs are requested.', 'siteaudithub' ) ); } if ( isset( $parts['user'] ) || isset( $parts['pass'] ) ) { return new \WP_Error( 'siteaudithub_url_credentials', __( 'URLs containing credentials are not requested.', 'siteaudithub' ) ); } $host = strtolower( trim( $parts['host'], '[]' ) ); $internal = self::is_internal( $url ); $self_ok = $internal && (int) Settings::get( 'allow_private_self' ) && self::same_port( $parts ); if ( isset( $parts['port'] ) ) { $port = (int) $parts['port']; $own_ports = array(); foreach ( array( home_url(), site_url() ) as $own_url ) { $own_port = wp_parse_url( $own_url, PHP_URL_PORT ); if ( $own_port ) { $own_ports[] = (int) $own_port; } } $allowed_ports = array( 80, 443, 8080, 8443 ); if ( ! in_array( $port, $allowed_ports, true ) && ! ( $internal && in_array( $port, $own_ports, true ) ) ) { return new \WP_Error( 'siteaudithub_url_port', __( 'Requests to this port are blocked.', 'siteaudithub' ) ); } } if ( $self_ok ) { return true; } $blocked_names = array( 'localhost', 'metadata', 'metadata.google.internal', 'instance-data', 'metadata.azure.internal' ); if ( in_array( $host, $blocked_names, true ) || preg_match( '/\.(localhost|internal|local)$/', $host ) ) { return new \WP_Error( 'siteaudithub_url_private', __( 'Requests to internal hostnames are blocked.', 'siteaudithub' ) ); } $ips = self::resolve( $host ); if ( empty( $ips ) ) { return new \WP_Error( 'siteaudithub_url_dns', __( 'The hostname could not be resolved.', 'siteaudithub' ) ); } foreach ( $ips as $ip ) { if ( self::is_blocked_ip( $ip ) ) { return new \WP_Error( 'siteaudithub_url_private', __( 'Requests to private, loopback, reserved or metadata network addresses are blocked.', 'siteaudithub' ) ); } } return true; } /** * Whether URL parts use the same port as this site (so the "own site" * exception never reaches other services on the same host). * * @param array $parts wp_parse_url() parts. * @return bool */ private static function same_port( array $parts ) { $port = isset( $parts['port'] ) ? (int) $parts['port'] : ( 'https' === strtolower( $parts['scheme'] ) ? 443 : 80 ); foreach ( array( home_url(), site_url() ) as $own ) { $o = wp_parse_url( $own ); $own_port = isset( $o['port'] ) ? (int) $o['port'] : ( isset( $o['scheme'] ) && 'https' === $o['scheme'] ? 443 : 80 ); // Allow the http/https pair of the site's own default ports. if ( $port === $own_port || ( ! isset( $o['port'] ) && in_array( $port, array( 80, 443 ), true ) ) ) { return true; } } return false; } /** * Resolve a host to IPs (IPv4 and, when available, IPv6). * * @param string $host Host or IP literal. * @return array */ public static function resolve( $host ) { if ( filter_var( $host, FILTER_VALIDATE_IP ) ) { return array( $host ); } $ips = array(); $ipv4 = @gethostbynamel( $host ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged if ( is_array( $ipv4 ) ) { $ips = $ipv4; } if ( function_exists( 'dns_get_record' ) && defined( 'DNS_AAAA' ) ) { $records = @dns_get_record( $host, DNS_AAAA ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged if ( is_array( $records ) ) { foreach ( $records as $record ) { if ( ! empty( $record['ipv6'] ) ) { $ips[] = $record['ipv6']; } } } } return array_values( array_unique( $ips ) ); } /** * Whether an IP is private, loopback, link-local, reserved, CGNAT or metadata. * * @param string $ip IP. * @return bool */ public static function is_blocked_ip( $ip ) { $ip = strtolower( trim( $ip, '[]' ) ); // IPv4-mapped IPv6 (::ffff:127.0.0.1). if ( preg_match( '/^::ffff:(\d+\.\d+\.\d+\.\d+)$/', $ip, $m ) ) { $ip = $m[1]; } if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) { return true; } if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) { return true; } if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) { $long = ip2long( $ip ); $ranges = array( array( '0.0.0.0', 8 ), array( '100.64.0.0', 10 ), array( '127.0.0.0', 8 ), array( '169.254.0.0', 16 ), array( '192.0.0.0', 24 ), array( '198.18.0.0', 15 ), array( '224.0.0.0', 4 ), array( '240.0.0.0', 4 ), ); foreach ( $ranges as $range ) { $mask = -1 << ( 32 - $range[1] ); if ( ( $long & $mask ) === ( ip2long( $range[0] ) & $mask ) ) { return true; } } return false; } // IPv6: loopback, unspecified, unique-local, link-local, multicast. if ( '::1' === $ip || '::' === $ip ) { return true; } $first = substr( $ip, 0, 4 ); if ( preg_match( '/^(fc|fd)/', $first ) || preg_match( '/^fe[89ab]/', $first ) || 0 === strpos( $ip, 'ff' ) ) { return true; } return false; } /** * Resolve a filesystem path and confirm it lives inside an allowed root. * * @param string $path Path. * @param array $roots Allowed root directories. * @return string|false Real path or false. */ public static function safe_path( $path, array $roots ) { if ( ! is_string( $path ) || '' === $path || false !== strpos( $path, "\0" ) ) { return false; } $real = realpath( $path ); if ( false === $real ) { return false; } $real = wp_normalize_path( $real ); foreach ( $roots as $root ) { $root_real = realpath( $root ); if ( false === $root_real ) { continue; } $root_real = trailingslashit( wp_normalize_path( $root_real ) ); if ( 0 === strpos( trailingslashit( $real ), $root_real ) || $real === untrailingslashit( $root_real ) ) { return $real; } } return false; } /** * Neutralize spreadsheet formula injection in CSV cells. * * @param string $value Cell. * @return string */ public static function csv_cell( $value ) { $value = (string) $value; if ( '' !== $value && in_array( $value[0], array( '=', '+', '-', '@', "\t", "\r" ), true ) ) { $value = "'" . $value; } return $value; } }