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.
87 lines
3.3 KiB
PHP
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;
|
|
}
|
|
}
|
|
}
|