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

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

292 lines 7.8 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 return class_exists( '\SmartCrawl\Modules\Advanced\Robots\Controller' ) ||
191 function_exists( 'smartcrawl_get_robots_url' ) ||
192 is_plugin_active( 'smartcrawl-seo/wpmu-dev-seo.php' );
193 }
194
195 /**
196 * Add bot trap rules to SmartCrawl's robots.txt content.
197 *
198 * @param string $content The existing robots.txt content from SmartCrawl.
199 * @return string The modified robots.txt content with bot trap rules.
200 */
201 public function add_bot_trap_to_smartcrawl_robots( $content ): string {
202 if ( ! $this->is_enabled() ) {
203 return $content;
204 }
205
206 $bot_trap_block = $this->get_block();
207
208 return $content . PHP_EOL . PHP_EOL . $bot_trap_block;
209 }
210
211 /**
212 * Remove the bot trap rule from the robots.txt.
213 */
214 public function remove_rule() {
215 global $wp_filesystem;
216 // Initialize the WP filesystem, no more using 'file-put-contents' function.
217 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
218 require_once ABSPATH . '/wp-admin/includes/file.php';
219 WP_Filesystem();
220 }
221
222 if ( ! $wp_filesystem->exists( $this->robots_file ) ) {
223 return;
224 }
225
226 $contents = $wp_filesystem->get_contents( $this->robots_file );
227 $eol = $this->detect_line_ending( $contents );
228
229 // Remove only the plugin block.
230 $pattern = '/\\s*' . preg_quote( $this->start_comment, '/' ) .
231 '.*?' . preg_quote( $this->end_comment, '/' ) . '\\s*/s';
232
233 $new_contents = preg_replace( $pattern, '', $contents );
234 $new_contents = trim( $new_contents );
235
236 if ( '' === $new_contents ) {
237 // File only had plugin block — delete it.
238 wp_delete_file( $this->robots_file );
239 } else {
240 $dir_name = pathinfo( $this->robots_file, PATHINFO_DIRNAME );
241 if ( $wp_filesystem->is_writable( $dir_name ) ) {
242 $wp_filesystem->put_contents( $this->robots_file, $new_contents . $eol );
243 }
244 }
245 }
246
247 /**
248 * Log the event into db, we will use the data in logs page later.
249 *
250 * @param string $ip The IP address involved in the event.
251 * @param string $uri The URI that was accessed.
252 * @param string $scenario The scenario under which the event is logged.
253 */
254 public function log_event( $ip, $uri, $scenario ) {
255 $model = new Lockout_Log();
256 $model->ip = $ip;
257 $user_agent = defender_get_data_from_request( 'HTTP_USER_AGENT', 's' );
258 $model->user_agent = isset( $user_agent ) ? User_Agent::fast_cleaning( $user_agent ) : null;
259 $model->date = time();
260 $model->tried = $uri;
261 $model->blog_id = get_current_blog_id();
262
263 $ip_to_country = $this->ip_to_country( $ip );
264
265 if ( ! empty( $ip_to_country ) && isset( $ip_to_country['iso'] ) ) {
266 $model->country_iso_code = $ip_to_country['iso'];
267 }
268
269 switch ( $scenario ) {
270 case self::SCENARIO_MALICIOUS_BOT:
271 default:
272 $model->type = Lockout_Log::LOCKOUT_MALICIOUS_BOT;
273 $model->log = esc_html__( 'Lockout occurred: Bot ignored robots.txt rules.', 'defender-security' );
274 break;
275 }
276 $model->save();
277 }
278
279 /**
280 * Creates a lockout for a blocked IP.
281 *
282 * @param Lockout_Ip $model The lockout IP model.
283 * @param string $message The lockout message.
284 * @param int $time The timestamp when the lockout will be lifted.
285 */
286 public function create_blocked_lockout( &$model, $message, $time ) {
287 $model->lockout_message = $message;
288 $model->release_time = $time;
289 $model->save();
290 }
291 }
292