PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / component / known-bots / bots / class-facebook-bot.php

class-facebook-bot.php in Defender Security – Malware Scanner, Login Security & Firewall 6.3.0, at src/component/known-bots/bots/class-facebook-bot.php

257 lines 6.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles Facebook Bot IPs.
4 *
5 * @package WP_Defender\Component\Known_Bots\Bots
6 */
7
8 namespace WP_Defender\Component\Known_Bots\Bots;
9
10 /**
11 * This class is responsible for fetching and managing Facebook Bot IPs.
12 */
13 class Facebook_Bot extends Abstract_Bot {
14 /**
15 * API endpoints to query for IP info.
16 *
17 * @var string[]
18 */
19 public const IP_INFO_SERVICES = array(
20 'https://ipinfo.io/%s/json',
21 'https://ipwho.is/%s',
22 'http://ip-api.com/json/%s',
23 );
24
25 /**
26 * Prefix for site transient cache keys.
27 *
28 * @var string
29 */
30 public const FB_CACHE_KEY_PREFIX = 'wpdef_ip_is_fb_';
31
32 /**
33 * Cache lifetimes.
34 */
35 public const FB_CACHE_TTL_CONFIRMED = WEEK_IN_SECONDS;
36 public const FB_CACHE_TTL_UNCONFIRMED = HOUR_IN_SECONDS;
37
38 /**
39 * Returns the name of the bot.
40 *
41 * @return string The name of the bot.
42 */
43 public function get_name(): string {
44 return 'facebookbot';
45 }
46
47 /**
48 * Fetches the IPs for Facebook Bot.
49 *
50 * This method retrieves the announced IP ranges for Meta/Facebook (AS32934 and AS63293) from RIPEstat.
51 * It returns an array of IP addresses in both IPv4 and IPv6 CIDR formats.
52 *
53 * @return array An array of IP addresses used by Facebook Bot.
54 */
55 public function fetch_ips(): array {
56 $asns = array( 'AS32934', 'AS63293' );
57 $ips = array();
58
59 foreach ( $asns as $asn ) {
60 $url = "https://stat.ripe.net/data/announced-prefixes/data.json?resource={$asn}";
61 $response = wp_remote_get( $url, array( 'timeout' => 5 ) );
62
63 if ( is_array( $response ) && ! is_wp_error( $response ) ) {
64 $data = json_decode( wp_remote_retrieve_body( $response ), true );
65 foreach ( $data['data']['prefixes'] ?? array() as $entry ) {
66 if ( isset( $entry['prefix'] ) && is_string( $entry['prefix'] ) ) {
67 $ips[] = $entry['prefix'];
68 }
69 }
70 }
71 }
72
73 return array_unique( $ips );
74 }
75
76 /**
77 * Checks if the user agent belongs to Facebook.
78 *
79 * @param string $user_agent Optional user agent string to check.
80 *
81 * @return bool
82 */
83 public function is_ua( string $user_agent = '' ): bool {
84 $ua = $this->normalize_ua( $user_agent );
85 if ( '' === $ua ) {
86 return false;
87 }
88
89 return (bool) preg_match( '/facebookexternalhit|facebot|facebookcatalog|meta-(externalagent|externalads|externalfetcher|webindexer)/i', $ua );
90 }
91
92 /**
93 * Determines if the request User-Agent belongs to Apple iMessage link preview.
94 *
95 * Apple iMessage uses a full browser UA containing AppleWebKit, Safari, Facebot,
96 * facebookexternalhit, and Twitterbot to fetch link preview metadata directly
97 * from consumer device IPs. It does not use Meta crawler URLs.
98 *
99 * @param string $user_agent Optional user agent string.
100 * @return bool
101 */
102 public function is_imessage_ua( string $user_agent = '' ): bool {
103 $ua = $this->normalize_ua( $user_agent );
104 if ( '' === $ua ) {
105 return false;
106 }
107
108 // Must contain both preview substrings.
109 if ( false === stripos( $ua, 'facebookexternalhit' ) || false === stripos( $ua, 'twitterbot' ) ) {
110 return false;
111 }
112
113 // Must not be a Facebook crawler URL pattern (attacker trying to forge preview tokens).
114 if ( false !== stripos( $ua, 'facebook.com/externalhit' ) ) {
115 return false;
116 }
117
118 // Apple iMessage always contains Safari or WebKit browser tokens.
119 return false !== stripos( $ua, 'applewebkit' ) || false !== stripos( $ua, 'safari' );
120 }
121
122 /**
123 * Checks if IP is from Facebook, based on cache, IP prefix allowlist,
124 * DNS verification, and external IP info services.
125 *
126 * @param string $ip The IP address to check.
127 *
128 * @return bool
129 */
130 public function is_ip( string $ip ): bool {
131 if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
132 return false;
133 }
134
135 // Legitimate Apple iMessage preview clients originate from consumer IPs, not Meta data centers.
136 if ( $this->is_imessage_ua() ) {
137 return true;
138 }
139
140 $cache_key = self::FB_CACHE_KEY_PREFIX . md5( $ip );
141
142 $cached = get_site_transient( $cache_key );
143 if ( false !== $cached ) {
144 return (bool) $cached;
145 }
146
147 // 1. Check announced Meta IP prefix allowlist (AS32934 / AS63293).
148 $allowed_ips = get_site_transient( 'wpdef_known_bot_ips_' . $this->get_name() );
149 if ( ! is_array( $allowed_ips ) || array() === $allowed_ips ) {
150 $allowed_ips = $this->fetch_ips();
151 if ( array() !== $allowed_ips ) {
152 set_site_transient( 'wpdef_known_bot_ips_' . $this->get_name(), $allowed_ips, DAY_IN_SECONDS );
153 }
154 }
155
156 if ( array() !== $allowed_ips && $this->is_ip_in_format( $ip, $allowed_ips ) ) {
157 set_site_transient( $cache_key, 1, self::FB_CACHE_TTL_CONFIRMED );
158
159 return true;
160 }
161
162 // 2. Forward-confirmed reverse DNS (FCrDNS) check.
163 if ( $this->verify_dns( $ip, '(^|\.)(facebook\.com|fbsv\.net|fbcdn\.net)' ) ) {
164 set_site_transient( $cache_key, 1, self::FB_CACHE_TTL_CONFIRMED );
165
166 return true;
167 }
168
169 // 3. Fallback to external IP info services.
170 $is_fb = $this->query_ip_info_services( $ip );
171
172 set_site_transient(
173 $cache_key,
174 $is_fb ? 1 : 0,
175 $is_fb ? self::FB_CACHE_TTL_CONFIRMED : self::FB_CACHE_TTL_UNCONFIRMED
176 );
177
178 return $is_fb;
179 }
180
181 /**
182 * Queries external IP information services to verify whether an IP belongs to Facebook/Meta.
183 *
184 * @param string $ip The IP address to check.
185 *
186 * @return bool
187 */
188 private function query_ip_info_services( string $ip ): bool {
189 foreach ( self::IP_INFO_SERVICES as $endpoint ) {
190 $url = sprintf( $endpoint, $ip );
191
192 $response = wp_remote_get(
193 $url,
194 array(
195 'timeout' => 3,
196 'headers' => array( 'Accept' => 'application/json' ),
197 )
198 );
199
200 if ( is_wp_error( $response ) ) {
201 continue;
202 }
203
204 $body = wp_remote_retrieve_body( $response );
205 $data = json_decode( $body, true );
206
207 if ( ! is_array( $data ) ) {
208 continue;
209 }
210
211 $org = $data['org'] ?? $data['connection']['org'] ?? null;
212 $isp = $data['isp'] ?? $data['connection']['isp'] ?? null;
213
214 if (
215 preg_match( '/facebook|meta/i', (string) ( $org ?? '' ) ) ||
216 preg_match( '/facebook|meta/i', (string) ( $isp ?? '' ) )
217 ) {
218 return true;
219 }
220 }
221
222 return false;
223 }
224
225 /**
226 * Clear all Facebook IP check transients.
227 */
228 public function clear_fb_transients(): void {
229 global $wpdb;
230
231 $cache_key = self::FB_CACHE_KEY_PREFIX;
232 $like_pattern = "_site_transient_{$cache_key}%";
233 $timeout_pattern = "_site_transient_timeout_{$cache_key}%";
234
235 if ( is_multisite() ) {
236 $table = $wpdb->sitemeta;
237 $key_column = 'meta_key';
238 } else {
239 $table = $wpdb->options;
240 $key_column = 'option_name';
241 }
242
243 // Delete Facebook IP check transients.
244 $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
245 $wpdb->prepare(
246 "DELETE FROM {$table} WHERE {$key_column} LIKE %s OR {$key_column} LIKE %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
247 $like_pattern,
248 $timeout_pattern
249 )
250 );
251
252 if ( function_exists( 'wp_cache_flush' ) ) {
253 wp_cache_flush();
254 }
255 }
256 }
257