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
← All changes | src/component/class-malicious-bot.php +106 -66 5.7.1 → 6.3.0 View file →
@@ -7,12 +7,14 @@
7 7
8 8 namespace WP_Defender\Component;
9 9
10 10 use WP_Defender\Component;
11 +use WP_Defender\Traits\Country;
12 +use WP_Defender\Model\Lockout_Log;
13 +use WP_Defender\Traits\File_Operations;
14 +use WP_Defender\Integrations\Smartcrawl;
11 15 use WP_Defender\Model\Setting\User_Agent_Lockout;
12 -use WP_Defender\Model\Lockout_Log;
13 -use WP_Defender\Traits\Country;
14 -use WP_Filesystem_Base;
16 +use WP_Defender\Model\Notification\Firewall_Notification;
15 17
16 18 /**
17 19 * Handles operations to insert a weekly rotating hash URL into the footer,
18 20 * and blocking IP addresses that access this URL.
@@ -18,11 +20,14 @@
18 20 * and blocking IP addresses that access this URL.
19 21 */
20 22 class Malicious_Bot extends Component {
21 23 use Country;
24 + use File_Operations;
22 25
23 26 public const URL_HASH_KEY = 'wpdef_malicious_bot_url_hash';
24 27 public const SCENARIO_MALICIOUS_BOT = 'malicious_bot';
28 + public const USER_AGENT_REGEX = '/(^User-agent:\s*\*)/mi';
29 + public const URL_QUERY = 'wpdef-malicious-bot-url';
25 30
26 31 /**
27 32 * The path to the robots.txt file.
28 33 *
@@ -34,9 +39,9 @@
34 39 * The start comment for the robots.txt.
35 40 *
36 41 * @var string
37 42 */
38 - private $start_comment = '# WP Defender - Begin';
43 + private $start_comment = PHP_EOL . '# WP Defender - Begin' . PHP_EOL;
39 44
40 45 /**
41 46 * The end comment for the robots.txt.
42 47 *
@@ -41,9 +46,9 @@
41 46 * The end comment for the robots.txt.
42 47 *
43 48 * @var string
44 49 */
45 - private $end_comment = '# WP Defender - End';
50 + private $end_comment = PHP_EOL . '# WP Defender - End' . PHP_EOL;
46 51
47 52 /**
48 53 * The model for handling the data.
49 54 *
@@ -51,8 +56,15 @@
51 56 */
52 57 protected $model;
53 58
54 59 /**
60 + * SmartCrawl integration instance.
61 + *
62 + * @var Smartcrawl
63 + */
64 + private $smartcrawl;
65 +
66 + /**
55 67 * Constructor for the Malicious_Bot class.
56 68 * Initializes the model and sets the path to the robots.txt file.
57 69 *
58 70 * @param User_Agent_Lockout $model The model instance for malicious bot functionality.
@@ -59,8 +71,9 @@
59 71 */
60 72 public function __construct( User_Agent_Lockout $model ) {
61 73 $this->model = $model;
62 74 $this->robots_file = ABSPATH . 'robots.txt';
75 + $this->smartcrawl = new Smartcrawl();
63 76 }
64 77
65 78 /**
66 79 * Check if the malicious bot is enabled.
@@ -93,8 +106,10 @@
93 106 public function rotate_hash() {
94 107 $new_hash = $this->generate_hash();
95 108 $this->set_hash( $new_hash );
96 109
110 + $this->register_rewrite_rule();
111 +
97 112 flush_rewrite_rules();
98 113
99 114 $this->remove_rule();
100 115 $this->inject_rule();
@@ -113,15 +128,15 @@
113 128 * Handle the robots.txt rule.
114 129 */
115 130 public function handle_robots_txt() {
116 131 if ( $this->is_enabled() ) {
117 - if ( $this->is_smartcrawl_active() ) {
132 + if ( $this->smartcrawl->should_serve_robots() ) {
118 133 add_filter( 'smartcrawl_robots_txt_content', array( $this, 'add_bot_trap_to_smartcrawl_robots' ) );
119 134 } else {
120 135 $this->inject_rule();
121 136 }
122 137 } else {
123 - if ( $this->is_smartcrawl_active() ) {
138 + if ( $this->smartcrawl->is_activated() ) {
124 139 remove_filter( 'smartcrawl_robots_txt_content', array( $this, 'add_bot_trap_to_smartcrawl_robots' ) );
125 140 }
126 141 $this->remove_rule();
127 142 }
@@ -133,14 +148,14 @@
133 148 * @param string $eol The end-of-line character to use, defaults to "\n".
134 149 *
135 150 * @return string The block of text to be added to the robots.txt file.
136 151 */
137 - public function get_block( string $eol = "\n" ): string {
152 + public function get_block( string $eol = PHP_EOL ): string {
138 153 $disallow_path = $this->get_hash();
139 154
140 - return $this->start_comment . $eol .
155 + return $this->start_comment .
141 156 'User-agent: *' . $eol .
142 - "Disallow: /{$disallow_path}" . $eol .
157 + "Disallow: /{$disallow_path}" .
143 158 $this->end_comment;
144 159 }
145 160
146 161 /**
@@ -146,24 +161,16 @@
146 161 /**
147 162 * Inject the malicious bot rule into the robots.txt.
148 163 */
149 164 public function inject_rule() {
150 - global $wp_filesystem;
151 - // Initialize the WP filesystem, no more using 'file-put-contents' function.
152 - if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
153 - require_once ABSPATH . '/wp-admin/includes/file.php';
154 - WP_Filesystem();
155 - }
165 + $wp_filesystem = $this->get_wp_filesystem();
156 166
157 167 if ( $wp_filesystem->exists( $this->robots_file ) ) {
158 168 // Inject rule into physical robots.txt file.
159 169 $contents = $wp_filesystem->get_contents( $this->robots_file );
160 - $eol = $this->detect_line_ending( $contents );
170 + if ( ! str_contains( $contents, $this->start_comment ) ) {
171 + $contents = $this->add_rules_to_content( $contents );
161 172
162 - if ( strpos( $contents, $this->start_comment ) === false ) {
163 - $block = $eol . $this->get_block( $eol ) . $eol;
164 - $contents .= $block;
165 -
166 173 $dir_name = pathinfo( $this->robots_file, PATHINFO_DIRNAME );
167 174 if ( $wp_filesystem->is_writable( $dir_name ) ) {
168 175 $wp_filesystem->put_contents( $this->robots_file, $contents );
169 176 }
@@ -169,35 +176,13 @@
169 176 }
170 177 }
171 178 } else {
172 179 // Inject virtual rule via filter.
173 - add_filter(
174 - 'robots_txt',
175 - function ( $output ) {
176 - return $output . "\n" . $this->get_block();
177 - },
178 - 10,
179 - 2
180 - );
180 + add_filter( 'robots_txt', array( $this, 'add_rules_to_content' ) );
181 181 }
182 182 }
183 183
184 184 /**
185 - * Check if SmartCrawl plugin is active.
186 - *
187 - * @return bool True if SmartCrawl is active, false otherwise.
188 - */
189 - public function is_smartcrawl_active(): bool {
190 - if ( ! function_exists( 'is_plugin_active' ) ) {
191 - require_once ABSPATH . 'wp-admin/includes/plugin.php';
192 - }
193 -
194 - return class_exists( '\SmartCrawl\Modules\Advanced\Robots\Controller' ) ||
195 - function_exists( 'smartcrawl_get_robots_url' ) ||
196 - is_plugin_active( 'smartcrawl-seo/wpmu-dev-seo.php' );
197 - }
198 -
199 - /**
200 185 * Add bot trap rules to SmartCrawl's robots.txt content.
201 186 *
202 187 * @param string $content The existing robots.txt content from SmartCrawl.
203 188 * @return string The modified robots.txt content with bot trap rules.
@@ -206,11 +191,9 @@
206 191 if ( ! $this->is_enabled() ) {
207 192 return $content;
208 193 }
209 194
210 - $bot_trap_block = $this->get_block();
211 -
212 - return $content . PHP_EOL . PHP_EOL . $bot_trap_block;
195 + return $this->add_rules_to_content( $content );
213 196 }
214 197
215 198 /**
216 199 * Remove the bot trap rule from the robots.txt.
@@ -215,14 +198,9 @@
215 198 /**
216 199 * Remove the bot trap rule from the robots.txt.
217 200 */
218 201 public function remove_rule() {
219 - global $wp_filesystem;
220 - // Initialize the WP filesystem, no more using 'file-put-contents' function.
221 - if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
222 - require_once ABSPATH . '/wp-admin/includes/file.php';
223 - WP_Filesystem();
224 - }
202 + $wp_filesystem = $this->get_wp_filesystem();
225 203
226 204 if ( ! $wp_filesystem->exists( $this->robots_file ) ) {
227 205 return;
228 206 }
@@ -255,22 +233,10 @@
255 233 * @param string $uri The URI that was accessed.
256 234 * @param string $scenario The scenario under which the event is logged.
257 235 */
258 236 public function log_event( $ip, $uri, $scenario ) {
259 - $model = new Lockout_Log();
260 - $model->ip = $ip;
261 - $user_agent = defender_get_data_from_request( 'HTTP_USER_AGENT', 's' );
262 - $model->user_agent = isset( $user_agent ) ? User_Agent::fast_cleaning( $user_agent ) : null;
263 - $model->date = time();
264 - $model->tried = $uri;
265 - $model->blog_id = get_current_blog_id();
237 + $model = Lockout_Log::create( $ip, $uri );
266 238
267 - $ip_to_country = $this->ip_to_country( $ip );
268 -
269 - if ( ! empty( $ip_to_country ) && isset( $ip_to_country['iso'] ) ) {
270 - $model->country_iso_code = $ip_to_country['iso'];
271 - }
272 -
273 239 switch ( $scenario ) {
274 240 case self::SCENARIO_MALICIOUS_BOT:
275 241 default:
276 242 $model->type = Lockout_Log::LOCKOUT_MALICIOUS_BOT;
@@ -277,8 +243,10 @@
277 243 $model->log = esc_html__( 'Lockout occurred: Bot ignored robots.txt rules.', 'defender-security' );
278 244 break;
279 245 }
280 246 $model->save();
247 + // Notify directly because the 'defender_notify' hook isn't registered yet at firewall-time.
248 + wd_di()->get( Firewall_Notification::class )->notify_lockout( $model );
281 249 }
282 250
283 251 /**
284 252 * Creates a lockout for a blocked IP.
@@ -290,6 +258,78 @@
290 258 public function create_blocked_lockout( &$model, $message, $time ) {
291 259 $model->lockout_message = $message;
292 260 $model->release_time = $time;
293 261 $model->save();
262 + }
263 +
264 + /**
265 + * Merge the rule into existing User-agent: * block if present,
266 + * otherwise prepends a new block.
267 + *
268 + * @param string $contents The existing robots.txt content.
269 + *
270 + * @return string The modified robots.txt content with Defender rule added.
271 + */
272 + public function add_rules_to_content( string $contents ): string {
273 + if ( preg_match( self::USER_AGENT_REGEX, $contents ) ) {
274 + $disallow_path = $this->get_hash();
275 + $defender_rule = "{$this->start_comment}Disallow: /$disallow_path{$this->end_comment}";
276 + return preg_replace(
277 + self::USER_AGENT_REGEX,
278 + '$1' . PHP_EOL . $defender_rule,
279 + $contents,
280 + 1
281 + );
282 +
283 + }
284 + return $this->get_block( PHP_EOL ) . $contents;
285 + }
286 +
287 + /**
288 + * Detect if there's an empty Disallow line in robots.txt.
289 + *
290 + * @return bool True if empty Disallow line is detected, false otherwise.
291 + */
292 + public function has_empty_disallow_line(): bool {
293 + $contents = '';
294 + $wp_filesystem = $this->get_wp_filesystem();
295 +
296 + // Check physical robots.txt file first.
297 + if ( $wp_filesystem->exists( $this->robots_file ) ) {
298 + $contents = $wp_filesystem->get_contents( $this->robots_file );
299 + } elseif ( $this->smartcrawl->should_serve_robots() ) {
300 + // Get SmartCrawl virtual robots.txt content.
301 + $contents = $this->smartcrawl->get_robots_content();
302 + } else {
303 + // Get WordPress virtual robots.txt content.
304 + $contents = apply_filters( 'robots_txt', '', false );
305 + }
306 +
307 + if ( ! is_string( $contents ) || '' === trim( $contents ) ) {
308 + return false;
309 + }
310 +
311 + // Pattern to match "Disallow:" followed by optional whitespace and end of line (empty value).
312 + // Allows for optional leading whitespace before "Disallow:".
313 + $pattern = '/^\s*Disallow:\s*$/mi';
314 + if ( ! preg_match( $pattern, $contents ) ) {
315 + return false;
316 + }
317 +
318 + // Make sure it's not within our managed block.
319 + $defender_block_pattern = '/' . preg_quote( $this->start_comment, '/' ) .
320 + '.*?' . preg_quote( $this->end_comment, '/' ) . '/s';
321 + $contents_without_defender = preg_replace( $defender_block_pattern, '', $contents );
322 +
323 + return preg_match( $pattern, $contents_without_defender ) === 1;
324 + }
325 +
326 + /**
327 + * Registers a rewrite rule for the malicious bot URL.
328 + * The URL will be in the format: /{hash}/
329 + * where {hash} is a 16-character hexadecimal string.
330 + */
331 + public function register_rewrite_rule() {
332 + $hash = $this->get_hash();
333 + add_rewrite_rule( "^{$hash}/?$", 'index.php?' . self::URL_QUERY . '=' . $hash, 'top' );
294 334 }
295 335 }