HEX
Server: Apache/2.4.58 (Ubuntu)
System: Linux wordpress 6.8.0-88-generic #89-Ubuntu SMP PREEMPT_DYNAMIC Sat Oct 11 01:02:46 UTC 2025 x86_64
User: www-data (33)
PHP: 8.3.28
Disabled: NONE
Upload Files
File: /var/www/html/wp-content/plugins/wp-fail2ban/core.php
<?php declare(strict_types=1);
/**
 * WP fail2ban core
 *
 * @package wp-fail2ban
 * @since   4.4.0   Require PHP 7.4
 * @since   4.3.0
 */
namespace org\lecklider\charles\wordpress\wp_fail2ban\core;

use org\lecklider\charles\wordpress\wp_fail2ban\Config;
use org\lecklider\charles\wordpress\wp_fail2ban\InvalidIpException;
use org\lecklider\charles\wordpress\wp_fail2ban\IpRangeList;
use org\lecklider\charles\wordpress\wp_fail2ban\IP;
use org\lecklider\charles\wordpress\wp_fail2ban\Syslog;

use function org\lecklider\charles\wordpress\wp_fail2ban\bail;

defined( 'ABSPATH' ) or exit;

/**
 * Compute remote IP address
 *
 * @since  5.0.0
 *
 * @return IP|null
 *
 * @wp-f2b-hard Untrusted X-Forwarded-For header
 */
function _remote_addr(): ?IP {
	try {
		// Typical path first
		if ( ! defined( 'WP_FAIL2BAN_REMOTE_ADDR' ) ) {
			$ip = new IP( $_SERVER['REMOTE_ADDR'], true, 'REMOTE_ADDR' );

			if ( array_key_exists( 'HTTP_X_FORWARDED_FOR', $_SERVER ) ) {
				/**
				 * User-defined proxies, typically upstream nginx
				 */
				$proxies = new IpRangeList( Config::get( 'WP_FAIL2BAN_PROXIES' ), 'WP_FAIL2BAN_PROXIES' );

				if ( 0 == count( $proxies ) ) {
					// No proxies set; don't care about the header
					return $ip;

				} elseif ( $proxies->containsIP( $ip ) ) {
					// From a known proxy

					$xIPs = explode( ',', $_SERVER['HTTP_X_FORWARDED_FOR'], 2 );
					return new IP( $xIPs[0], true, 'HTTP_X_FORWARDED_FOR' );

				} else {
					// Not a known proxy: hard fail and bail out
					Syslog::single( LOG_NOTICE, 'Untrusted X-Forwarded-For header', 'WP_FAIL2BAN_AUTH_LOG', (string) $ip );

					do_action( __FUNCTION__ . '.unknown_proxy', $ip );

					bail();
				}
			} else {
				return $ip;
			}
		} else {
			/**
			 * For plugins and themes that anonymise requests
			 *
			 * @since 5.0.0 Refactored
			 * @since 3.6.0
			 */
			return new IP( WP_FAIL2BAN_REMOTE_ADDR, true, 'WP_FAIL2BAN_REMOTE_ADDR' ); // @codeCoverageIgnore
		}
	} catch ( InvalidIpException $e ) {
		error_log( sprintf( "%s is invalid: '%s'", $e->getMessage(), $_SERVER['REMOTE_ADDR'] ) );
		\wp_die(
			'Internal server error',
			'Internal server error',
			array(
				'exit'     => ! defined( 'PHPUNIT_COMPOSER_INSTALL' ),
				'response' => 500,
			)
		);
	}

	return null;
}

/**
 * Cache results
 *
 * @since  5.0.0    Complete refactoring
 *
 * @return IP|null
 */
function remote_addr(): ?IP {
	static $remote_addr = null;

	if ( is_null( $remote_addr ) ) {
		$remote_addr = _remote_addr();
	}

	return $remote_addr;
}

/**
 * Catch empty usernames
 *
 * @see \wp_authenticate()
 *
 * @since  4.4.0    Add type hints
 * @since  4.3.4.0  Refactor to use Syslog::single
 * @since  4.3.0
 *
 * @param  mixed|null $user
 * @param  string     $username
 * @param  string     $password
 *
 * @return mixed|null
 *
 * @wp-f2b-soft Empty username
 */
function authenticate( $user, string $username, string $password ) {
	// : ?mixed
	if ( empty( $username ) && isset( $_POST['log'] ) ) {
		Syslog::single( LOG_NOTICE, 'Empty username' );

		do_action( __FUNCTION__, $user, $username, $password );
	}

	return $user;
}

/**
 * Hook: wp_login
 *
 * @since  4.4.0    Add type hints, return type
 * @since  4.3.4.0  Refactor to use Syslog::single
 * @since  4.3.0    Add action
 * @since  4.1.0    Add REST support
 * @since  3.5.0    Refactored for unit testing
 * @since  1.0.0
 *
 * @param  string $user_login
 * @param  mixed  $user
 *
 * @return void
 *
 * @wp-f2b-good Accepted password for .*
 *
 * @codeCoverageIgnore
 */
function wp_login( string $user_login, $user ): void {
	Syslog::single( LOG_INFO, "Accepted password for {$user_login}" );

	do_action( __FUNCTION__, $user_login, $user );
}

/**
 * Hook: wp_login_failed
 *
 * @since  4.4.0    Add type hints, return type
 * @since  4.3.4.0  Refactor to use Syslog::single
 * @since  4.3.0.5  Handle empty username
 * @since  4.3.0    Add action
 * @since  4.2.4    Add message filter
 * @since  4.2.0    Change username check
 * @since  4.1.0    Add REST support
 * @since  3.5.0    Refactored for unit testing
 * @since  1.0.0
 *
 * @param  string $username
 *
 * @return void
 *
 * @wp-f2b-hard REST authentication attempt for unknown user .*
 * @wp-f2b-hard XML-RPC authentication attempt for unknown user .*
 * @wp-f2b-soft Authentication failure for .*
 * @wp-f2b-soft REST authentication failure for .*
 * @wp-f2b-soft XML-RPC authentication failure for .*
 * @wp-f2b-soft Authentication attempt for unknown user .*
 */
function wp_login_failed( string $username ): void {
	$username = trim( $username );

	if ( empty( $username ) ) {
		$msg    = 'Empty username';
		$filter = '::empty';

	} else {
		global $wp_xmlrpc_server;

		if ( defined( 'REST_REQUEST' ) ) {
			$msg    = 'REST a';
			$filter = '::REST';
		} elseif ( $wp_xmlrpc_server ) {
			$msg    = 'XML-RPC a';
			$filter = '::XML-RPC';
		} else {
			$msg    = 'A';
			$filter = '';
		}

		$msg .= ( wp_cache_get( $username, 'useremail' ) || wp_cache_get( sanitize_user( $username ), 'userlogins' ) )
				? "uthentication failure for {$username}"
				: "uthentication attempt for unknown user {$username}";
	}

	$msg = apply_filters( "wp_fail2ban::wp_login_failed{$filter}", $msg );

	Syslog::single( LOG_NOTICE, $msg );

	do_action( __FUNCTION__, $username );
}