403 ) ); } /** * Validate an outbound URL before wp_remote_* calls. * Blocks cloud metadata endpoints and non-http(s) schemes. * Private/LAN addresses stay allowed: this is an admin/editor tool and * intranet/localhost feeds are a legitimate use (dev sites, supplier LANs). * * @param string $url URL. * @return true|\WP_Error */ public static function validate_outbound_url( string $url ) { $url = trim( $url ); if ( '' === $url ) { return new \WP_Error( 'rafgrid_bad_url', __( 'Empty URL.', 'rafgrid' ) ); } $scheme = strtolower( (string) wp_parse_url( $url, PHP_URL_SCHEME ) ); if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) { return new \WP_Error( 'rafgrid_bad_url', __( 'Only http(s) URLs are allowed.', 'rafgrid' ) ); } $host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) ); // Cloud metadata endpoints: never legitimate here, always blocked. if ( in_array( $host, array( 'metadata.google.internal', 'metadata' ), true ) || '169.254.169.254' === $host ) { return new \WP_Error( 'rafgrid_blocked_url', __( 'This URL is blocked.', 'rafgrid' ) ); } if ( filter_var( $host, FILTER_VALIDATE_IP ) && self::is_link_local( $host ) ) { return new \WP_Error( 'rafgrid_blocked_url', __( 'Link-local URLs are blocked.', 'rafgrid' ) ); } /** * Filter outbound URL validation. Return WP_Error to block. * * @param true|\WP_Error $result Result. * @param string $url URL. */ return apply_filters( 'rafgrid/outbound_url', true, $url ); } private static function is_link_local( string $ip ): bool { if ( str_starts_with( $ip, '169.254.' ) || '::1' === $ip || str_starts_with( $ip, 'fe80:' ) ) { return true; } return false; } }