Plugin Directory

Changeset 3733310


Ignore:
Timestamp:
10/07/2026 08:39:00 PM (12 days ago)
Author:
potar
Message:

Release 1.1.14: harden Bot Block exploit detection and document protection behavior

Location:
site-first-seo
Files:
6 edited
1 copied

Legend:

Unmodified
Added
Removed
  • site-first-seo/tags/1.1.14/includes/class-sfs-botblock.php

    r3706009 r3733310  
    177177        $ua      = self::server( 'HTTP_USER_AGENT', '' );
    178178        $ip      = self::client_ip();
    179         $method  = self::server( 'REQUEST_METHOD', 'GET' );
     179        $method  = strtoupper( self::server( 'REQUEST_METHOD', 'GET' ) );
    180180        $referer = self::server( 'HTTP_REFERER', '' );
    181181        $body     = self::classify_request_body( $method );
    182182        $behavior = self::track_ip_behavior( $ip, $path, $ua );
    183         $crawler  = self::crawler_classification( $ua, $ip, $behavior );
     183        $exploit_rules = self::common_exploit_rules( self::server( 'REQUEST_URI', $path ) );
     184        // Do not perform crawler DNS lookups for requests already known to be probes.
     185        $crawler  = self::crawler_classification( $ua, $ip, $behavior, empty( $exploit_rules ) || ! (int) get_option( self::OPT_BLOCK_EXPLOITS, 1 ) );
    184186
    185187        // 1) Deterministically block sensitive-file probes when protection is enabled.
    186         if ( (int) get_option( self::OPT_BLOCK_EXPLOITS, 1 ) && self::is_common_exploit_path( $path ) ) {
    187             self::log( 'hard_block', 'common_exploit', 100, $ua, $ip, $path, $method, $referer, 403, 0, array( 'common_exploit_path' ), $body, $crawler );
     188        if ( (int) get_option( self::OPT_BLOCK_EXPLOITS, 1 ) && ! empty( $exploit_rules ) ) {
     189            self::log( 'hard_block', 'common_exploit', 100, $ua, $ip, $path, $method, $referer, 403, 0, $exploit_rules, $body, $crawler );
    188190            status_header( 403 );
    189191            exit;
    … …  
    202204
    203205        // 3) Verified search crawlers and configured allowlists
    204         if ( (int) get_option( self::OPT_ALLOW_SEARCH, 1 ) && $crawler === 'trusted_search' ) {
     206        if ( (int) get_option( self::OPT_ALLOW_SEARCH, 1 ) && $crawler === 'trusted_search' && in_array( $method, array( 'GET', 'HEAD' ), true ) ) {
    205207            self::schedule_log( 'allow', 'trusted_crawler', 0, $ua, $ip, $path, $method, $referer, array( 'verified_search_crawler' ), $body, $crawler );
    206208            return;
    … …  
    208210
    209211        $allow = self::build_allowlist();
     212        // A declared bot identity does not authorize submitting data or write requests.
     213        if ( ! in_array( $method, array( 'GET', 'HEAD' ), true ) ) { $allow['ua'] = array(); }
    210214        if ( self::matches_allow( $ua, $ip, $path, $allow ) ) {
    211215            self::schedule_log( 'allow', 'allowlist', 0, $ua, $ip, $path, $method, $referer, array( 'allowlist' ), $body, $crawler );
    … …  
    270274        $normalized = rawurldecode( rawurldecode( $path ) );
    271275        $normalized = str_replace( '\\', '/', $normalized );
     276        $normalized = str_replace( "\0", '', $normalized );
    272277        $normalized = preg_replace( '#/+#', '/', $normalized );
     278        // Windows alternate data streams can follow an otherwise sensitive filename.
     279        $normalized = preg_replace( '#::\\$DATA(?=/|$)#i', '', $normalized );
    273280        if ( ! is_string( $normalized ) ) { return false; }
    274281
    275282        $patterns = array(
    276283            '#(?:^|/)\.git(?:/|$)#i',
     284            '#(?:^|/)\.git(?:ignore|modules|attributes)(?:[.~_-][^/]*)?(?:/|$)#i',
    277285            '#(?:^|/)\.(?:svn|hg)(?:/|$)#i',
    278286            '#(?:^|/)\.env(?:[._-][^/]*)?(?:/|$)#i',
    279287            '#(?:^|/)(?:wp-config\.php|configuration\.php)(?:[.~_-][^/]*)?$#i',
    280288            '#(?:^|/)(?:config|configuration)/(?:secrets?|credentials?|database)(?:\.[^/]*)?$#i',
    281             '#(?:^|/)\.(?:aws|ssh)/(?:credentials|config|id_rsa)(?:\.[^/]*)?$#i',
     289            '#(?:^|/)\.aws(?:/|$)#i',
     290            '#(?:^|/)\.ssh/(?:config|authorized_keys|known_hosts|id_(?:rsa|dsa|ecdsa|ed25519))(?:\.[^/]*)?(?:/|$)#i',
     291            '#(?:^|/)\.(?:azure|config/gcloud)(?:/|$)#i',
     292            '#(?:^|/)(?:alfacgiapi|alfa_data)(?:\.(?:php[0-9]?|cgi|pl|txt)(?:[.~_-][^/]*)?)?(?:/|$)#i',
     293            '#(?:^|/)perl\.alfa(?:\.(?:php[0-9]?|cgi|pl|txt))?(?:/|$)#i',
     294            '#(?:^|/)(?:php-cgi(?:\.exe)?|cgi-bin/php(?:[0-9]+(?:\.[0-9]+)*)?(?:-cgi)?(?:\.exe)?)(?:/|$)#i',
    282295            '#(?:^|/)(?:composer\.(?:json|lock)|auth\.json)$#i',
    283296            '#(?:^|/)vendor/phpunit/phpunit/src/util/php/eval-stdin\.php$#i',
    … …  
    288301        }
    289302        return false;
     303    }
     304
     305    /** Match paths and CGI command-line injection before any request allowlisting. */
     306    protected static function common_exploit_rules( string $uri ): array {
     307        $parts = explode( '?', $uri, 2 );
     308        $rules = self::is_common_exploit_path( $parts[0] ) ? array( 'common_exploit_path' ) : array();
     309        $query = str_replace( '+', ' ', rawurldecode( rawurldecode( $parts[1] ?? '' ) ) );
     310        // Encoded soft hyphens are also used as CGI option delimiters on Windows.
     311        $query = str_replace( array( "\xC2\xAD", "\xAD" ), '-', $query );
     312        if ( preg_match( '#(?:^|[\s&])-d\s*(?:allow_url_include|auto_prepend_file|cgi\.force_redirect|cgi\.redirect_status_env|disable_functions)\s*=#i', $query ) ) {
     313            $rules[] = 'php_cgi_argument_injection';
     314        }
     315        return $rules;
    290316    }
    291317
    … …  
    430456            $score += 5;
    431457            $rules[] = 'probe_method';
     458            if ( $hits > 10 && $ua_count > 3 ) {
     459                $score += 15;
     460                $rules[] = 'rotating_probe_burst';
     461            }
    432462        }
    433463        if ( in_array( $method, array( 'PUT','PATCH','DELETE' ), true ) ) {
    … …  
    444474                $rules[] = 'post_without_referer';
    445475            }
    446             if ( $crawler === 'declared_search' || $crawler === 'declared_ai' || $crawler === 'declared_bot' ) {
     476            if ( $crawler === 'trusted_search' || $crawler === 'declared_search' || $crawler === 'declared_ai' || $crawler === 'declared_bot' ) {
    447477                $score += 20;
    448478                $rules[] = 'crawler_post_request';
    … …  
    664694                        <h2><?php esc_html_e( 'Protection & Allow Toggles', 'site-first-seo' ); ?></h2>
    665695                        <p><label><input type="checkbox" name="block_common_exploits" value="1" <?php checked( $blockE, 1 ); ?> /> <strong><?php esc_html_e( 'Block Common Exploits', 'site-first-seo' ); ?></strong></label></p>
    666                         <p class="description"><?php esc_html_e( 'Immediately returns 403 for probes targeting .git, .env, credentials, backup configuration files, and other sensitive development files. This protection applies in every Bot Block mode.', 'site-first-seo' ); ?></p>
     696                        <p class="description"><?php esc_html_e( 'Immediately returns 403 for probes targeting .git, .env, cloud credentials, backup configuration files, ALFA shell paths, and PHP-CGI injection attempts, including encoded and Windows alternate-data-stream variants. This protection applies in every Bot Block mode.', 'site-first-seo' ); ?></p>
    667697                        <p class="description"><?php esc_html_e( 'Behavior scoring also watches one-minute IP velocity, repeated paths, rotating user agents, suspicious POST requests, and unexpected write methods.', 'site-first-seo' ); ?></p>
    668698                        <p><label><input type="checkbox" name="allow_search" value="1" <?php checked( $allowS, 1 ); ?> /> <?php esc_html_e( 'Allow verified search crawlers', 'site-first-seo' ); ?></label></p>
    … …  
    956986            'ua_count'  => max( 1, min( 100, isset( $_POST['sim_ua_count'] ) ? absint( $_POST['sim_ua_count'] ) : 1 ) ),
    957987        );
    958         $crawler = self::crawler_classification( $ua, $ip, $behavior );
    959 
    960         if ( (int) get_option( self::OPT_BLOCK_EXPLOITS, 1 ) && self::is_common_exploit_path( $path ) ) {
     988        $exploit_rules = self::common_exploit_rules( $path );
     989        $path = explode( '?', $path, 2 )[0];
     990        $crawler = self::crawler_classification( $ua, $ip, $behavior, empty( $exploit_rules ) || ! (int) get_option( self::OPT_BLOCK_EXPLOITS, 1 ) );
     991
     992        if ( (int) get_option( self::OPT_BLOCK_EXPLOITS, 1 ) && ! empty( $exploit_rules ) ) {
    961993            $result = array(
    962994                'score'    => 100,
    963                 'rules'    => array( 'common_exploit_path' ),
     995                'rules'    => $exploit_rules,
    964996                'crawler'  => $crawler,
    965997                'decision' => 'hard_block',
    … …  
    14271459    }
    14281460
    1429     protected static function crawler_classification( string $ua, string $ip, array $behavior ): string {
     1461    protected static function crawler_classification( string $ua, string $ip, array $behavior, bool $verify_search = true ): string {
    14301462        if ( trim( $ua ) === '' || $ua === '-' ) { return 'empty_ua'; }
    14311463
    14321464        if ( preg_match( '#(googlebot|googleother|google-inspectiontool|storebot-google|bingbot|duckduckbot|applebot|yandexbot)#i', $ua ) ) {
    1433             return self::is_verified_search_crawler( $ua, $ip ) ? 'trusted_search' : 'declared_search';
     1465            return $verify_search && self::is_verified_search_crawler( $ua, $ip ) ? 'trusted_search' : 'declared_search';
    14341466        }
    14351467        if ( preg_match( '#(gptbot|chatgpt-user|ccbot|claudebot|anthropic-ai|perplexitybot|google-extended|cohere-ai|bytespider)#i', $ua ) ) {
  • site-first-seo/tags/1.1.14/readme.txt

    r3706009 r3733310  
    55Requires at least: 6.0
    66Tested up to: 7.0
    7 Stable tag: 1.1.13
     7Stable tag: 1.1.14
    88Requires PHP: 8.0
    99License: GPLv2 or later
    … …  
    4343* Module Controls: enables or disables major modules from a central settings screen.
    4444
     45= Bot Block protection and traffic monitoring =
     46
     47Bot Block offers Monitor, Soft block (429), and Hard block (403) modes. Monitor records suspicious behavior without enforcing score-based blocks. Enable Block Common Exploits to immediately reject recognized sensitive-file and exploit probes in every mode, before crawler and user allowlists are evaluated.
     48
     49Exploit protection covers source-control metadata, .env files, cloud credentials, configuration backups, ALFA shell paths, PHP-CGI executable probes, and CGI argument-injection queries. Detection handles encoded paths, backslash separators, and Windows ::$DATA variants. Known exploit probes do not trigger crawler DNS lookups.
     50
     51Google, Bing, and Apple search crawler identities use cached reverse and forward DNS verification. Automatic crawler and user-agent allowances apply to GET and HEAD requests. POST and other write methods receive behavioral scoring unless a safe endpoint or an explicit path/IP allowance applies. Scoring watches request velocity, repeated paths, rotating user agents, suspicious POST requests, and rotating HEAD/OPTIONS probe bursts.
     52
     53Activity logs and CSV exports retain decisions, scores, matched rules, crawler classifications, request body types, and observed response status. Request body contents are not stored. The rule simulator supports exploit paths and PHP-CGI query probes. Cache-aware browser beacons remain available, with aggregate totals shown separately from PHP-visible bot/human percentages.
     54
     55Protection applies to requests reaching WordPress; files served directly by the web server or CDN require server-level protection. A declared crawler User-Agent is not proof of crawler identity, and logged requests do not represent all cached traffic.
     56
    4557= Local and entity SEO =
    4658
    … …  
    108120
    109121== Changelog ==
     122
     123= 1.1.14 =
     124* Expanded Bot Block protection for ALFA shell paths, PHP-CGI argument injection, cloud credentials, and encoded Windows alternate-data-stream probes before allowlisting.
     125* Avoided crawler DNS lookups for known exploit probes and retained existing request logging and verified search crawler handling.
     126* Restricted user-agent allowlisting to GET/HEAD requests and strengthened scoring for rotating HEAD/OPTIONS probe bursts.
    110127
    111128= 1.1.13 =
    … …  
    204221== Upgrade Notice ==
    205222
     223= 1.1.14 =
     224Hardens Bot Block exploit detection and probe handling while preserving cache-aware browser beacons and existing log settings.
     225
    206226= 1.1.13 =
    207227Adds cache-aware browser beacons to help Bot Block account for full-page cached browser traffic without changing the existing PHP-visible bot/human metric.
  • site-first-seo/tags/1.1.14/site-first-seo.php

    r3706009 r3733310  
    33 * Plugin Name: Site-First SEO
    44 * Description: On-site SEO and GEO toolkit (Dashboard/Visits, Headings & Images, Titles & Meta + SERP preview, Links Overview, AI Readiness, Indexation Monitor, Content Decay, Internal Link Manager, Local SEO Schema, Sitemaps, Robots.txt, llms.txt, Redirects & 404s, Exports, Bot Block, Settings).
    5  * Version: 1.1.13
     5 * Version: 1.1.14
    66 * Author: potar
    77 * Requires at least: 6.0
    … …  
    1818define('SFSEO_BOOTSTRAPPED', true);
    1919
    20 if (!defined('SFSEO_VER'))  define('SFSEO_VER', '1.1.13'); // keep in sync with header
     20if (!defined('SFSEO_VER'))  define('SFSEO_VER', '1.1.14'); // keep in sync with header
    2121if (!defined('SFSEO_CAP'))  define('SFSEO_CAP', 'manage_options');
    2222if (!defined('SFSEO_FILE')) define('SFSEO_FILE', __FILE__);
  • site-first-seo/trunk/includes/class-sfs-botblock.php

    r3706009 r3733310  
    177177        $ua      = self::server( 'HTTP_USER_AGENT', '' );
    178178        $ip      = self::client_ip();
    179         $method  = self::server( 'REQUEST_METHOD', 'GET' );
     179        $method  = strtoupper( self::server( 'REQUEST_METHOD', 'GET' ) );
    180180        $referer = self::server( 'HTTP_REFERER', '' );
    181181        $body     = self::classify_request_body( $method );
    182182        $behavior = self::track_ip_behavior( $ip, $path, $ua );
    183         $crawler  = self::crawler_classification( $ua, $ip, $behavior );
     183        $exploit_rules = self::common_exploit_rules( self::server( 'REQUEST_URI', $path ) );
     184        // Do not perform crawler DNS lookups for requests already known to be probes.
     185        $crawler  = self::crawler_classification( $ua, $ip, $behavior, empty( $exploit_rules ) || ! (int) get_option( self::OPT_BLOCK_EXPLOITS, 1 ) );
    184186
    185187        // 1) Deterministically block sensitive-file probes when protection is enabled.
    186         if ( (int) get_option( self::OPT_BLOCK_EXPLOITS, 1 ) && self::is_common_exploit_path( $path ) ) {
    187             self::log( 'hard_block', 'common_exploit', 100, $ua, $ip, $path, $method, $referer, 403, 0, array( 'common_exploit_path' ), $body, $crawler );
     188        if ( (int) get_option( self::OPT_BLOCK_EXPLOITS, 1 ) && ! empty( $exploit_rules ) ) {
     189            self::log( 'hard_block', 'common_exploit', 100, $ua, $ip, $path, $method, $referer, 403, 0, $exploit_rules, $body, $crawler );
    188190            status_header( 403 );
    189191            exit;
    … …  
    202204
    203205        // 3) Verified search crawlers and configured allowlists
    204         if ( (int) get_option( self::OPT_ALLOW_SEARCH, 1 ) && $crawler === 'trusted_search' ) {
     206        if ( (int) get_option( self::OPT_ALLOW_SEARCH, 1 ) && $crawler === 'trusted_search' && in_array( $method, array( 'GET', 'HEAD' ), true ) ) {
    205207            self::schedule_log( 'allow', 'trusted_crawler', 0, $ua, $ip, $path, $method, $referer, array( 'verified_search_crawler' ), $body, $crawler );
    206208            return;
    … …  
    208210
    209211        $allow = self::build_allowlist();
     212        // A declared bot identity does not authorize submitting data or write requests.
     213        if ( ! in_array( $method, array( 'GET', 'HEAD' ), true ) ) { $allow['ua'] = array(); }
    210214        if ( self::matches_allow( $ua, $ip, $path, $allow ) ) {
    211215            self::schedule_log( 'allow', 'allowlist', 0, $ua, $ip, $path, $method, $referer, array( 'allowlist' ), $body, $crawler );
    … …  
    270274        $normalized = rawurldecode( rawurldecode( $path ) );
    271275        $normalized = str_replace( '\\', '/', $normalized );
     276        $normalized = str_replace( "\0", '', $normalized );
    272277        $normalized = preg_replace( '#/+#', '/', $normalized );
     278        // Windows alternate data streams can follow an otherwise sensitive filename.
     279        $normalized = preg_replace( '#::\\$DATA(?=/|$)#i', '', $normalized );
    273280        if ( ! is_string( $normalized ) ) { return false; }
    274281
    275282        $patterns = array(
    276283            '#(?:^|/)\.git(?:/|$)#i',
     284            '#(?:^|/)\.git(?:ignore|modules|attributes)(?:[.~_-][^/]*)?(?:/|$)#i',
    277285            '#(?:^|/)\.(?:svn|hg)(?:/|$)#i',
    278286            '#(?:^|/)\.env(?:[._-][^/]*)?(?:/|$)#i',
    279287            '#(?:^|/)(?:wp-config\.php|configuration\.php)(?:[.~_-][^/]*)?$#i',
    280288            '#(?:^|/)(?:config|configuration)/(?:secrets?|credentials?|database)(?:\.[^/]*)?$#i',
    281             '#(?:^|/)\.(?:aws|ssh)/(?:credentials|config|id_rsa)(?:\.[^/]*)?$#i',
     289            '#(?:^|/)\.aws(?:/|$)#i',
     290            '#(?:^|/)\.ssh/(?:config|authorized_keys|known_hosts|id_(?:rsa|dsa|ecdsa|ed25519))(?:\.[^/]*)?(?:/|$)#i',
     291            '#(?:^|/)\.(?:azure|config/gcloud)(?:/|$)#i',
     292            '#(?:^|/)(?:alfacgiapi|alfa_data)(?:\.(?:php[0-9]?|cgi|pl|txt)(?:[.~_-][^/]*)?)?(?:/|$)#i',
     293            '#(?:^|/)perl\.alfa(?:\.(?:php[0-9]?|cgi|pl|txt))?(?:/|$)#i',
     294            '#(?:^|/)(?:php-cgi(?:\.exe)?|cgi-bin/php(?:[0-9]+(?:\.[0-9]+)*)?(?:-cgi)?(?:\.exe)?)(?:/|$)#i',
    282295            '#(?:^|/)(?:composer\.(?:json|lock)|auth\.json)$#i',
    283296            '#(?:^|/)vendor/phpunit/phpunit/src/util/php/eval-stdin\.php$#i',
    … …  
    288301        }
    289302        return false;
     303    }
     304
     305    /** Match paths and CGI command-line injection before any request allowlisting. */
     306    protected static function common_exploit_rules( string $uri ): array {
     307        $parts = explode( '?', $uri, 2 );
     308        $rules = self::is_common_exploit_path( $parts[0] ) ? array( 'common_exploit_path' ) : array();
     309        $query = str_replace( '+', ' ', rawurldecode( rawurldecode( $parts[1] ?? '' ) ) );
     310        // Encoded soft hyphens are also used as CGI option delimiters on Windows.
     311        $query = str_replace( array( "\xC2\xAD", "\xAD" ), '-', $query );
     312        if ( preg_match( '#(?:^|[\s&])-d\s*(?:allow_url_include|auto_prepend_file|cgi\.force_redirect|cgi\.redirect_status_env|disable_functions)\s*=#i', $query ) ) {
     313            $rules[] = 'php_cgi_argument_injection';
     314        }
     315        return $rules;
    290316    }
    291317
    … …  
    430456            $score += 5;
    431457            $rules[] = 'probe_method';
     458            if ( $hits > 10 && $ua_count > 3 ) {
     459                $score += 15;
     460                $rules[] = 'rotating_probe_burst';
     461            }
    432462        }
    433463        if ( in_array( $method, array( 'PUT','PATCH','DELETE' ), true ) ) {
    … …  
    444474                $rules[] = 'post_without_referer';
    445475            }
    446             if ( $crawler === 'declared_search' || $crawler === 'declared_ai' || $crawler === 'declared_bot' ) {
     476            if ( $crawler === 'trusted_search' || $crawler === 'declared_search' || $crawler === 'declared_ai' || $crawler === 'declared_bot' ) {
    447477                $score += 20;
    448478                $rules[] = 'crawler_post_request';
    … …  
    664694                        <h2><?php esc_html_e( 'Protection & Allow Toggles', 'site-first-seo' ); ?></h2>
    665695                        <p><label><input type="checkbox" name="block_common_exploits" value="1" <?php checked( $blockE, 1 ); ?> /> <strong><?php esc_html_e( 'Block Common Exploits', 'site-first-seo' ); ?></strong></label></p>
    666                         <p class="description"><?php esc_html_e( 'Immediately returns 403 for probes targeting .git, .env, credentials, backup configuration files, and other sensitive development files. This protection applies in every Bot Block mode.', 'site-first-seo' ); ?></p>
     696                        <p class="description"><?php esc_html_e( 'Immediately returns 403 for probes targeting .git, .env, cloud credentials, backup configuration files, ALFA shell paths, and PHP-CGI injection attempts, including encoded and Windows alternate-data-stream variants. This protection applies in every Bot Block mode.', 'site-first-seo' ); ?></p>
    667697                        <p class="description"><?php esc_html_e( 'Behavior scoring also watches one-minute IP velocity, repeated paths, rotating user agents, suspicious POST requests, and unexpected write methods.', 'site-first-seo' ); ?></p>
    668698                        <p><label><input type="checkbox" name="allow_search" value="1" <?php checked( $allowS, 1 ); ?> /> <?php esc_html_e( 'Allow verified search crawlers', 'site-first-seo' ); ?></label></p>
    … …  
    956986            'ua_count'  => max( 1, min( 100, isset( $_POST['sim_ua_count'] ) ? absint( $_POST['sim_ua_count'] ) : 1 ) ),
    957987        );
    958         $crawler = self::crawler_classification( $ua, $ip, $behavior );
    959 
    960         if ( (int) get_option( self::OPT_BLOCK_EXPLOITS, 1 ) && self::is_common_exploit_path( $path ) ) {
     988        $exploit_rules = self::common_exploit_rules( $path );
     989        $path = explode( '?', $path, 2 )[0];
     990        $crawler = self::crawler_classification( $ua, $ip, $behavior, empty( $exploit_rules ) || ! (int) get_option( self::OPT_BLOCK_EXPLOITS, 1 ) );
     991
     992        if ( (int) get_option( self::OPT_BLOCK_EXPLOITS, 1 ) && ! empty( $exploit_rules ) ) {
    961993            $result = array(
    962994                'score'    => 100,
    963                 'rules'    => array( 'common_exploit_path' ),
     995                'rules'    => $exploit_rules,
    964996                'crawler'  => $crawler,
    965997                'decision' => 'hard_block',
    … …  
    14271459    }
    14281460
    1429     protected static function crawler_classification( string $ua, string $ip, array $behavior ): string {
     1461    protected static function crawler_classification( string $ua, string $ip, array $behavior, bool $verify_search = true ): string {
    14301462        if ( trim( $ua ) === '' || $ua === '-' ) { return 'empty_ua'; }
    14311463
    14321464        if ( preg_match( '#(googlebot|googleother|google-inspectiontool|storebot-google|bingbot|duckduckbot|applebot|yandexbot)#i', $ua ) ) {
    1433             return self::is_verified_search_crawler( $ua, $ip ) ? 'trusted_search' : 'declared_search';
     1465            return $verify_search && self::is_verified_search_crawler( $ua, $ip ) ? 'trusted_search' : 'declared_search';
    14341466        }
    14351467        if ( preg_match( '#(gptbot|chatgpt-user|ccbot|claudebot|anthropic-ai|perplexitybot|google-extended|cohere-ai|bytespider)#i', $ua ) ) {
  • site-first-seo/trunk/readme.txt

    r3706009 r3733310  
    55Requires at least: 6.0
    66Tested up to: 7.0
    7 Stable tag: 1.1.13
     7Stable tag: 1.1.14
    88Requires PHP: 8.0
    99License: GPLv2 or later
    … …  
    4343* Module Controls: enables or disables major modules from a central settings screen.
    4444
     45= Bot Block protection and traffic monitoring =
     46
     47Bot Block offers Monitor, Soft block (429), and Hard block (403) modes. Monitor records suspicious behavior without enforcing score-based blocks. Enable Block Common Exploits to immediately reject recognized sensitive-file and exploit probes in every mode, before crawler and user allowlists are evaluated.
     48
     49Exploit protection covers source-control metadata, .env files, cloud credentials, configuration backups, ALFA shell paths, PHP-CGI executable probes, and CGI argument-injection queries. Detection handles encoded paths, backslash separators, and Windows ::$DATA variants. Known exploit probes do not trigger crawler DNS lookups.
     50
     51Google, Bing, and Apple search crawler identities use cached reverse and forward DNS verification. Automatic crawler and user-agent allowances apply to GET and HEAD requests. POST and other write methods receive behavioral scoring unless a safe endpoint or an explicit path/IP allowance applies. Scoring watches request velocity, repeated paths, rotating user agents, suspicious POST requests, and rotating HEAD/OPTIONS probe bursts.
     52
     53Activity logs and CSV exports retain decisions, scores, matched rules, crawler classifications, request body types, and observed response status. Request body contents are not stored. The rule simulator supports exploit paths and PHP-CGI query probes. Cache-aware browser beacons remain available, with aggregate totals shown separately from PHP-visible bot/human percentages.
     54
     55Protection applies to requests reaching WordPress; files served directly by the web server or CDN require server-level protection. A declared crawler User-Agent is not proof of crawler identity, and logged requests do not represent all cached traffic.
     56
    4557= Local and entity SEO =
    4658
    … …  
    108120
    109121== Changelog ==
     122
     123= 1.1.14 =
     124* Expanded Bot Block protection for ALFA shell paths, PHP-CGI argument injection, cloud credentials, and encoded Windows alternate-data-stream probes before allowlisting.
     125* Avoided crawler DNS lookups for known exploit probes and retained existing request logging and verified search crawler handling.
     126* Restricted user-agent allowlisting to GET/HEAD requests and strengthened scoring for rotating HEAD/OPTIONS probe bursts.
    110127
    111128= 1.1.13 =
    … …  
    204221== Upgrade Notice ==
    205222
     223= 1.1.14 =
     224Hardens Bot Block exploit detection and probe handling while preserving cache-aware browser beacons and existing log settings.
     225
    206226= 1.1.13 =
    207227Adds cache-aware browser beacons to help Bot Block account for full-page cached browser traffic without changing the existing PHP-visible bot/human metric.
  • site-first-seo/trunk/site-first-seo.php

    r3706009 r3733310  
    33 * Plugin Name: Site-First SEO
    44 * Description: On-site SEO and GEO toolkit (Dashboard/Visits, Headings & Images, Titles & Meta + SERP preview, Links Overview, AI Readiness, Indexation Monitor, Content Decay, Internal Link Manager, Local SEO Schema, Sitemaps, Robots.txt, llms.txt, Redirects & 404s, Exports, Bot Block, Settings).
    5  * Version: 1.1.13
     5 * Version: 1.1.14
    66 * Author: potar
    77 * Requires at least: 6.0
    … …  
    1818define('SFSEO_BOOTSTRAPPED', true);
    1919
    20 if (!defined('SFSEO_VER'))  define('SFSEO_VER', '1.1.13'); // keep in sync with header
     20if (!defined('SFSEO_VER'))  define('SFSEO_VER', '1.1.14'); // keep in sync with header
    2121if (!defined('SFSEO_CAP'))  define('SFSEO_CAP', 'manage_options');
    2222if (!defined('SFSEO_FILE')) define('SFSEO_FILE', __FILE__);
Note: See TracChangeset for help on using the changeset viewer.