Files
argus-wp-defence/includes/class-argus-rest-inventory.php
T
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

87 lines
3.3 KiB
PHP

<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
class Argus_REST_Inventory {
public static function snapshot() {
if ( ! function_exists( 'rest_get_server' ) ) {
return;
}
$server = rest_get_server();
$routes = $server->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;
}
}
}