Automatic WordPress security: local firewall, malware and file-integrity scanning, vulnerability protection, quarantine, scheduled backups, an optional page cache, and automatic global threat intelligence. See README.md for installation, update, and uninstall instructions.
96 lines
3.4 KiB
PHP
96 lines
3.4 KiB
PHP
<?php
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
class Argus_API_Guard {
|
|
|
|
public static function init() {
|
|
add_filter( 'xmlrpc_methods', array( __CLASS__, 'strip_pingback' ) );
|
|
add_action( 'rest_api_init', array( __CLASS__, 'guard_user_enumeration' ) );
|
|
}
|
|
|
|
public static function strip_pingback( $methods ) {
|
|
if ( ! Argus_Settings::get( 'xmlrpc_block_pingback', true ) ) {
|
|
return $methods;
|
|
}
|
|
unset( $methods['pingback.ping'], $methods['pingback.extensions.getPingbacks'] );
|
|
return $methods;
|
|
}
|
|
|
|
public static function guard_user_enumeration() {
|
|
if ( ! Argus_Settings::get( 'rest_api_protection_enabled', true ) ) {
|
|
return;
|
|
}
|
|
|
|
add_filter(
|
|
'rest_pre_dispatch',
|
|
function ( $result, $server, $request ) {
|
|
if ( is_user_logged_in() ) {
|
|
return $result;
|
|
}
|
|
$route = $request->get_route();
|
|
if ( 0 !== strpos( $route, '/wp/v2/users' ) ) {
|
|
return $result;
|
|
}
|
|
|
|
$ip = Argus_Request_Inputs::client_ip();
|
|
|
|
if ( Argus_Ban_Engine::is_banned( $ip ) ) {
|
|
Argus_Policy_Engine::deny_already_banned( $ip );
|
|
return $result;
|
|
}
|
|
|
|
$recent = self::recent_user_enumeration_hits( $ip );
|
|
|
|
if ( $recent >= 10 ) {
|
|
|
|
$decision = Argus_Policy_Engine::evaluate( $ip, 'rest_abuse', array( 'route' => $route, 'recent_hits' => $recent ) );
|
|
|
|
Argus_Findings::record(
|
|
'account',
|
|
'high',
|
|
array(
|
|
|
|
'what_happened' => sprintf( 'Unauthenticated user-enumeration probing from %s targeting %s', $ip, $route ),
|
|
'why_it_matters' => 'Repeated unauthenticated requests to the users endpoint are a well-known reconnaissance step -- enumerating valid usernames to fuel a subsequent brute-force run.',
|
|
'what_argus_found' => sprintf( '%d unauthenticated requests to %s from %s. Action taken: %s (%s).', $recent, $route, $ip, $decision['action'], $decision['observation_only'] ? 'observed only, MONITOR mode' : 'enforced' ),
|
|
'when_it_happened' => current_time( 'mysql' ),
|
|
'why_suspicious' => 'A real client has no reason to request this endpoint repeatedly without authenticating -- this pattern is consistent with an automated recon/scanning tool.',
|
|
'what_could_be_affected' => 'Enumerated usernames are commonly fed into a follow-up brute-force or credential-stuffing attack against wp-login.php.',
|
|
'what_should_you_do' => $decision['observation_only']
|
|
? 'ARGUS is in MONITOR mode and did not block this. Review recent activity and switch to BLOCK mode once you are confident legitimate traffic is not being flagged.'
|
|
: 'No action needed -- ARGUS already blocked this IP.',
|
|
),
|
|
array( 'ip' => $ip, 'route' => $route, 'recent_hits' => $recent, 'decision' => $decision )
|
|
);
|
|
|
|
Argus_Policy_Engine::enforce_decision( $ip, 'rest_abuse', $decision, array( 'route' => $route, 'recent_hits' => $recent ) );
|
|
} else {
|
|
Argus_Events::record( 'rest_user_enum_probe', 'low', 'Unauthenticated request to ' . $route, array( 'route' => $route ), $ip );
|
|
}
|
|
|
|
return $result;
|
|
},
|
|
10,
|
|
3
|
|
);
|
|
}
|
|
|
|
protected static function recent_user_enumeration_hits( $ip ) {
|
|
global $wpdb;
|
|
$table = Argus_DB::table( 'events' );
|
|
$since = gmdate( 'Y-m-d H:i:s', time() - 300 );
|
|
|
|
return (int) $wpdb->get_var(
|
|
$wpdb->prepare(
|
|
"SELECT COUNT(*) FROM {$table} WHERE event_type = %s AND ip = %s AND created_at >= %s", // phpcs:ignore
|
|
'rest_user_enum_probe',
|
|
$ip,
|
|
$since
|
|
)
|
|
);
|
|
}
|
|
}
|