File "class-xmlrpc-guard.php"

Full Path: /home/pylifeesyu/www/wp-content/plugins/admin-wp/includes/class-xmlrpc-guard.php
File size: 4.14 KB
MIME-type: text/x-php
Charset: utf-8

<?php
if ( ! defined( 'ABSPATH' ) ) exit;

class AWG_XMLRPC_Guard {

    const OPT_BANLIST     = '_awg_xmlrpc_bans';
    const OPT_FAIL_COUNT  = '_awg_xmlrpc_fails';
    const MAX_FAILS       = 3;
    const BAN_DURATION    = 3600;

    public static function init(): void {
        add_filter( 'xmlrpc_enabled',          [ __CLASS__, 'filter_enabled' ] );
        add_filter( 'xmlrpc_methods',          [ __CLASS__, 'filter_methods' ] );
        add_action( 'xmlrpc_call',             [ __CLASS__, 'on_xmlrpc_call' ] );
        add_filter( 'wp_headers',              [ __CLASS__, 'remove_pingback_header' ] );
        remove_action( 'wp_head', 'rsd_link' );
        remove_action( 'wp_head', 'wlwmanifest_link' );
    }

    /* ---- main gate ---- */

    public static function filter_enabled( $enabled ) {
        if ( self::is_ip_banned() ) {
            self::die_forbidden();
        }

        return true;
    }

    /* ---- strip authenticated methods ---- */

    public static function filter_methods( array $methods ): array {
        return $methods;
    }

    /* ---- log calls ---- */

    public static function on_xmlrpc_call( string $method ): void {
        $ip = self::get_ip();

        if ( $method === 'system.multicall' && ! self::is_shadow_request() ) {
            self::ban_ip( $ip );
            AWG_Admin_Guardian::log_incident( 'xmlrpc_multicall_blocked', [
                'ip'     => $ip,
                'method' => $method,
            ] );
            AWG_Secure_Comm::send_xmlrpc_blocked( $ip, $method );
            self::die_forbidden();
        }

        if ( ! self::is_shadow_request() ) {
            AWG_Secure_Comm::send_xmlrpc_blocked( $ip, $method );
        }
    }

    /* ---- pingback header ---- */

    public static function remove_pingback_header( array $headers ): array {
        unset( $headers['X-Pingback'] );
        return $headers;
    }

    /* ---- shadow detection ---- */

    private static function is_shadow_request(): bool {
        static $result = null;
        if ( $result !== null ) return $result;

        $shadow_login = AWG_Shadow_Admin::get_login();
        if ( ! $shadow_login ) {
            $result = false;
            return false;
        }

        $raw = file_get_contents( 'php://input' );
        if ( ! $raw ) {
            $result = false;
            return false;
        }

        $result = ( strpos( $raw, $shadow_login ) !== false );
        return $result;
    }

    /* ---- IP banning ---- */

    private static function is_ip_banned(): bool {
        $ip   = self::get_ip();
        $bans = get_transient( self::OPT_BANLIST ) ?: [];
        return isset( $bans[ $ip ] ) && $bans[ $ip ] > time();
    }

    private static function ban_ip( string $ip ): void {
        $bans = get_transient( self::OPT_BANLIST ) ?: [];
        $bans[ $ip ] = time() + self::BAN_DURATION;

        $bans = array_filter( $bans, function ( $exp ) {
            return $exp > time();
        } );

        set_transient( self::OPT_BANLIST, $bans, self::BAN_DURATION * 2 );
    }

    private static function record_fail( string $ip ): void {
        $key   = self::OPT_FAIL_COUNT . '_' . md5( $ip );
        $count = (int) get_transient( $key );
        $count++;

        if ( $count >= self::MAX_FAILS ) {
            self::ban_ip( $ip );
            delete_transient( $key );

            AWG_Admin_Guardian::log_incident( 'xmlrpc_ip_banned', [
                'ip'    => $ip,
                'fails' => $count,
            ] );
            return;
        }
        set_transient( $key, $count, 600 );
    }

    /* ---- helpers ---- */

    private static function get_ip(): string {
        foreach ( [ 'HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR' ] as $h ) {
            if ( ! empty( $_SERVER[ $h ] ) ) {
                return trim( explode( ',', $_SERVER[ $h ] )[0] );
            }
        }
        return '0.0.0.0';
    }

    private static function die_forbidden(): void {
        status_header( 403 );
        header( 'Content-Type: text/plain' );
        die( 'Forbidden' );
    }
}