$hook_obj ) { if ( 0 !== strpos( (string) $hook_name, 'wp_ajax_nopriv_' ) ) { continue; } $action = substr( (string) $hook_name, strlen( 'wp_ajax_nopriv_' ) ); $plugin = self::owning_plugin_for_hook( $hook_obj ); $nopriv_actions[] = array( 'action' => $action, 'plugin' => $plugin ); } update_option( 'argus_wpd_ajax_inventory', array( 'checked_at' => current_time( 'mysql', true ), 'nopriv_actions' => $nopriv_actions ), false ); if ( ! empty( $nopriv_actions ) ) { Argus_Findings::record( 'ajax_inventory', 'info', array( 'what_happened' => 'Unauthenticated-reachable AJAX actions inventoried', 'why_it_matters' => 'wp_ajax_nopriv_* handlers are callable by anyone, logged in or not -- this is a completely normal, widely-used WordPress mechanism (contact forms, AJAX search, and many other legitimate features all use it), not inherently a problem.', 'what_argus_found' => sprintf( '%d action(s) found: %s', count( $nopriv_actions ), implode( ', ', array_map( function ( $a ) { return $a['action'] . ( $a['plugin'] ? ' (' . $a['plugin'] . ')' : '' ); }, $nopriv_actions ) ) ), 'when_it_happened' => current_time( 'mysql' ), 'why_suspicious' => 'This is an inventory, not a detection -- shown so you know your site\'s full unauthenticated attack surface in one place.', 'what_could_be_affected' => 'Depends on what each handler does -- review any you don\'t recognize.', 'what_should_you_do' => 'No action needed unless you see an action name you don\'t recognize from a plugin you don\'t remember installing.', ), array( 'nopriv_actions' => $nopriv_actions ) ); } return $nopriv_actions; } protected static function owning_plugin_for_hook( $hook_obj ) { if ( ! isset( $hook_obj->callbacks ) || ! is_array( $hook_obj->callbacks ) ) { return null; } foreach ( $hook_obj->callbacks as $priority_group ) { foreach ( $priority_group as $cb ) { $callback = $cb['function'] ?? null; $plugin = Argus_REST_Inventory::owning_plugin( $callback ); if ( $plugin ) { return $plugin; } } } return null; } }