HEX
Server: nginx
System: Linux pool195-106-36.bur.atomicsites.net 6.12.57+deb12-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.57-1~bpo12+1 (2025-11-17) x86_64
User: (0)
PHP: 8.3.32
Disabled: pcntl_fork
Upload Files
File: //wordpress/mu-plugins/wpcloud-bot-protection/wpcloud-bot-protection.php
<?php
/**
 * Plugin Name: WP Cloud Bot Protection (Blackbox)
 * Description: Gates wp-login.php authentication on a Blackbox verdict.
 * Version:     0.1.0
 * Author:      Bastion
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'WP_Cloud_Bot_Protection' ) ) {

	class WP_Cloud_Bot_Protection {

		const JS_URL   = 'https://blackbox-api.wp.com/v1/dist/v.js';
		const API_BASE = 'https://blackbox-api.wp.com';
		const TIMEOUT  = 2.0;
		const FIELD    = 'wpcloud_bb_session';

		const VERDICT_ALLOW     = 'allow';
		const VERDICT_CHALLENGE = 'challenge';
		const VERDICT_BLOCK     = 'block';
		const VERDICT_ERROR     = 'error';

		private static $verdict_cache = [];

		public static function boot(): void {
			add_action( 'login_enqueue_scripts', [ __CLASS__, 'enqueue_client' ] );
			add_action( 'login_form', [ __CLASS__, 'render_form_hooks' ] );

			add_action( 'lostpassword_form', [ __CLASS__, 'render_form_hooks' ] );
			add_action( 'lostpassword_post', [ __CLASS__, 'gate_password_reset' ], 10, 2 );

			add_filter( 'authenticate', [ __CLASS__, 'gate_authentication' ], 40, 3 );
		}

		public static function enqueue_client(): void {
			if ( ! self::configured() ) {
				return;
			}

			wp_enqueue_script(
				'wpcloud-blackbox-client',
				self::JS_URL,
				[],
				null,
				[
					'in_footer' => true,
					'strategy'  => 'defer',
				]
			);

			$bootstrap_version = filemtime( __DIR__ . '/assets/bootstrap.js' );
			if ( ! $bootstrap_version ) {
				$bootstrap_version = '0.1.0';
			}

			wp_enqueue_script(
				'wpcloud-blackbox-bootstrap',
				plugins_url( 'assets/bootstrap.js', __FILE__ ),
				[ 'wpcloud-blackbox-client' ],
				(string) $bootstrap_version,
				[
					'in_footer' => true,
					'strategy'  => 'defer',
				]
			);

			wp_add_inline_script(
				'wpcloud-blackbox-bootstrap',
				sprintf(
					'window.__wpcloudBlackbox = { apiKey: %s, field: %s };',
					wp_json_encode( self::public_key() ),
					wp_json_encode( self::FIELD )
				),
				'before'
			);
		}

		public static function render_form_hooks(): void {
			if ( ! self::configured() ) {
				return;
			}

			printf(
				'<input type="hidden" name="%1$s" id="%1$s" value="" />',
				esc_attr( self::FIELD )
			);
			echo '<div id="wpcloud-blackbox-challenge" style="margin:1em 0;"></div>';
		}

		public static function gate_authentication( $user, $username, $password ) {
			if ( ! self::configured() ) {
				return $user;
			}

			if ( ! self::is_login_form_post() ) {
				return $user;
			}

			if ( '' === $username || null === $username ) {
				return $user;
			}

			$verdict = self::verdict_for(
				self::session_id_from_post(),
				$username,
				[
					'action'       => 'wp_login',
					'login_method' => 'password',
				]
			);

			switch ( $verdict ) {
				case self::VERDICT_BLOCK:
					return new WP_Error(
						'wpcloud_blackbox_blocked',
						__( '<strong>Error:</strong> Sign-in blocked. If this looks wrong, contact support.', 'wpcloud-bot-protection' )
					);

				case self::VERDICT_CHALLENGE:
					return new WP_Error( 'wpcloud_blackbox_challenge', self::challenge_message() );

				case self::VERDICT_ALLOW:
				case self::VERDICT_ERROR:
				default:
					return $user;
			}
		}

		public static function gate_password_reset( $errors, $user_data = null ): void {
			if ( ! self::configured() ) {
				return;
			}
			if ( ! is_wp_error( $errors ) ) {
				return;
			}

			if ( ! self::is_login_form_post() ) {
				return;
			}

			$verdict = self::verdict_for(
				self::session_id_from_post(),
				'lostpassword',
				[
					'action' => 'password_reset',
				]
			);

			switch ( $verdict ) {
				case self::VERDICT_BLOCK:
					$errors->add(
						'wpcloud_blackbox_blocked',
						__( '<strong>Error:</strong> Password reset blocked. If this looks wrong, contact support.', 'wpcloud-bot-protection' )
					);
					break;

				case self::VERDICT_CHALLENGE:
					$errors->add( 'wpcloud_blackbox_challenge', self::challenge_message() );
					break;

				case self::VERDICT_ALLOW:
				case self::VERDICT_ERROR:
				default:
					break;
			}
		}

		private static function verdict_for( string $session_id, string $discriminator, array $context ): string {
			$cache_key = $session_id . '|' . $discriminator;
			if ( ! isset( self::$verdict_cache[ $cache_key ] ) ) {
				self::$verdict_cache[ $cache_key ] = self::verify( $session_id, $context );
			}
			return self::$verdict_cache[ $cache_key ];
		}

		private static function session_id_from_post(): string {
			return isset( $_POST[ self::FIELD ] )
				? sanitize_text_field( wp_unslash( $_POST[ self::FIELD ] ) )
				: '';
		}

		private static function challenge_message(): string {
			return __( '<strong>Error:</strong> Please complete the verification step and try again.', 'wpcloud-bot-protection' );
		}

		private static function verify( string $session_id, array $context ): string {
			$url = '' !== $session_id
				? self::API_BASE . '/v1/verify/' . rawurlencode( $session_id )
				: self::API_BASE . '/v1/verify';

			$payload = [ 'context' => $context ];
			if ( '' === $session_id ) {
				$visitor_ip = self::visitor_ip();
				if ( '' !== $visitor_ip ) {
					$payload['visitor_ip'] = $visitor_ip;
				}
				$payload['full_headers'] = self::request_headers();
			}

			$request_body = (string) wp_json_encode( $payload );

			$response = wp_remote_post(
				$url,
				[
					'timeout' => self::TIMEOUT,
					'headers' => array_merge(
						[
							'Content-Type' => 'application/json',
							'Accept'       => 'application/json',
						],
						self::signed_auth_headers( 'POST', $url, $request_body )
					),
					'body'    => $request_body,
				]
			);

			if ( is_wp_error( $response ) ) {
				error_log( 'Bot Protection: verify transport error: ' . $response->get_error_message() );
				return self::VERDICT_ERROR;
			}

			$status = wp_remote_retrieve_response_code( $response );
			$body   = json_decode( wp_remote_retrieve_body( $response ), true );
			$data   = is_array( $body ) && isset( $body['data'] ) && is_array( $body['data'] ) ? $body['data'] : [];

			if ( 409 === $status ) {
				return isset( $data['decision'] )
					? self::normalize_decision( $data['decision'] )
					: self::VERDICT_ALLOW;
			}

			if ( $status < 200 || $status >= 300 ) {
				error_log( sprintf( 'Bot Protection: verify HTTP %d: %s', $status, wp_remote_retrieve_body( $response ) ) );
				return self::VERDICT_ERROR;
			}

			return self::normalize_decision( $data['decision'] ?? '' );
		}

		private static function visitor_ip(): string {
			$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? wp_unslash( $_SERVER['REMOTE_ADDR'] ) : '';
			return is_string( $ip ) && false !== filter_var( $ip, FILTER_VALIDATE_IP ) ? $ip : '';
		}

		private static function request_headers(): array {
			if ( function_exists( 'getallheaders' ) ) {
				$headers = getallheaders();
				return is_array( $headers ) ? $headers : [];
			}

			$headers = [];
			foreach ( $_SERVER as $name => $value ) {
				if ( 0 === strpos( (string) $name, 'HTTP_' ) ) {
					$key             = str_replace( ' ', '-', ucwords( strtolower( str_replace( '_', ' ', substr( $name, 5 ) ) ) ) );
					$headers[ $key ] = $value;
				}
			}
			return $headers;
		}

		private static function normalize_decision( $decision ): string {
			$decision = is_string( $decision ) ? strtolower( $decision ) : '';
			$valid    = [ self::VERDICT_ALLOW, self::VERDICT_CHALLENGE, self::VERDICT_BLOCK, self::VERDICT_ERROR ];
			return in_array( $decision, $valid, true ) ? $decision : self::VERDICT_ERROR;
		}

		private static function is_login_form_post(): bool {
			return 'POST' === ( $_SERVER['REQUEST_METHOD'] ?? '' )
				&& isset( $GLOBALS['pagenow'] )
				&& 'wp-login.php' === $GLOBALS['pagenow'];
		}

		private static function site_id(): string {
			return defined( 'ATOMIC_SITE_ID' ) ? (string) constant( 'ATOMIC_SITE_ID' ) : '';
		}

		private static function site_api_key(): string {
			return defined( 'ATOMIC_SITE_API_KEY' ) ? (string) constant( 'ATOMIC_SITE_API_KEY' ) : '';
		}

		private static function public_key(): string {
			return 'wpcloud:' . self::site_id();
		}

		private static function signed_auth_headers( string $method, string $url, string $body ): array {
			$timestamp = (string) time();
			$nonce     = bin2hex( random_bytes( 16 ) );
			$path      = (string) wp_parse_url( $url, PHP_URL_PATH );
			$canonical = implode( "\n", [ $timestamp, $nonce, strtoupper( $method ), $path, hash( 'sha256', $body ) ] );
			$signature = hash_hmac( 'sha256', $canonical, self::site_api_key() );

			return [
				'X-WPCLOUD-Site-Id'   => self::site_id(),
				'X-WPCLOUD-Timestamp' => $timestamp,
				'X-WPCLOUD-Nonce'     => $nonce,
				'X-WPCLOUD-Signature' => $signature,
			];
		}

		private static function configured(): bool {
			return '' !== self::site_id() && '' !== self::site_api_key();
		}
	}

	WP_Cloud_Bot_Protection::boot();
}