PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.7.2
Defender Security – Malware Scanner, Login Security & Firewall v5.7.2
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 / class-malicious-bot.php

class-malicious-bot.php in Defender Security – Malware Scanner, Login Security & Firewall 5.7.2, at src/component/class-malicious-bot.php

296 lines 8.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles malicious bot functionality.
4 *
5 * @package WP_Defender\Component
6 */
7
8 namespace WP_Defender\Component;
9
10 use WP_Defender\Component;
11 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;
15
16 /**
17 * Handles operations to insert a weekly rotating hash URL into the footer,
18 * and blocking IP addresses that access this URL.
19 */
20 class Malicious_Bot extends Component {
21 use Country;
22
23 public const URL_HASH_KEY = 'wpdef_malicious_bot_url_hash';
24 public const SCENARIO_MALICIOUS_BOT = 'malicious_bot';
25
26 /**
27 * The path to the robots.txt file.
28 *
29 * @var string
30 */
31 private $robots_file;
32
33 /**
34 * The start comment for the robots.txt.
35 *
36 * @var string
37 */
38 private $start_comment = '# WP Defender - Begin';
39
40 /**
41 * The end comment for the robots.txt.
42 *
43 * @var string
44 */
45 private $end_comment = '# WP Defender - End';
46
47 /**
48 * The model for handling the data.
49 *
50 * @var User_Agent_Lockout
51 */
52 protected $model;
53
54 /**
55 * Constructor for the Malicious_Bot class.
56 * Initializes the model and sets the path to the robots.txt file.
57 *
58 * @param User_Agent_Lockout $model The model instance for malicious bot functionality.
59 */
60 public function __construct( User_Agent_Lockout $model ) {
61 $this->model = $model;
62 $this->robots_file = ABSPATH . 'robots.txt';
63 }
64
65 /**
66 * Check if the malicious bot is enabled.
67 */
68 public function is_enabled(): bool {
69 return $this->model->enabled && $this->model->malicious_bot_enabled;
70 }
71
72 /**
73 * Get the hash for the malicious bot URL.
74 *
75 * @return string|false The hash if set, false otherwise.
76 */
77 public function get_hash() {
78 return get_site_option( self::URL_HASH_KEY, false );
79 }
80
81 /**
82 * Set the hash for the malicious bot URL.
83 *
84 * @param string $hash The hash to set, should be a 16-character hexadecimal string.
85 */
86 private function set_hash( string $hash ) {
87 update_site_option( self::URL_HASH_KEY, $hash );
88 }
89
90 /**
91 * Rotate the hash for the malicious bot URL.
92 */
93 public function rotate_hash() {
94 $new_hash = $this->generate_hash();
95 $this->set_hash( $new_hash );
96
97 flush_rewrite_rules();
98
99 $this->remove_rule();
100 $this->inject_rule();
101 }
102
103 /**
104 * Generate a new hash for the malicious bot URL.
105 *
106 * @return string|false A 16-character hexadecimal string or false on failure.
107 */
108 private function generate_hash() {
109 return substr( bin2hex( Crypt::random_bytes( 32 ) ), 0, 16 );
110 }
111
112 /**
113 * Handle the robots.txt rule.
114 */
115 public function handle_robots_txt() {
116 if ( $this->is_enabled() ) {
117 if ( $this->is_smartcrawl_active() ) {
118 add_filter( 'smartcrawl_robots_txt_content', array( $this, 'add_bot_trap_to_smartcrawl_robots' ) );
119 } else {
120 $this->inject_rule();
121 }
122 } else {
123 if ( $this->is_smartcrawl_active() ) {
124 remove_filter( 'smartcrawl_robots_txt_content', array( $this, 'add_bot_trap_to_smartcrawl_robots' ) );
125 }
126 $this->remove_rule();
127 }
128 }
129
130 /**
131 * Get rule block for robots.txt.
132 *
133 * @param string $eol The end-of-line character to use, defaults to "\n".
134 *
135 * @return string The block of text to be added to the robots.txt file.
136 */
137 public function get_block( string $eol = "\n" ): string {
138 $disallow_path = $this->get_hash();
139
140 return $this->start_comment . $eol .
141 'User-agent: *' . $eol .
142 "Disallow: /{$disallow_path}" . $eol .
143 $this->end_comment;
144 }
145
146 /**
147 * Inject the malicious bot rule into the robots.txt.
148 */
149 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 }
156
157 if ( $wp_filesystem->exists( $this->robots_file ) ) {
158 // Inject rule into physical robots.txt file.
159 $contents = $wp_filesystem->get_contents( $this->robots_file );
160 $eol = $this->detect_line_ending( $contents );
161
162 if ( strpos( $contents, $this->start_comment ) === false ) {
163 $block = $eol . $this->get_block( $eol ) . $eol;
164 $contents .= $block;
165
166 $dir_name = pathinfo( $this->robots_file, PATHINFO_DIRNAME );
167 if ( $wp_filesystem->is_writable( $dir_name ) ) {
168 $wp_filesystem->put_contents( $this->robots_file, $contents );
169 }
170 }
171 } else {
172 // 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 );
181 }
182 }
183
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 * Add bot trap rules to SmartCrawl's robots.txt content.
201 *
202 * @param string $content The existing robots.txt content from SmartCrawl.
203 * @return string The modified robots.txt content with bot trap rules.
204 */
205 public function add_bot_trap_to_smartcrawl_robots( $content ): string {
206 if ( ! $this->is_enabled() ) {
207 return $content;
208 }
209
210 $bot_trap_block = $this->get_block();
211
212 return $content . PHP_EOL . PHP_EOL . $bot_trap_block;
213 }
214
215 /**
216 * Remove the bot trap rule from the robots.txt.
217 */
218 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 }
225
226 if ( ! $wp_filesystem->exists( $this->robots_file ) ) {
227 return;
228 }
229
230 $contents = $wp_filesystem->get_contents( $this->robots_file );
231 $eol = $this->detect_line_ending( $contents );
232
233 // Remove only the plugin block.
234 $pattern = '/\\s*' . preg_quote( $this->start_comment, '/' ) .
235 '.*?' . preg_quote( $this->end_comment, '/' ) . '\\s*/s';
236
237 $new_contents = preg_replace( $pattern, '', $contents );
238 $new_contents = trim( $new_contents );
239
240 if ( '' === $new_contents ) {
241 // File only had plugin block — delete it.
242 wp_delete_file( $this->robots_file );
243 } else {
244 $dir_name = pathinfo( $this->robots_file, PATHINFO_DIRNAME );
245 if ( $wp_filesystem->is_writable( $dir_name ) ) {
246 $wp_filesystem->put_contents( $this->robots_file, $new_contents . $eol );
247 }
248 }
249 }
250
251 /**
252 * Log the event into db, we will use the data in logs page later.
253 *
254 * @param string $ip The IP address involved in the event.
255 * @param string $uri The URI that was accessed.
256 * @param string $scenario The scenario under which the event is logged.
257 */
258 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();
266
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 switch ( $scenario ) {
274 case self::SCENARIO_MALICIOUS_BOT:
275 default:
276 $model->type = Lockout_Log::LOCKOUT_MALICIOUS_BOT;
277 $model->log = esc_html__( 'Lockout occurred: Bot ignored robots.txt rules.', 'defender-security' );
278 break;
279 }
280 $model->save();
281 }
282
283 /**
284 * Creates a lockout for a blocked IP.
285 *
286 * @param Lockout_Ip $model The lockout IP model.
287 * @param string $message The lockout message.
288 * @param int $time The timestamp when the lockout will be lifted.
289 */
290 public function create_blocked_lockout( &$model, $message, $time ) {
291 $model->lockout_message = $message;
292 $model->release_time = $time;
293 $model->save();
294 }
295 }
296