get_routes(); $flagged = array(); foreach ( $routes as $route => $handlers ) { if ( 0 === strpos( $route, '/wp/v2' ) || 0 === strpos( $route, '/oembed' ) || 0 === strpos( $route, '/batch' ) ) { continue; } foreach ( $handlers as $handler ) { $permission = $handler['permission_callback'] ?? null; $methods = is_array( $handler['methods'] ?? null ) ? array_keys( array_filter( $handler['methods'] ) ) : (array) ( $handler['methods'] ?? array() ); $is_wide_open = ( null === $permission ) || '__return_true' === $permission; $accepts_write = (bool) array_intersect( array( 'POST', 'PUT', 'PATCH', 'DELETE' ), $methods ); if ( $is_wide_open && $accepts_write ) { $flagged[] = array( 'route' => $route, 'methods' => $methods, 'plugin' => self::owning_plugin( $handler['callback'] ?? null ), ); } } } update_option( 'argus_wpd_rest_inventory', array( 'checked_at' => current_time( 'mysql', true ), 'flagged' => $flagged, 'total_routes' => count( $routes ) ), false ); foreach ( $flagged as $route_info ) { Argus_Findings::record( 'rest_inventory', 'low', array( 'what_happened' => sprintf( 'Unauthenticated, state-changing REST route found: %s (%s)', $route_info['route'], implode( '/', $route_info['methods'] ) ), 'why_it_matters' => 'A route that accepts POST/PUT/PATCH/DELETE with no permission check is reachable by anyone, including automated scanners -- this is often intentional (e.g. a public contact-form endpoint) but worth a quick review.', 'what_argus_found' => sprintf( 'Route registered by: %s.', $route_info['plugin'] ?: 'unknown' ), 'when_it_happened' => current_time( 'mysql' ), 'why_suspicious' => 'This is an inventory finding, not a detected attack -- ARGUS does not know what this endpoint actually does, only that nothing stops an unauthenticated caller from reaching it.', 'what_could_be_affected' => 'Depends entirely on what the endpoint does -- review its plugin\'s documentation or source.', 'what_should_you_do' => 'If this endpoint is meant to be public, no action is needed. If not, check whether the owning plugin has a setting to require authentication, or contact its developer.', ), array( 'route' => $route_info['route'], 'methods' => $route_info['methods'], 'plugin' => $route_info['plugin'] ) ); } return $flagged; } public static function owning_plugin( $callback ) { try { if ( is_array( $callback ) && is_object( $callback[0] ?? null ) ) { $ref = new ReflectionClass( $callback[0] ); } elseif ( is_string( $callback ) && function_exists( $callback ) ) { $ref = new ReflectionFunction( $callback ); } elseif ( $callback instanceof Closure ) { $ref = new ReflectionFunction( $callback ); } else { return null; } $file = $ref->getFileName(); if ( ! $file ) { return null; } $rel = str_replace( wp_normalize_path( WP_PLUGIN_DIR ) . '/', '', wp_normalize_path( $file ) ); return explode( '/', $rel )[0] ?? null; } catch ( ReflectionException $e ) { return null; } } }