✏️ 编辑:class-user-agent.php
路径:
/home/forge/kingkrunch.com/wp/wp-content/plugins/wp-defender/src/component/class-user-agent.php
大小:10.9 KB · 修改:2025-10-22 00:15:46 · 权限:0644 · 可写
← 返回目录
👁 查看
⬇ 下载
<?php /** * Handles User-Agent based operations including lockouts and logging for security purposes. * * @package WP_Defender\Component */ namespace WP_Defender\Component; use WP_Defender\Component; use WP_Defender\Traits\Country; use WP_Defender\Model\Lockout_Ip; use WP_Defender\Model\Lockout_Log; use WP_Defender\Model\Setting\User_Agent_Lockout; use WP_Defender\Model\Notification\Firewall_Notification; use WP_Filesystem_Base; /** * Handles User-Agent based operations including lockouts and logging for security purposes. * * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent * @since 2.6.0 */ class User_Agent extends Component { use Country; public const SCENARIO_USER_AGENT_LOCKOUT = 'user_agent_lockout'; public const REASON_BAD_USER_AGENT = 'bad_user_agent', REASON_BAD_POST = 'bad_post'; /** * Human Readable text denotes user agent header is empty. */ public const EMPTY_USER_AGENT_TEXT = 'Empty User Agent'; public const GO_HTTP_CLIENT_KEY = 'go-http-client'; public const PYTHON_REQUESTS_KEY = 'python-requests'; /** * Use for cache. * * @var User_Agent_Lockout */ protected $model; /** * Lockout IP model instance. * * @var Lockout_Ip */ protected $lockout_ip_model; /** * Initializes the User_Agent component with necessary models. */ public function __construct() { $this->model = wd_di()->get( User_Agent_Lockout::class ); $this->lockout_ip_model = wd_di()->get( Lockout_Ip::class ); } /** * Logs a user agent event into the database. * * @param string $ip The IP address associated with the event. * @param string $user_agent The user agent string associated with the event. * @param string $reason The reason for the event. */ private function log_event( $ip, $user_agent, $reason ) { $model = new Lockout_Log(); $model->ip = $ip; $model->user_agent = $user_agent; $model->date = time(); $model->tried = $user_agent; $model->blog_id = get_current_blog_id(); $model->type = Lockout_Log::LOCKOUT_UA; $ip_to_country = $this->ip_to_country( $ip ); if ( isset( $ip_to_country['iso'] ) ) { $model->country_iso_code = $ip_to_country['iso']; } switch ( $reason ) { case self::REASON_BAD_POST: // Distinguish between different block cases of User agent lockouts. $model->tried = self::REASON_BAD_POST; $model->log = esc_html__( 'Locked out due to empty User-Agent and Referer headers', 'wpdef' ); break; case self::REASON_BAD_USER_AGENT: default: $model->tried = $user_agent; $model->log = esc_html__( 'Locked out due to attempted login with banned user agent', 'wpdef' ); break; } $model->save(); // The 'defender_notify' hook doesn't work, so send notify directly. $module = wd_di()->get( Firewall_Notification::class ); if ( $module->check_options( $model ) ) { $module->send( $model ); } } /** * Queue hooks when this class init. */ public function add_hooks() { } /** * Checks if the User_Agent component is active. * * @return bool Returns true if the component is active, false otherwise. */ public function is_active_component(): bool { return $this->model->is_active() && ! is_admin(); } /** * Determines if the provided user agent is considered bad. * * @param string $user_agent The user agent to check. * * @return bool Returns true if the user agent is bad, false otherwise. */ public function is_bad_user_agent( $user_agent ): bool { $allowlist = str_replace( '#', '\#', $this->model->get_lockout_list( 'allowlist' ) ); $allowlist_regex_pattern = '#' . implode( '|', $allowlist ) . '#i'; $allowlist_match = preg_match( $allowlist_regex_pattern, $user_agent ); if ( count( $allowlist ) > 0 && ! empty( $allowlist_match ) ) { return false; } $blocklist = str_replace( '#', '\#', $this->model->get_all_selected_blocklist_ua() ); $blocklist_regex_pattern = '#' . implode( '|', $blocklist ) . '#i'; $blocklist_match = preg_match( $blocklist_regex_pattern, $user_agent ); if ( count( $blocklist ) > 0 && ! empty( $blocklist_match ) ) { return true; } return false; } /** * Retrieves the message to display for blocked user agents. * * @return string The block message. */ public function get_message(): string { return ! empty( $this->model->message ) ? $this->model->message : esc_html__( 'You have been blocked from accessing this website.', 'wpdef' ); } /** * Blocks a user agent or IP and logs the event. * * @param string $user_agent The user agent to block. * @param string $ip The IP address to block. * @param string $reason The reason for blocking. */ public function block_user_agent_or_ip( $user_agent, $ip, $reason ) { // since 2.6.0. do_action( 'wd_user_agent_before_block', $user_agent, $ip, $reason ); $this->log_event( $ip, $user_agent, $reason ); do_action( 'wd_user_agent_lockout', $this->model, self::SCENARIO_USER_AGENT_LOCKOUT ); // Shouldn't block IP via hook 'wd_blacklist_this_ip', block only when the button 'Ban IP' is clicked. if ( defender_is_wp_org_version() ) { Rate::run_counter_of_ua_lockouts(); } } /** * Cleans a user agent string quickly. * * @param string $user_agent The user agent string to clean. * * @return string The cleaned user agent string. */ public static function fast_cleaning( $user_agent ): string { return trim( sanitize_text_field( $user_agent ) ); } /** * Sanitize User Agent. * * @return string */ public function sanitize_user_agent(): string { $user_agent = defender_get_data_from_request( 'HTTP_USER_AGENT', 's' ); if ( empty( $user_agent ) ) { return ''; } $user_agent = apply_filters( 'wd_current_user_agent', $user_agent ); $user_agent = self::fast_cleaning( $user_agent ); $user_agent = strtolower( $user_agent ); return $user_agent; } /** * Checks if the POST request has blank User-Agent and Referer headers. * * @param string $user_agent The user agent of the request. * * @return bool Returns true if the headers are considered bad, false otherwise. */ public function is_bad_post( $user_agent ): bool { $server = defender_get_data_from_request( null, 's' ); return true === $this->model->empty_headers && 'POST' === $server['REQUEST_METHOD'] && empty( $user_agent ) && empty( $server['HTTP_REFERER'] ); } /** * Verifies the format and usability of an import file for User Agent Lockout settings. * * @param string $file The file path to verify. * * @return array|bool Returns the data if the file is valid, false otherwise. */ public function verify_import_file( $file ) { global $wp_filesystem; // Initialize the WP filesystem, no more using 'file-put-contents' function. if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) { require_once ABSPATH . '/wp-admin/includes/file.php'; WP_Filesystem(); } $contents = $wp_filesystem->get_contents( $file ); $lines = explode( "\n", $contents ); $data = array(); foreach ( $lines as $line ) { if ( '' === $line ) { continue; } $line = str_getcsv( $line, ',', '"', '\\' ); if ( count( $line ) !== 2 ) { return false; } if ( ! in_array( $line[1], array( 'allowlist', 'blocklist' ), true ) ) { return false; } $ua = $line[0]; $ua = self::fast_cleaning( $ua ); if ( '' === $ua ) { continue; } $line[0] = $ua; $data[] = $line; } return $data; } /** * Get human readable user agent log status text. * * @param string $log_type Type of the log. Handles on 'ua_lockout'. * @param string $user_agent User Agent name. * * @return string Human-readable text if log_type is UA else empty string. */ public function get_status_text( $log_type, $user_agent ): string { if ( ! in_array( $log_type, Lockout_Log::get_ua_lockout_types(), true ) ) { return ''; } $status_text = self::EMPTY_USER_AGENT_TEXT; if ( self::REASON_BAD_POST === $user_agent ) { return $status_text; } $user_agent_key = $this->model->get_access_status( $user_agent ); if ( ! empty( $user_agent_key[0] ) ) { $status_text = $this->lockout_ip_model->get_access_status_text( $user_agent_key[0] ); } return $status_text; } /** * Get Blocklist presets. * * @return array */ public static function get_blocklist_presets(): array { return array( 'brute_forcing_tools' => array( 'feroxbuster' => 'Feroxbuster', 'gobuster' => 'Gobuster', ), 'security_scanners' => array( 'sqlmap' => 'SQLMap', 'wfuzz' => 'Wfuzz', ), 'seo_crawlers' => array( 'dotbot' => 'DotBot (Moz)', 'mj12bot' => 'MJ12Bot (Majestic)', 'ahrefsbot' => 'AhrefsBot', 'semrushbot' => 'SEMrushBot', ), ); } /** * Get only keys of nested Blocklist preset arrays. * * @return array */ public static function get_nested_keys_of_blocklist_presets(): array { $all_keys = array(); $presets = self::get_blocklist_presets(); foreach ( $presets as $category => $tools ) { foreach ( $tools as $key => $value ) { $all_keys[] = $key; } } return $all_keys; } /** * Is the current UA in the Blocklist preset list? * * @param string $key User Agent key. * * @return bool */ public static function is_blocklist_presets( $key ): bool { return in_array( $key, self::get_nested_keys_of_blocklist_presets(), true ); } /** * Get Script presets. * * @return array */ public static function get_script_presets(): array { return array( self::PYTHON_REQUESTS_KEY => array( 'label' => 'Python Script', 'desc' => __( '( This will block all requests from python-requests/* agent )', 'wpdef' ), ), self::GO_HTTP_CLIENT_KEY => array( 'label' => 'Go Http Clients', 'desc' => __( '( This will block all requests from Go-http-client/* agent )', 'wpdef' ), ), ); } /** * Is the current UA in the Script preset list? * * @param string $key User Agent key. * * @return bool */ public static function is_script_presets( $key ): bool { return in_array( $key, array_keys( self::get_script_presets() ), true ); } /** * Check and remove duplicates in passed UA array. * * @param array $arr_source Source array. * @param array $arr_search Search array. * * @return array */ public static function check_and_remove_duplicates( $arr_source, $arr_search ): array { foreach ( $arr_search as $ua ) { $key = array_search( $ua, $arr_source, true ); if ( false !== $key ) { unset( $arr_source[ $key ] ); } } return ! empty( $arr_source ) ? array_values( $arr_source ) : array(); } }
💾 保存
取消
保存为 UTF-8,换行统一为 LF