Files
argus-wp-defence/includes/class-argus-api-guard.php
root df0f2fccb8 ARGUS WordPress Defence 1.0.0 — first production release
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.
2026-08-09 13:40:16 +00:00

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
)
);
}
}