PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 3.1.0
Defender Security – Malware Scanner, Login Security & Firewall v3.1.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 / notfound-lockout.php

notfound-lockout.php in Defender Security – Malware Scanner, Login Security & Firewall 3.1.0, at src/component/notfound-lockout.php

335 lines 8.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WP_Defender\Component;
4
5 use WP_Defender\Component;
6 use WP_Defender\Model\Lockout_Ip;
7 use WP_Defender\Model\Lockout_Log;
8 use WP_Defender\Component\User_Agent;
9
10 class Notfound_Lockout extends Component {
11 use \WP_Defender\Traits\Country;
12
13 public const SCENARIO_ERROR_404 = 'error_404', SCENARIO_ERROR_404_IGNORE = 'error_404_ignore', SCENARIO_LOCKOUT_404 = '404_lockout';
14 /**
15 * Use for cache.
16 *
17 * @var \WP_Defender\Model\Setting\Notfound_Lockout
18 */
19 public $model;
20
21 public function __construct() {
22 $this->model = wd_di()->get( \WP_Defender\Model\Setting\Notfound_Lockout::class );
23 }
24
25 /**
26 * Queue hooks when this class init.
27 */
28 public function add_hooks() {
29 add_action( 'template_redirect', array( &$this, 'process_404_detect' ) );
30 }
31
32 /**
33 * Check if useragent is looks like from googlebot.
34 *
35 * @param string $user_agent
36 *
37 * @return bool
38 */
39 private function is_google_ua( $user_agent = '' ) {
40 if ( empty( $user_agent ) ) {
41 if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
42 return false;
43 }
44 $user_agent = User_Agent::fast_cleaning( $_SERVER['HTTP_USER_AGENT'] );
45 }
46 if ( function_exists( 'mb_strtolower' ) ) {
47 $user_agent = mb_strtolower( $user_agent, 'UTF-8' );
48 } else {
49 $user_agent = strtolower( $user_agent );
50 }
51
52 if ( false !== stristr( $user_agent, 'googlebot' ) ) {
53 return true;
54 }
55
56 return false;
57 }
58
59 /**
60 * Check if IP is from Google, base on https://support.google.com/webmasters/answer/80553?hl=en.
61 *
62 * @param $ip
63 *
64 * @return bool
65 */
66 private function is_google_ip( $ip ) {
67 $hostname = gethostbyaddr( $ip );
68 // Check if this hostname has googlebot or google.com.
69 if ( preg_match( '/\.googlebot|google\.com$/i', $hostname ) ) {
70 $hosts = gethostbynamel( $hostname );
71
72 if ( ! is_array( $hosts ) ) {
73 return false;
74 }
75
76 // Check if this match the original ip.
77 foreach ( $hosts as $host ) {
78 if ( $ip === $host ) {
79 return true;
80 }
81 }
82 }
83
84 return false;
85 }
86
87 /**
88 * @param string $user_agent
89 *
90 * @return bool
91 */
92 private function is_bing_ua( $user_agent = '' ) {
93 if ( empty( $user_agent ) ) {
94 if ( empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
95 return false;
96 }
97 $user_agent = User_Agent::fast_cleaning( $_SERVER['HTTP_USER_AGENT'] );
98 }
99
100 if ( function_exists( 'mb_strtolower' ) ) {
101 $user_agent = mb_strtolower( $user_agent, 'UTF-8' );
102 } else {
103 $user_agent = strtolower( $user_agent );
104 }
105 // MSN Bot Useragent https://www.bing.com/webmaster/help/which-crawlers-does-bing-use-8c184ec0.
106 $msn_ua = 'Bingbot|MSNBot|MSNBot-Media|AdIdxBot|BingPreview';
107
108 if ( preg_match( '/' . $msn_ua . '/i', $user_agent ) ) {
109 return true;
110 }
111
112 return false;
113 }
114
115 /**
116 * Check if IP is from Bing, base on https://www.bing.com/webmaster/help/how-to-verify-bingbot-3905dc26.
117 *
118 * @param string $ip
119 *
120 * @return bool
121 */
122 private function is_bing_ip( $ip ) {
123 $hostname = gethostbyaddr( $ip );
124 if ( preg_match( '/\.msnbot|msn\.com$/i', $hostname ) ) {
125 $hosts = gethostbynamel( $hostname );
126
127 if ( ! is_array( $hosts ) ) {
128 return false;
129 }
130
131 // Check if this match the original ip.
132 foreach ( $hosts as $host ) {
133 if ( $ip === $host ) {
134 return true;
135 }
136 }
137 }
138
139 return false;
140 }
141
142 public function process_404_detect() {
143 if ( ! is_404() ) {
144 return;
145 }
146
147 $ip = $this->get_user_ip();
148 // Check if this from google,
149 if ( $this->is_google_ua() && $this->is_google_ip( $ip ) ) {
150 return;
151 }
152 // or bing.
153 if ( $this->is_bing_ua() && $this->is_bing_ip( $ip ) ) {
154 return;
155 }
156
157 if ( false === $this->model->detect_logged && is_user_logged_in() ) {
158 return;
159 }
160
161 $uri = $_SERVER['REQUEST_URI'];
162 // Strip encode.
163 $uri = urldecode( $uri );
164 $model = Lockout_Ip::get( $ip );
165 $model = $this->record_fail_attempt( $ip, $model );
166
167 $ext = pathinfo( $uri, PATHINFO_EXTENSION );
168 $ext = trim( $ext );
169 // Downfall from match URL to extension.
170 foreach ( $this->model->get_lockout_list( 'allowlist' ) as $pattern ) {
171 $pattern = preg_quote( $pattern, '/' );
172 if ( preg_match( '/' . $pattern . '$/i', $uri ) ) {
173 // Whitelisted, just return.
174 return;
175 }
176 }
177
178 foreach ( $this->model->get_lockout_list( 'blocklist' ) as $pattern ) {
179 $pattern = preg_quote( $pattern, '/' );
180 if ( preg_match( '/' . $pattern . '$/i', $uri ) ) {
181 $this->lock( $model, 'blacklist', $uri );
182 $this->log_event( $ip, $uri, self::SCENARIO_LOCKOUT_404 );
183
184 return;
185 }
186 }
187
188 if ( strlen( $ext ) ) {
189 // If ext isn't null.
190 foreach ( $this->model->get_lockout_list( 'allowlist' ) as $whitelist_ext ) {
191 if ( str_replace( '.', '', strtolower( $whitelist_ext ) ) === $ext ) {
192 // Ext is whitelist, log and return.
193 $this->log_event( $ip, $uri, self::SCENARIO_ERROR_404_IGNORE );
194
195 return;
196 }
197 }
198
199 foreach ( $this->model->get_lockout_list( 'blocklist' ) as $blacklist_ext ) {
200 if ( str_replace( '.', '', strtolower( $blacklist_ext ) ) === $ext ) {
201 // Block it.
202 $this->lock( $model, 'blacklist', $uri );
203 $this->log_event( $ip, $uri, self::SCENARIO_LOCKOUT_404 );
204
205 return;
206 }
207 }
208 }
209
210 $this->log_event( $ip, $uri, self::SCENARIO_ERROR_404 );
211
212 // Count the attempt.
213 $window = strtotime( '- ' . $this->model->timeframe . ' seconds' );
214
215 // We will get the latest till oldest, limit by attempt.
216 if ( ! is_array( $model->meta['nf'] ) ) {
217 $model->meta['nf'] = array();
218 }
219
220 $checks = array_slice( $model->meta['nf'], $this->model->attempt * - 1 );
221
222 if ( count( $checks ) < $this->model->attempt ) {
223 return;
224 }
225 // If the last time is larger,
226 $check = min( $checks );
227 if ( $check >= $window ) {
228 // then lock it.
229 $this->lock( $model, 'normal', $uri );
230 $this->log_event( $ip, $uri, self::SCENARIO_LOCKOUT_404 );
231 }
232 }
233
234 /**
235 * @param Lockout_Ip $model
236 * @param string $scenario
237 * @param string $uri
238 */
239 private function lock( Lockout_Ip $model, $scenario = 'normal', $uri = '' ) {
240 if ( 'permanent' === $this->model->lockout_type ) {
241 $scenario = 'blacklist';
242 }
243 $model->status = Lockout_Ip::STATUS_BLOCKED;
244 $model->lock_time_404 = time();
245 if ( 'blacklist' === $scenario ) {
246 $model->release_time = strtotime( '+5 years' );
247 } else {
248 $model->release_time = strtotime( '+ ' . $this->model->duration . ' ' . $this->model->duration_unit );
249 }
250 $model->lockout_message = $this->model->lockout_message;
251 $model->save();
252
253 if ( 'blacklist' === $scenario ) {
254 do_action( 'wd_blacklist_this_ip', $model->ip );
255 }
256
257 do_action( 'wd_404_lockout', $model, $scenario );
258 }
259
260 /**
261 * Store the fail attempt of current IP.
262 *
263 * @param string $ip
264 * @param Lockout_Ip $model
265 *
266 * @return Lockout_Ip
267 */
268 protected function record_fail_attempt( $ip, $model ) {
269 // Fix warning with a non-numeric value.
270 if ( ! is_numeric( $model->attempt_404 ) ) {
271 $model->attempt_404 = 1;
272 } else {
273 ++$model->attempt_404;
274 }
275 $model->ip = $ip;
276
277 // Cache the time here, so it consumes less memory than query the logs.
278 if (
279 ! isset( $model->meta['nf'] ) ||
280 ( isset( $model->meta['nf'] ) && ! is_array( $model->meta['nf'] ) )
281 ) {
282 $model->meta['nf'] = array();
283 }
284
285 $model->meta['nf'][] = time();
286 $model->save();
287
288 return $model;
289 }
290
291 /**
292 * Log the event into db, we will use the data in logs page later.
293 *
294 * @param string $ip
295 * @param string $uri
296 * @param string $scenario
297 */
298 public function log_event( $ip, $uri, $scenario ) {
299 $model = new Lockout_Log();
300 $model->ip = $ip;
301 $model->user_agent = isset( $_SERVER['HTTP_USER_AGENT'] )
302 ? User_Agent::fast_cleaning( $_SERVER['HTTP_USER_AGENT'] )
303 : null;
304 $model->date = time();
305 $model->tried = $uri;
306 $model->blog_id = get_current_blog_id();
307
308 $ip_to_country = $this->ip_to_country( $ip );
309
310 if ( ! empty( $ip_to_country ) && isset( $ip_to_country['iso'] ) ) {
311 $model->country_iso_code = $ip_to_country['iso'];
312 }
313
314 switch ( $scenario ) {
315 case self::SCENARIO_ERROR_404:
316 $model->type = Lockout_Log::ERROR_404;
317 $model->log = sprintf( __( "Request for file %s which doesn't exist", 'wpdef' ), $uri );
318 break;
319 case self::SCENARIO_ERROR_404_IGNORE:
320 $model->type = Lockout_Log::ERROR_404_IGNORE;
321 $model->log = sprintf( __( "Request for file %s which doesn't exist", 'wpdef' ), $uri );
322 break;
323 case self::SCENARIO_LOCKOUT_404:
324 default:
325 $model->type = Lockout_Log::LOCKOUT_404;
326 $model->log = sprintf( __( 'Lockout occurred: Too many 404 requests for %s', 'wpdef' ), $uri );
327 break;
328 }
329 $model->save();
330 if ( Lockout_Log::LOCKOUT_404 === $model->type ) {
331 do_action( 'defender_notify', 'firewall-notification', $model );
332 }
333 }
334 }
335