2882 lines
91 KiB
PHP
2882 lines
91 KiB
PHP
<?php
|
|
/**
|
|
* General functions
|
|
*
|
|
* @package WPVulnerability
|
|
*
|
|
* @since 2.0.0
|
|
*/
|
|
|
|
defined( 'ABSPATH' ) || die( 'No script kiddies please!' );
|
|
|
|
/**
|
|
* Clear the existing cache for the specified type.
|
|
*
|
|
* @since 2.0.0
|
|
*
|
|
* @param string $type The type of cache to clear (core, plugins, themes).
|
|
* @return void
|
|
*/
|
|
function wpvulnerability_clear_cache( $type ) {
|
|
$additional_keys = array(
|
|
'core' => array( 'wpvulnerability-core-version' ),
|
|
'plugins' => array(
|
|
'wpvulnerability-plugins-signature',
|
|
'wpvulnerability-plugins-data',
|
|
'wpvulnerability-plugins-cache-data',
|
|
),
|
|
'themes' => array( 'wpvulnerability-themes-signature' ),
|
|
);
|
|
|
|
if ( is_multisite() ) {
|
|
delete_site_option( "wpvulnerability-{$type}" );
|
|
delete_site_option( "wpvulnerability-{$type}-vulnerable" );
|
|
delete_site_option( "wpvulnerability-{$type}-cache" );
|
|
|
|
if ( isset( $additional_keys[ $type ] ) ) {
|
|
foreach ( $additional_keys[ $type ] as $option_name ) {
|
|
delete_site_option( $option_name );
|
|
}
|
|
}
|
|
} else {
|
|
delete_option( "wpvulnerability-{$type}" );
|
|
delete_option( "wpvulnerability-{$type}-vulnerable" );
|
|
delete_option( "wpvulnerability-{$type}-cache" );
|
|
|
|
if ( isset( $additional_keys[ $type ] ) ) {
|
|
foreach ( $additional_keys[ $type ] as $option_name ) {
|
|
delete_option( $option_name );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks and validates user capabilities for managing vulnerability settings in a WordPress environment.
|
|
*
|
|
* This function verifies if the current user has the appropriate permissions to manage network settings
|
|
* in a multisite installation or manage options in a single site installation. It ensures that only
|
|
* Administrators in a single site and Super Administrators in multisite can access these settings.
|
|
*
|
|
* @since 3.0.0
|
|
*
|
|
* @return bool Returns true if the current user has the required capabilities, false otherwise.
|
|
*/
|
|
function wpvulnerability_capabilities() {
|
|
// Check if the user is logged in.
|
|
if ( ! is_user_logged_in() ) {
|
|
return false;
|
|
}
|
|
|
|
// Check if in a Multisite environment.
|
|
if ( is_multisite() && is_super_admin() && ( is_network_admin() || is_main_site() ) ) {
|
|
return true;
|
|
} elseif ( is_admin() && current_user_can( 'manage_options' ) ) {
|
|
return true;
|
|
}
|
|
|
|
// Return false if the user does not have the required capabilities.
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks if the `shell_exec` function can be used.
|
|
*
|
|
* This function implements a 4-level security check system:
|
|
* 1. Global disable via constant
|
|
* 2. Security mode (strict/standard/disabled)
|
|
* 3. Component-specific whitelist
|
|
* 4. PHP configuration check
|
|
*
|
|
* @since 3.4.0
|
|
* @since 4.3.0 Enhanced with 4-level security checks and component-specific control.
|
|
*
|
|
* @param string $component Optional. Component name for granular control.
|
|
*
|
|
* @return bool True if `shell_exec` is available and allowed, false otherwise.
|
|
*/
|
|
function wpvulnerability_can_shell_exec( $component = '' ) {
|
|
// Level 1: Global disable via constant.
|
|
if ( defined( 'WPVULNERABILITY_DISABLE_SHELL_EXEC' ) && WPVULNERABILITY_DISABLE_SHELL_EXEC ) {
|
|
return false;
|
|
}
|
|
|
|
// Level 2: Security mode.
|
|
$security_mode = wpvulnerability_get_security_mode();
|
|
if ( 'disabled' === $security_mode ) {
|
|
return false;
|
|
}
|
|
|
|
if ( 'strict' === $security_mode ) {
|
|
return false;
|
|
}
|
|
|
|
// Level 3: Component-specific whitelist (when specified).
|
|
if ( ! empty( $component ) && defined( 'WPVULNERABILITY_SHELL_EXEC_WHITELIST' ) ) {
|
|
$whitelist = WPVULNERABILITY_SHELL_EXEC_WHITELIST;
|
|
|
|
if ( is_string( $whitelist ) ) {
|
|
$whitelist = array_map( 'trim', explode( ',', $whitelist ) );
|
|
}
|
|
|
|
if ( is_array( $whitelist ) && ! empty( $whitelist ) ) {
|
|
if ( ! in_array( $component, $whitelist, true ) ) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Level 4: PHP configuration check.
|
|
if ( ! function_exists( 'shell_exec' ) ) {
|
|
return false;
|
|
}
|
|
|
|
// Check if `shell_exec` is disabled in PHP configuration.
|
|
if ( in_array( 'shell_exec', array_map( 'trim', explode( ',', (string) ini_get( 'disable_functions' ) ) ), true ) ) {
|
|
return false;
|
|
}
|
|
|
|
// Try to execute a simple command to confirm functionality.
|
|
$test = @shell_exec( escapeshellcmd( 'echo test' ) ); // phpcs:ignore
|
|
|
|
// If the command execution failed or returned null, shell_exec is not working.
|
|
return null !== $test;
|
|
}
|
|
|
|
/**
|
|
* Conditionally log diagnostic messages for the plugin.
|
|
*
|
|
* This helper respects the WordPress debug mode and allows developers to hook into the
|
|
* decision using the {@see 'wpvulnerability_should_log'} filter. Logged messages are
|
|
* encoded as JSON when possible to provide structured context without breaking the
|
|
* WordPress Coding Standards that discourage verbose debugging in production.
|
|
*
|
|
* @since 4.1.7
|
|
*
|
|
* @param string $message Message to record in the debug log.
|
|
* @param array<string, mixed> $context Optional. Additional context about the message. Default empty array.
|
|
*
|
|
* @return void
|
|
*/
|
|
function wpvulnerability_maybe_log( $message, $context = array() ) {
|
|
if ( empty( $message ) ) {
|
|
return;
|
|
}
|
|
|
|
$should_log = defined( 'WP_DEBUG' ) && WP_DEBUG;
|
|
|
|
/**
|
|
* Filter whether a diagnostic message should be logged.
|
|
*
|
|
* @since 4.1.7
|
|
*
|
|
* @param bool $should_log Whether the message should be logged.
|
|
* @param string $message Message to log.
|
|
* @param array $context Additional context data.
|
|
*/
|
|
$should_log = apply_filters( 'wpvulnerability_should_log', $should_log, $message, $context );
|
|
|
|
if ( ! $should_log ) {
|
|
return;
|
|
}
|
|
|
|
$log_entry = array(
|
|
'plugin' => 'wpvulnerability',
|
|
'message' => (string) $message,
|
|
);
|
|
|
|
if ( ! empty( $context ) ) {
|
|
$log_entry['context'] = (array) $context;
|
|
}
|
|
|
|
$encoded_entry = wp_json_encode( $log_entry );
|
|
|
|
if ( false === $encoded_entry ) {
|
|
$encoded_entry = sprintf(
|
|
'wpvulnerability: %s',
|
|
sanitize_text_field( $log_entry['message'] )
|
|
);
|
|
}
|
|
|
|
error_log( $encoded_entry ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
|
}
|
|
|
|
/**
|
|
* Retrieve the cache expiration in hours.
|
|
*
|
|
* The value can be defined via the WPVULNERABILITY_CACHE_HOURS constant,
|
|
* configured in the plugin settings, or falls back to 12 hours.
|
|
*
|
|
* @since 4.1.0
|
|
*
|
|
* @return int Cache duration in hours.
|
|
*/
|
|
function wpvulnerability_cache_hours() {
|
|
$default = 12;
|
|
|
|
if ( defined( 'WPVULNERABILITY_CACHE_HOURS' ) && WPVULNERABILITY_CACHE_HOURS !== $default ) {
|
|
return (int) WPVULNERABILITY_CACHE_HOURS;
|
|
}
|
|
|
|
$settings = is_multisite() ? get_site_option( 'wpvulnerability-config', array() ) : get_option( 'wpvulnerability-config', array() );
|
|
if ( ! is_array( $settings ) ) {
|
|
$settings = array();
|
|
}
|
|
if ( isset( $settings['cache'] ) ) {
|
|
$cache_raw = $settings['cache'];
|
|
$cache = is_scalar( $cache_raw ) ? (int) $cache_raw : 0;
|
|
if ( in_array( $cache, array( 1, 6, 12, 24 ), true ) ) {
|
|
return $cache;
|
|
}
|
|
}
|
|
|
|
return $default;
|
|
}
|
|
|
|
/**
|
|
* Retrieve a JSON-encoded vulnerability count option as an integer.
|
|
*
|
|
* Centralises the `(int) json_decode( get_*_option( ... ), true )` pattern used
|
|
* in the admin dashboard and analysis tabs, providing a properly typed return
|
|
* value that satisfies PHPStan level 9.
|
|
*
|
|
* @since 4.3.3
|
|
*
|
|
* @param string $option_key Option name (without the `wpvulnerability-` prefix).
|
|
*
|
|
* @return int Decoded integer count, or 0 when the option is empty or invalid.
|
|
*/
|
|
function wpvulnerability_get_component_count( string $option_key ): int {
|
|
$raw = is_multisite()
|
|
? get_site_option( 'wpvulnerability-' . $option_key . '-vulnerable', '0' )
|
|
: get_option( 'wpvulnerability-' . $option_key . '-vulnerable', '0' );
|
|
|
|
$decoded = json_decode( is_string( $raw ) ? $raw : '0', true );
|
|
return is_scalar( $decoded ) ? (int) $decoded : 0;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the plugin configuration array, always returning a typed array.
|
|
*
|
|
* Wraps `get_option`/`get_site_option` for the `wpvulnerability-config` key and
|
|
* ensures the return value is an `array<string, mixed>`, merging defaults so
|
|
* that callers can safely access keys without additional type guards.
|
|
*
|
|
* @since 4.3.3
|
|
*
|
|
* @param array<string, mixed> $defaults Optional default values to merge.
|
|
*
|
|
* @return array<string, mixed> Plugin configuration.
|
|
*/
|
|
function wpvulnerability_get_config( array $defaults = array() ): array {
|
|
$raw = is_multisite()
|
|
? get_site_option( 'wpvulnerability-config', array() )
|
|
: get_option( 'wpvulnerability-config', array() );
|
|
|
|
$config = is_array( $raw ) ? $raw : array();
|
|
|
|
return empty( $defaults ) ? $config : wp_parse_args( $config, $defaults );
|
|
}
|
|
|
|
/**
|
|
* Retrieve the supported log retention values.
|
|
*
|
|
* @since 4.2.0
|
|
*
|
|
* @return int[] Valid log retention periods expressed in days. The value "0" disables retention.
|
|
*/
|
|
function wpvulnerability_get_log_retention_values() {
|
|
return array( 0, 1, 7, 14, 28 );
|
|
}
|
|
|
|
/**
|
|
* Determine whether log retention is forced via a constant.
|
|
*
|
|
* @since 4.2.0
|
|
*
|
|
* @return int|null Number of days when forced, or null when editable.
|
|
*/
|
|
function wpvulnerability_forced_log_retention() {
|
|
if ( defined( 'WPVULNERABILITY_LOG_RETENTION_DAYS' ) ) {
|
|
$forced = (int) WPVULNERABILITY_LOG_RETENTION_DAYS;
|
|
if ( in_array( $forced, wpvulnerability_get_log_retention_values(), true ) ) {
|
|
return $forced;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the configured log retention period in days.
|
|
*
|
|
* The value can be defined through the WPVULNERABILITY_LOG_RETENTION_DAYS constant,
|
|
* configured via the settings UI, or falls back to zero (disabled).
|
|
*
|
|
* @since 4.2.0
|
|
*
|
|
* @return int Log retention in days. Zero disables retention.
|
|
*/
|
|
function wpvulnerability_log_retention_days() {
|
|
$default = 0;
|
|
|
|
$forced = wpvulnerability_forced_log_retention();
|
|
if ( null !== $forced ) {
|
|
return $forced;
|
|
}
|
|
|
|
$settings = is_multisite() ? get_site_option( 'wpvulnerability-config', array() ) : get_option( 'wpvulnerability-config', array() );
|
|
if ( ! is_array( $settings ) ) {
|
|
$settings = array();
|
|
}
|
|
if ( isset( $settings['log_retention'] ) ) {
|
|
$retention_raw = $settings['log_retention'];
|
|
$retention = is_scalar( $retention_raw ) ? (int) $retention_raw : 0;
|
|
if ( in_array( $retention, wpvulnerability_get_log_retention_values(), true ) ) {
|
|
return $retention;
|
|
}
|
|
}
|
|
|
|
return $default;
|
|
}
|
|
|
|
/**
|
|
* Register the custom post type used to store API logs.
|
|
*
|
|
* @since 4.2.0
|
|
*
|
|
* @return void
|
|
*/
|
|
function wpvulnerability_register_log_post_type() {
|
|
register_post_type(
|
|
'wpvulnerability_log',
|
|
array(
|
|
'labels' => array(
|
|
'name' => __( 'WPVulnerability Logs', 'wpvulnerability' ),
|
|
'singular_name' => __( 'WPVulnerability Log', 'wpvulnerability' ),
|
|
),
|
|
'public' => false,
|
|
'exclude_from_search' => true,
|
|
'publicly_queryable' => false,
|
|
'show_ui' => false,
|
|
'show_in_menu' => false,
|
|
'supports' => array( 'title', 'editor' ),
|
|
)
|
|
);
|
|
}
|
|
add_action( 'init', 'wpvulnerability_register_log_post_type' );
|
|
|
|
/**
|
|
* Determine whether a URL should be logged as an API call.
|
|
*
|
|
* @since 4.2.0
|
|
*
|
|
* @param string $url Requested URL.
|
|
*
|
|
* @return bool True when the URL targets the configured API host, false otherwise.
|
|
*/
|
|
function wpvulnerability_should_log_api_request( $url ) {
|
|
if ( 0 >= wpvulnerability_log_retention_days() ) {
|
|
return false;
|
|
}
|
|
|
|
$target_host = wp_parse_url( $url, PHP_URL_HOST );
|
|
$api_host = wp_parse_url( WPVULNERABILITY_API_HOST, PHP_URL_HOST );
|
|
|
|
if ( ! is_string( $target_host ) || '' === $target_host ) {
|
|
return false;
|
|
}
|
|
|
|
return 0 === strcasecmp( $target_host, (string) $api_host );
|
|
}
|
|
|
|
/**
|
|
* Convert the HTTP response into a storable string for the log entry.
|
|
*
|
|
* @since 4.2.0
|
|
*
|
|
* @param array<string, mixed>|WP_Error $response Response returned by wp_remote_get().
|
|
*
|
|
* @return string Encoded response body ready for storage.
|
|
*/
|
|
function wpvulnerability_prepare_log_body( $response ) {
|
|
if ( is_wp_error( $response ) ) {
|
|
$error_data = array(
|
|
'code' => $response->get_error_code(),
|
|
'message' => $response->get_error_message(),
|
|
'errors' => $response->errors,
|
|
'data' => $response->error_data,
|
|
);
|
|
$body = wp_json_encode( $error_data );
|
|
if ( false === $body ) {
|
|
$body = serialize( $error_data ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
|
|
}
|
|
return $body;
|
|
}
|
|
|
|
$body = wp_remote_retrieve_body( $response );
|
|
if ( '' === $body ) {
|
|
$encoded = wp_json_encode( array( 'message' => 'empty body' ) );
|
|
return false !== $encoded ? $encoded : '{"message":"empty body"}';
|
|
}
|
|
|
|
return $body;
|
|
}
|
|
|
|
/**
|
|
* Persist an API log entry when appropriate.
|
|
*
|
|
* @since 4.2.0
|
|
*
|
|
* @param string $url Requested URL.
|
|
* @param array<string, mixed>|WP_Error $response Response returned by wp_remote_get().
|
|
*
|
|
* @return void
|
|
*/
|
|
function wpvulnerability_maybe_log_api_response( $url, $response ) {
|
|
if ( ! wpvulnerability_should_log_api_request( $url ) ) {
|
|
return;
|
|
}
|
|
|
|
$log_id = wp_insert_post(
|
|
array(
|
|
'post_type' => 'wpvulnerability_log',
|
|
'post_status' => 'publish',
|
|
'post_title' => wp_strip_all_tags( $url ),
|
|
'post_content' => wp_slash( wpvulnerability_prepare_log_body( $response ) ),
|
|
'post_author' => 0,
|
|
),
|
|
true
|
|
);
|
|
|
|
if ( is_wp_error( $log_id ) ) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve the pagination sizes available for the logs table.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @return int[] Array of valid per-page options.
|
|
*/
|
|
function wpvulnerability_get_log_per_page_options() {
|
|
return array( 10, 50, 100, 250, 1000 );
|
|
}
|
|
|
|
/**
|
|
* Retrieve the default pagination size for the logs table.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @return int Default per-page value.
|
|
*/
|
|
function wpvulnerability_get_default_log_per_page() {
|
|
return 100;
|
|
}
|
|
|
|
/**
|
|
* Retrieve log posts for the administration table.
|
|
*
|
|
* @since 4.2.0
|
|
* @since 4.3.0 Added the $paged argument and updated the default page size.
|
|
*
|
|
* @param int $per_page Optional. Number of logs to return per page. Default 100.
|
|
* @param int $paged Optional. Page number to retrieve. Default 1.
|
|
*
|
|
* @return WP_Post[] Array of log posts.
|
|
*/
|
|
function wpvulnerability_get_api_logs( $per_page = 100, $paged = 1 ) {
|
|
$per_page = max( 1, (int) $per_page );
|
|
$paged = max( 1, (int) $paged );
|
|
$offset = ( $paged - 1 ) * $per_page;
|
|
|
|
return get_posts(
|
|
array(
|
|
'post_type' => 'wpvulnerability_log',
|
|
'post_status' => 'publish',
|
|
'posts_per_page' => $per_page,
|
|
'orderby' => 'date',
|
|
'order' => 'DESC',
|
|
'offset' => $offset,
|
|
'no_found_rows' => true,
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Count the total amount of stored API log entries.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @return int Number of log posts.
|
|
*/
|
|
function wpvulnerability_count_api_logs() {
|
|
$counts = wp_count_posts( 'wpvulnerability_log' );
|
|
|
|
if ( ! isset( $counts->publish ) ) {
|
|
return 0;
|
|
}
|
|
|
|
return (int) $counts->publish;
|
|
}
|
|
|
|
/**
|
|
* Retrieve a single log entry ensuring it belongs to the plugin log post type.
|
|
*
|
|
* @since 4.2.0
|
|
*
|
|
* @param int $log_id Log post ID.
|
|
*
|
|
* @return WP_Post|null Post object on success, null otherwise.
|
|
*/
|
|
function wpvulnerability_get_api_log( $log_id ) {
|
|
$log_id = (int) $log_id;
|
|
if ( $log_id <= 0 ) {
|
|
return null;
|
|
}
|
|
|
|
$log = get_post( $log_id );
|
|
if ( ! $log || 'wpvulnerability_log' !== $log->post_type ) {
|
|
return null;
|
|
}
|
|
|
|
return $log;
|
|
}
|
|
|
|
/**
|
|
* Format stored log content into a pretty printed JSON string when possible.
|
|
*
|
|
* @since 4.2.0
|
|
*
|
|
* @param string $content Stored log body.
|
|
*
|
|
* @return string Formatted content.
|
|
*/
|
|
function wpvulnerability_format_log_content( $content ) {
|
|
$content = (string) $content;
|
|
if ( '' === $content ) {
|
|
return '';
|
|
}
|
|
|
|
$decoded = json_decode( $content, true );
|
|
if ( null === $decoded || JSON_ERROR_NONE !== json_last_error() ) {
|
|
return $content;
|
|
}
|
|
|
|
$pretty = wp_json_encode( $decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
|
|
if ( false === $pretty ) {
|
|
return $content;
|
|
}
|
|
|
|
return $pretty;
|
|
}
|
|
|
|
/**
|
|
* Format the log date using the site's date and time settings.
|
|
*
|
|
* @since 4.2.0
|
|
*
|
|
* @param WP_Post $log Log post.
|
|
*
|
|
* @return string Formatted date string.
|
|
*/
|
|
function wpvulnerability_format_log_date( WP_Post $log ) {
|
|
$date_fmt = get_option( 'date_format' );
|
|
$time_fmt = get_option( 'time_format' );
|
|
$format = trim( ( is_scalar( $date_fmt ) ? (string) $date_fmt : '' ) . ' ' . ( is_scalar( $time_fmt ) ? (string) $time_fmt : '' ) );
|
|
if ( '' === $format ) {
|
|
$format = 'Y-m-d H:i:s';
|
|
}
|
|
|
|
return (string) mysql2date( $format, $log->post_date, true );
|
|
}
|
|
|
|
/**
|
|
* Delete logs that fall outside of the configured retention window.
|
|
*
|
|
* @since 4.2.0
|
|
*
|
|
* @return void
|
|
*/
|
|
function wpvulnerability_delete_expired_logs() {
|
|
$retention = wpvulnerability_log_retention_days();
|
|
if ( $retention <= 0 ) {
|
|
return;
|
|
}
|
|
|
|
$threshold_ts = strtotime( '-' . $retention . ' days', time() );
|
|
$threshold = gmdate( 'Y-m-d H:i:s', false !== $threshold_ts ? $threshold_ts : time() );
|
|
|
|
do {
|
|
$logs = get_posts(
|
|
array(
|
|
'post_type' => 'wpvulnerability_log',
|
|
'post_status' => 'publish',
|
|
'fields' => 'ids',
|
|
'posts_per_page' => 100,
|
|
'orderby' => 'date',
|
|
'order' => 'ASC',
|
|
'no_found_rows' => true,
|
|
'cache_results' => false,
|
|
'update_post_term_cache' => false,
|
|
'update_post_meta_cache' => false,
|
|
'date_query' => array(
|
|
array(
|
|
'before' => $threshold,
|
|
'inclusive' => false,
|
|
),
|
|
),
|
|
)
|
|
);
|
|
|
|
if ( empty( $logs ) ) {
|
|
break;
|
|
}
|
|
|
|
foreach ( $logs as $log_id ) {
|
|
wp_delete_post( $log_id, true );
|
|
}
|
|
$logs_count = count( $logs );
|
|
} while ( $logs_count >= 100 );
|
|
}
|
|
|
|
add_action( 'wpvulnerability_cleanup_logs', 'wpvulnerability_delete_expired_logs' );
|
|
|
|
/**
|
|
* Delete all stored API logs.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @return void
|
|
*/
|
|
function wpvulnerability_delete_all_logs() {
|
|
do {
|
|
$logs = get_posts(
|
|
array(
|
|
'post_type' => 'wpvulnerability_log',
|
|
'post_status' => 'publish',
|
|
'fields' => 'ids',
|
|
'posts_per_page' => 100,
|
|
'orderby' => 'date',
|
|
'order' => 'ASC',
|
|
'no_found_rows' => true,
|
|
'cache_results' => false,
|
|
'update_post_term_cache' => false,
|
|
'update_post_meta_cache' => false,
|
|
)
|
|
);
|
|
|
|
if ( empty( $logs ) ) {
|
|
break;
|
|
}
|
|
|
|
foreach ( $logs as $log_id ) {
|
|
wp_delete_post( $log_id, true );
|
|
}
|
|
$logs_count = count( $logs );
|
|
} while ( $logs_count >= 100 );
|
|
}
|
|
|
|
/**
|
|
* Normalize various truthy and falsy values into the expected 'y' or 'n' format.
|
|
*
|
|
* This helper ensures that configuration options stored as booleans or integers
|
|
* in previous plugin versions are converted into the new string-based format.
|
|
*
|
|
* @since 4.1.1
|
|
*
|
|
* @param mixed $value Value to normalize.
|
|
*
|
|
* @return string Returns 'y' when enabled, 'n' otherwise.
|
|
*/
|
|
function wpvulnerability_normalize_yes_no( $value ) {
|
|
if ( is_string( $value ) ) {
|
|
$value = strtolower( trim( (string) $value ) );
|
|
}
|
|
|
|
$truthy = array( 'y', 'yes', '1', 1, true, 'true', 'on' );
|
|
|
|
return in_array( $value, $truthy, true ) ? 'y' : 'n';
|
|
}
|
|
|
|
/**
|
|
* Determine if a stored yes/no value should be treated as enabled.
|
|
*
|
|
* @since 4.1.1
|
|
*
|
|
* @param mixed $value Value to evaluate.
|
|
*
|
|
* @return bool True when enabled, false otherwise.
|
|
*/
|
|
function wpvulnerability_is_yes( $value ) {
|
|
return 'y' === wpvulnerability_normalize_yes_no( $value );
|
|
}
|
|
|
|
/**
|
|
* Normalize the notification configuration array.
|
|
*
|
|
* @since 4.1.1
|
|
*
|
|
* @param mixed $notify Notification configuration values.
|
|
*
|
|
* @return array<string, string> Normalized notification configuration containing 'email', 'slack', 'teams', 'discord', and 'telegram'.
|
|
*/
|
|
function wpvulnerability_normalize_notify_settings( $notify ) {
|
|
$defaults = array(
|
|
'email' => 'n',
|
|
'slack' => 'n',
|
|
'teams' => 'n',
|
|
'discord' => 'n',
|
|
'telegram' => 'n',
|
|
);
|
|
$normalized = array();
|
|
|
|
if ( is_array( $notify ) ) {
|
|
foreach ( $notify as $channel => $value ) {
|
|
$normalized[ $channel ] = wpvulnerability_normalize_yes_no( $value );
|
|
}
|
|
}
|
|
|
|
return array_merge( $defaults, $normalized );
|
|
}
|
|
|
|
/**
|
|
* Sanitize a version string.
|
|
*
|
|
* This function removes any leading or trailing whitespace from the version string
|
|
* and strips out any non-alphanumeric characters except for hyphens, underscores, and dots.
|
|
*
|
|
* @since 2.0.0
|
|
*
|
|
* @param string|null $version The version string to sanitize.
|
|
*
|
|
* @return string The sanitized version string.
|
|
*/
|
|
function wpvulnerability_sanitize_version( $version ) {
|
|
// Remove any leading or trailing whitespace.
|
|
$version = trim( (string) $version );
|
|
|
|
// Strip out any non-alphanumeric characters except for hyphens, underscores, and dots.
|
|
$replaced = preg_replace( '/[^a-zA-Z0-9_\-.]+/', '', $version );
|
|
$version = null !== $replaced ? $replaced : '';
|
|
|
|
// Normalize WordPress pre-release build suffixes such as "-beta1-12345" to "-beta1".
|
|
if ( preg_match( '/^(\d+\.\d+(?:\.\d+)?-(?:beta|rc)\d+)(?:-\d+)$/i', $version, $matches ) ) {
|
|
$version = $matches[1];
|
|
}
|
|
|
|
return $version;
|
|
}
|
|
|
|
/**
|
|
* Sanitize a version string and validate its format.
|
|
*
|
|
* This function sanitizes the input version string and checks it against a regular expression
|
|
* to match the standard versioning format (major.minor[.patch[.build]]). It returns the matched version
|
|
* if it conforms to the expected format; otherwise, it returns the original version.
|
|
*
|
|
* @since 3.5.0 Introduced.
|
|
*
|
|
* @param string|null $version The version string to sanitize and validate.
|
|
* @return string|null The sanitized version string if it matches the standard format; otherwise, the original version string, or null when empty.
|
|
*/
|
|
function wpvulnerability_sanitize_and_validate_version( $version ) {
|
|
if ( null === $version ) {
|
|
return null;
|
|
}
|
|
|
|
// Sanitize the version string using the base sanitizer.
|
|
$version = wpvulnerability_sanitize_version( $version );
|
|
|
|
if ( '' === $version ) {
|
|
return null;
|
|
}
|
|
|
|
// Validate format (major.minor[.patch[.build]]) and sanitize.
|
|
if ( preg_match( '/^\d+\.\d+(\.\d+){0,2}(\.\d+)?/', $version, $match ) ) {
|
|
return trim( $match[0] );
|
|
}
|
|
|
|
return $version;
|
|
}
|
|
|
|
/**
|
|
* Detects the version of SQLite using the SQLite3 extension or system commands.
|
|
*
|
|
* Uses a hybrid detection approach:
|
|
* 1. PHP SQLite3 extension (most secure, reliability 90)
|
|
* 2. PDO SQLite (secondary secure method, reliability 90)
|
|
* 3. shell_exec commands (most accurate, reliability 95)
|
|
* 4. Binary existence check (basic fallback, reliability 30)
|
|
*
|
|
* @since 3.5.0 Introduced.
|
|
* @since 4.3.0 Enhanced with hybrid detection and reliability scoring.
|
|
*
|
|
* @return array{version: string|null, method: string, reliability: int, attempts: list<string>}
|
|
*/
|
|
function wpvulnerability_detect_sqlite() {
|
|
$result = array(
|
|
'version' => null,
|
|
'method' => 'none',
|
|
'reliability' => 0,
|
|
'attempts' => array(),
|
|
);
|
|
|
|
// Method 1: PHP SQLite3 extension (most secure).
|
|
if ( class_exists( 'SQLite3' ) ) {
|
|
$result['attempts'][] = 'sqlite3_extension';
|
|
|
|
try {
|
|
$sqlite = new SQLite3( ':memory:' );
|
|
$version_info = $sqlite->version();
|
|
|
|
if ( isset( $version_info['versionString'] ) ) {
|
|
$version = $version_info['versionString'];
|
|
if ( preg_match( '/-(\d+)$/', $version, $suffix_matches ) ) {
|
|
$version = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $version );
|
|
}
|
|
$version = wpvulnerability_sanitize_and_validate_version( $version );
|
|
|
|
if ( $version ) {
|
|
$result['version'] = $version;
|
|
$result['method'] = 'sqlite3_extension';
|
|
$result['reliability'] = 90;
|
|
return $result;
|
|
}
|
|
}
|
|
} catch ( Exception $e ) {
|
|
wpvulnerability_maybe_log( 'SQLite3 extension detection failed', array( 'error' => $e->getMessage() ) );
|
|
}
|
|
}
|
|
|
|
// Method 2: PDO SQLite extension (secondary secure method).
|
|
if ( ! class_exists( 'SQLite3' ) && class_exists( 'PDO' ) ) {
|
|
$result['attempts'][] = 'pdo_sqlite';
|
|
|
|
$drivers = \PDO::getAvailableDrivers(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.RestrictedClasses.mysql__PDO
|
|
|
|
if ( in_array( 'sqlite', $drivers, true ) ) {
|
|
try {
|
|
$pdo = new \PDO( 'sqlite::memory:' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.RestrictedClasses.mysql__PDO
|
|
$pdo->setAttribute( \PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.RestrictedClasses.mysql__PDO
|
|
$statement = $pdo->query( 'SELECT sqlite_version()' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.RestrictedClasses.mysql__PDO
|
|
|
|
if ( $statement ) {
|
|
$version_result = $statement->fetchColumn();
|
|
|
|
if ( false !== $version_result ) {
|
|
$version = (string) $version_result;
|
|
if ( preg_match( '/-(\d+)$/', $version, $suffix_matches ) ) {
|
|
$version = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $version );
|
|
}
|
|
$version = wpvulnerability_sanitize_and_validate_version( $version );
|
|
|
|
if ( $version ) {
|
|
$result['version'] = $version;
|
|
$result['method'] = 'pdo_sqlite';
|
|
$result['reliability'] = 90;
|
|
|
|
$pdo = null;
|
|
return $result;
|
|
}
|
|
}
|
|
}
|
|
|
|
$pdo = null;
|
|
} catch ( Exception $exception ) {
|
|
wpvulnerability_maybe_log( 'PDO SQLite detection failed', array( 'error' => $exception->getMessage() ) );
|
|
if ( isset( $pdo ) ) {
|
|
$pdo = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 3: shell_exec command (most accurate).
|
|
if ( wpvulnerability_can_shell_exec( 'sqlite' ) ) {
|
|
$result['attempts'][] = 'shell_sqlite3';
|
|
|
|
$version_output = wpvulnerability_safe_shell_exec( 'sqlite', 'sqlite3 --version' );
|
|
|
|
if ( ! empty( $version_output ) && preg_match( '/(\d+\.\d+(?:\.\d+)?(?:-\d+)?)/', $version_output, $matches ) ) {
|
|
$version = $matches[1];
|
|
if ( preg_match( '/-(\d+)$/', $version, $suffix_matches ) ) {
|
|
$version = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $version );
|
|
}
|
|
$version = wpvulnerability_sanitize_and_validate_version( $version );
|
|
|
|
if ( $version ) {
|
|
$result['version'] = $version;
|
|
$result['method'] = 'shell_exec';
|
|
$result['reliability'] = 95;
|
|
return $result;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 4: Binary existence check (basic fallback).
|
|
if ( wpvulnerability_can_shell_exec( 'sqlite' ) ) {
|
|
$result['attempts'][] = 'which_sqlite3';
|
|
|
|
$which_output = wpvulnerability_safe_shell_exec( 'sqlite', 'which sqlite3' );
|
|
|
|
if ( ! empty( $which_output ) ) {
|
|
$result['version'] = 'unknown';
|
|
$result['method'] = 'binary_exists';
|
|
$result['reliability'] = 30;
|
|
return $result;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Detects the version of Redis using the Redis extension or system commands.
|
|
*
|
|
* Uses a hybrid detection approach:
|
|
* 1. PHP Redis extension (most secure, reliability 90)
|
|
* 2. shell_exec commands (most accurate, reliability 95)
|
|
* 3. Binary existence check (basic fallback, reliability 30)
|
|
*
|
|
* @since 3.5.0 Introduced.
|
|
* @since 4.3.0 Enhanced with hybrid detection and reliability scoring.
|
|
*
|
|
* @return array{version: string|null, method: string, reliability: int, attempts: list<string>}
|
|
*/
|
|
function wpvulnerability_detect_redis() {
|
|
$result = array(
|
|
'version' => null,
|
|
'method' => 'none',
|
|
'reliability' => 0,
|
|
'attempts' => array(),
|
|
);
|
|
|
|
// Method 1: PHP Redis extension (most secure).
|
|
if ( class_exists( 'Redis' ) ) {
|
|
$result['attempts'][] = 'redis_extension';
|
|
|
|
$redis_client = null;
|
|
$temporary_connection = false;
|
|
|
|
// Attempt to reuse an existing Redis connection from WordPress object cache implementations.
|
|
$cache_instance = null;
|
|
if ( function_exists( 'wp_cache_get_instance' ) ) {
|
|
$cache_instance = wp_cache_get_instance();
|
|
} elseif ( isset( $GLOBALS['wp_object_cache'] ) && is_object( $GLOBALS['wp_object_cache'] ) ) {
|
|
$cache_instance = $GLOBALS['wp_object_cache'];
|
|
}
|
|
|
|
if ( is_object( $cache_instance ) ) {
|
|
foreach ( array( 'redis', 'redis_client', 'client', 'redis_instance', 'connection' ) as $property ) {
|
|
if ( isset( $cache_instance->{$property} ) && $cache_instance->{$property} instanceof Redis ) {
|
|
$redis_client = $cache_instance->{$property};
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( ! $redis_client instanceof Redis ) {
|
|
foreach ( array( 'get_redis', 'get_client', 'redis', 'redis_instance' ) as $method ) {
|
|
if ( ! method_exists( $cache_instance, $method ) ) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$reflection_method = new ReflectionMethod( $cache_instance, $method );
|
|
if ( $reflection_method->getNumberOfRequiredParameters() > 0 || ! $reflection_method->isPublic() ) {
|
|
continue;
|
|
}
|
|
} catch ( ReflectionException $exception ) {
|
|
continue;
|
|
}
|
|
|
|
if ( ! is_callable( array( $cache_instance, $method ) ) ) {
|
|
continue;
|
|
}
|
|
|
|
$maybe_client = $cache_instance->{$method}();
|
|
if ( $maybe_client instanceof Redis ) {
|
|
$redis_client = $maybe_client;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( ! $redis_client instanceof Redis ) {
|
|
$redis_client = new Redis();
|
|
$temporary_connection = true;
|
|
|
|
$host = defined( 'WP_REDIS_HOST' ) ? (string) WP_REDIS_HOST : '127.0.0.1';
|
|
$port = defined( 'WP_REDIS_PORT' ) ? (int) WP_REDIS_PORT : 6379;
|
|
$timeout = defined( 'WP_REDIS_TIMEOUT' ) ? (float) WP_REDIS_TIMEOUT : 0.0;
|
|
$connected = false;
|
|
|
|
try {
|
|
if ( defined( 'WP_REDIS_PATH' ) && '' !== (string) WP_REDIS_PATH ) {
|
|
$connected = $redis_client->connect( (string) WP_REDIS_PATH );
|
|
} elseif ( $timeout > 0 ) {
|
|
$connected = $redis_client->connect( $host, $port, $timeout );
|
|
} else {
|
|
$connected = $redis_client->connect( $host, $port );
|
|
}
|
|
|
|
if ( $connected ) {
|
|
$username = defined( 'WP_REDIS_USERNAME' ) ? (string) WP_REDIS_USERNAME : '';
|
|
$password = null;
|
|
if ( defined( 'WP_REDIS_PASSWORD' ) ) {
|
|
$password = (string) WP_REDIS_PASSWORD;
|
|
} elseif ( defined( 'WP_REDIS_AUTH' ) ) {
|
|
$password = (string) WP_REDIS_AUTH;
|
|
}
|
|
|
|
if ( '' !== $username && null !== $password ) {
|
|
$redis_client->auth( array( $username, $password ) );
|
|
} elseif ( null !== $password && '' !== $password ) {
|
|
$redis_client->auth( $password );
|
|
}
|
|
|
|
if ( defined( 'WP_REDIS_DATABASE' ) ) {
|
|
$redis_client->select( (int) WP_REDIS_DATABASE );
|
|
}
|
|
} else {
|
|
$redis_client = null;
|
|
$temporary_connection = false;
|
|
}
|
|
} catch ( RedisException $e ) {
|
|
$redis_client = null;
|
|
$temporary_connection = false;
|
|
} catch ( Exception $e ) {
|
|
$redis_client = null;
|
|
$temporary_connection = false;
|
|
}
|
|
}
|
|
|
|
if ( $redis_client instanceof Redis ) {
|
|
try {
|
|
$redis_info = $redis_client->info();
|
|
|
|
if ( isset( $redis_info['redis_version'] ) ) {
|
|
$version = $redis_info['redis_version'];
|
|
if ( preg_match( '/-(\d+)$/', $version, $suffix_matches ) ) {
|
|
$version = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $version );
|
|
}
|
|
$version = wpvulnerability_sanitize_and_validate_version( $version );
|
|
|
|
if ( $version ) {
|
|
$result['version'] = $version;
|
|
$result['method'] = 'redis_extension';
|
|
$result['reliability'] = 90;
|
|
|
|
if ( $temporary_connection ) {
|
|
$redis_client->close();
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
}
|
|
} catch ( RedisException $e ) {
|
|
wpvulnerability_maybe_log(
|
|
'Redis extension available but info() call failed',
|
|
array(
|
|
'exception' => array(
|
|
'code' => $e->getCode(),
|
|
'message' => $e->getMessage(),
|
|
),
|
|
)
|
|
);
|
|
} catch ( Exception $e ) {
|
|
wpvulnerability_maybe_log(
|
|
'Redis detection failed',
|
|
array(
|
|
'exception' => array(
|
|
'code' => $e->getCode(),
|
|
'message' => $e->getMessage(),
|
|
),
|
|
)
|
|
);
|
|
} finally {
|
|
if ( $temporary_connection ) {
|
|
$redis_client->close();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 2: shell_exec command (most accurate).
|
|
if ( wpvulnerability_can_shell_exec( 'redis' ) ) {
|
|
$result['attempts'][] = 'shell_redis_server';
|
|
|
|
$version_output = wpvulnerability_safe_shell_exec( 'redis', 'redis-server --version' );
|
|
|
|
if ( ! empty( $version_output ) && preg_match( '/redis-server\s+v=([\d.]+)/i', $version_output, $matches ) ) {
|
|
$version = $matches[1];
|
|
if ( preg_match( '/-(\d+)$/', $version, $suffix_matches ) ) {
|
|
$version = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $version );
|
|
}
|
|
$version = wpvulnerability_sanitize_and_validate_version( $version );
|
|
|
|
if ( $version ) {
|
|
$result['version'] = $version;
|
|
$result['method'] = 'shell_exec';
|
|
$result['reliability'] = 95;
|
|
return $result;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 3: Binary existence check (basic fallback).
|
|
if ( wpvulnerability_can_shell_exec( 'redis' ) ) {
|
|
$result['attempts'][] = 'which_redis_server';
|
|
|
|
$which_output = wpvulnerability_safe_shell_exec( 'redis', 'which redis-server' );
|
|
|
|
if ( ! empty( $which_output ) ) {
|
|
$result['version'] = 'unknown';
|
|
$result['method'] = 'binary_exists';
|
|
$result['reliability'] = 30;
|
|
return $result;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Normalizes Memcached version information returned by PHP extensions.
|
|
*
|
|
* @since 4.1.7
|
|
*
|
|
* @param mixed $version_info Version information as returned by Memcached::getVersion() or Memcache::getVersion().
|
|
* @return string|null Normalized version string or null when it cannot be determined.
|
|
*/
|
|
function wpvulnerability_normalize_memcached_version_info( $version_info ) {
|
|
$reported_versions = array();
|
|
|
|
if ( is_array( $version_info ) ) {
|
|
$reported_versions = $version_info;
|
|
} elseif ( is_string( $version_info ) ) {
|
|
$trimmed_version = trim( $version_info );
|
|
if ( '' !== $trimmed_version ) {
|
|
$reported_versions = array( $trimmed_version );
|
|
}
|
|
}
|
|
|
|
foreach ( $reported_versions as $reported_version ) {
|
|
if ( ! is_scalar( $reported_version ) ) {
|
|
continue;
|
|
}
|
|
$reported_version = trim( (string) $reported_version );
|
|
|
|
if ( '' === $reported_version || '255.255.255' === $reported_version ) {
|
|
continue;
|
|
}
|
|
|
|
if ( preg_match( '/-(\d+)$/', $reported_version, $suffix_matches ) ) {
|
|
$reported_version = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $reported_version );
|
|
}
|
|
|
|
return $reported_version;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Detects the version of Memcached using the Memcached extension or system commands.
|
|
*
|
|
* Uses a hybrid detection approach:
|
|
* 1. PHP Memcached/Memcache extension (most secure, reliability 90)
|
|
* 2. shell_exec commands (most accurate, reliability 95)
|
|
* 3. Binary existence check (basic fallback, reliability 30)
|
|
*
|
|
* @since 3.5.0 Introduced.
|
|
* @since 4.3.0 Enhanced with hybrid detection and reliability scoring.
|
|
*
|
|
* @return array{version: string|null, method: string, reliability: int, attempts: list<string>}
|
|
*/
|
|
function wpvulnerability_detect_memcached() {
|
|
$result = array(
|
|
'version' => null,
|
|
'method' => 'none',
|
|
'reliability' => 0,
|
|
'attempts' => array(),
|
|
);
|
|
|
|
// Method 1: PHP Memcached extension (most secure).
|
|
if ( class_exists( 'Memcached' ) ) {
|
|
$result['attempts'][] = 'memcached_extension';
|
|
|
|
try {
|
|
$memcached = new Memcached();
|
|
$version_info = $memcached->getVersion();
|
|
$version = wpvulnerability_normalize_memcached_version_info( $version_info );
|
|
|
|
if ( empty( $version ) ) {
|
|
$servers = $memcached->getServerList();
|
|
|
|
if ( ! empty( $servers ) ) {
|
|
$memcached->resetServerList();
|
|
|
|
foreach ( $servers as $server ) {
|
|
if ( ! is_array( $server ) || empty( $server['host'] ) ) {
|
|
continue;
|
|
}
|
|
|
|
$host_raw = $server['host'];
|
|
$host = is_scalar( $host_raw ) ? (string) $host_raw : '';
|
|
$port_raw = $server['port'] ?? 11211;
|
|
$port = is_scalar( $port_raw ) ? (int) $port_raw : 11211;
|
|
$weight_raw = $server['weight'] ?? 0;
|
|
$weight = is_scalar( $weight_raw ) ? (int) $weight_raw : 0;
|
|
|
|
$memcached->addServer( $host, $port, $weight );
|
|
}
|
|
|
|
$version_info = $memcached->getVersion();
|
|
$version = wpvulnerability_normalize_memcached_version_info( $version_info );
|
|
}
|
|
}
|
|
|
|
if ( $version ) {
|
|
$version = wpvulnerability_sanitize_and_validate_version( $version );
|
|
if ( $version ) {
|
|
$result['version'] = $version;
|
|
$result['method'] = 'memcached_extension';
|
|
$result['reliability'] = 90;
|
|
return $result;
|
|
}
|
|
}
|
|
} catch ( MemcachedException $e ) {
|
|
// Extension available but service not running.
|
|
unset( $memcached );
|
|
} catch ( Exception $e ) {
|
|
unset( $memcached );
|
|
}
|
|
}
|
|
|
|
// Try legacy Memcache extension.
|
|
if ( class_exists( 'Memcache' ) ) {
|
|
$result['attempts'][] = 'memcache_extension';
|
|
|
|
try {
|
|
$memcache = new Memcache();
|
|
$version_info = $memcache->getVersion();
|
|
$version = wpvulnerability_normalize_memcached_version_info( $version_info );
|
|
|
|
if ( $version ) {
|
|
$version = wpvulnerability_sanitize_and_validate_version( $version );
|
|
if ( $version ) {
|
|
$result['version'] = $version;
|
|
$result['method'] = 'memcache_extension';
|
|
$result['reliability'] = 90;
|
|
return $result;
|
|
}
|
|
}
|
|
} catch ( Exception $e ) {
|
|
unset( $memcache );
|
|
}
|
|
}
|
|
|
|
// Method 2: shell_exec command (most accurate).
|
|
if ( wpvulnerability_can_shell_exec( 'memcached' ) ) {
|
|
$result['attempts'][] = 'shell_memcached';
|
|
|
|
$version_output = wpvulnerability_safe_shell_exec( 'memcached', 'memcached -h' );
|
|
|
|
if ( ! empty( $version_output ) && preg_match( '/memcached\s+(\d+\.\d+(?:\.\d+)?(?:-\d+)?)/i', $version_output, $matches ) ) {
|
|
$version = $matches[1];
|
|
if ( preg_match( '/-(\d+)$/', $version, $suffix_matches ) ) {
|
|
$version = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $version );
|
|
}
|
|
$version = wpvulnerability_sanitize_and_validate_version( $version );
|
|
|
|
if ( $version ) {
|
|
$result['version'] = $version;
|
|
$result['method'] = 'shell_exec';
|
|
$result['reliability'] = 95;
|
|
return $result;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 3: Binary existence check (basic fallback).
|
|
if ( wpvulnerability_can_shell_exec( 'memcached' ) ) {
|
|
$result['attempts'][] = 'which_memcached';
|
|
|
|
$which_output = wpvulnerability_safe_shell_exec( 'memcached', 'which memcached' );
|
|
|
|
if ( ! empty( $which_output ) ) {
|
|
$result['version'] = 'unknown';
|
|
$result['method'] = 'binary_exists';
|
|
$result['reliability'] = 30;
|
|
return $result;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Detects the installed PHP version using available runtime information.
|
|
*
|
|
* @since 2.0.0
|
|
*
|
|
* @return string|null The detected PHP version in N.n or N.n.n format, or null if unavailable.
|
|
*/
|
|
function wpvulnerability_detect_php() {
|
|
// Initialize the version variable.
|
|
$version = null;
|
|
|
|
// First method: use the PHP_VERSION constant.
|
|
if ( defined( 'PHP_VERSION' ) ) {
|
|
$version = PHP_VERSION;
|
|
}
|
|
|
|
// First method: use the phpversion function.
|
|
if ( empty( $version ) && function_exists( 'phpversion' ) ) {
|
|
$version = phpversion();
|
|
}
|
|
|
|
// Second method: use system commands if the first fails and shell_exec is available.
|
|
if ( empty( $version ) && wpvulnerability_can_shell_exec() ) {
|
|
// Command to check PHP version (routed through the safe wrapper for validation + audit logging).
|
|
$version_output = wpvulnerability_safe_shell_exec( 'php', 'php -v' );
|
|
|
|
if ( ! empty( $version_output ) && preg_match( '/PHP\s+(\d+\.\d+(?:\.\d+)?(?:-\d+)?)/i', $version_output, $matches ) ) {
|
|
$version = $matches[1];
|
|
// Replace "-N" at the end with ".N" if present.
|
|
if ( preg_match( '/-(\d+)$/', $version, $suffix_matches ) ) {
|
|
$version = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $version );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return the sanitized and validated PHP version or null if it cannot be detected.
|
|
return wpvulnerability_sanitize_and_validate_version( $version );
|
|
}
|
|
|
|
/**
|
|
* Detects the version of cURL using the cURL extension or system commands.
|
|
*
|
|
* @since 3.5.0 Introduced.
|
|
*
|
|
* @return string|null The version of cURL in the format N.n.n, N.n, etc., or null if it cannot be detected.
|
|
*/
|
|
function wpvulnerability_detect_curl() {
|
|
// Product name for consistency.
|
|
$version = null;
|
|
|
|
// First method: use the cURL extension of PHP.
|
|
if ( function_exists( 'curl_version' ) ) {
|
|
$curl_info = curl_version();
|
|
$version = isset( $curl_info['version'] ) ? $curl_info['version'] : null;
|
|
}
|
|
|
|
// Second method: use system commands if the first fails and shell_exec is available.
|
|
if ( empty( $version ) && wpvulnerability_can_shell_exec() ) {
|
|
// Command to check cURL version (routed through the safe wrapper for validation + audit logging).
|
|
$version_output = wpvulnerability_safe_shell_exec( 'curl', 'curl --version' );
|
|
|
|
if ( ! empty( $version_output ) && preg_match( '/curl\s+(\d+\.\d+(?:\.\d+)?(?:-\d+)?)/i', $version_output, $matches ) ) {
|
|
$version = $matches[1];
|
|
// Replace "-N" at the end with ".N" if present.
|
|
if ( preg_match( '/-(\d+)$/', $version, $suffix_matches ) ) {
|
|
$version = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $version );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return the sanitized and validated version or null if it cannot be detected.
|
|
return wpvulnerability_sanitize_and_validate_version( $version );
|
|
}
|
|
|
|
/**
|
|
* Detects the version of ImageMagick using the Imagick extension or system commands.
|
|
*
|
|
* Uses a hybrid detection approach:
|
|
* 1. PHP Imagick extension (most secure, reliability 90)
|
|
* 2. shell_exec commands (most accurate, reliability 95-100)
|
|
* 3. Binary existence check (basic fallback, reliability 30)
|
|
*
|
|
* @since 3.5.0 Introduced.
|
|
* @since 4.3.0 Enhanced with hybrid detection and reliability scoring.
|
|
* @since 5.0.0 Version regex updated to handle IMEI-installed ImageMagick builds
|
|
* (format: `ImageMagick (IMEI - ...) 7.x.y-z`).
|
|
*
|
|
* @return array{version: string|null, method: string, reliability: int, attempts: list<string>}
|
|
*/
|
|
function wpvulnerability_detect_imagemagick() {
|
|
$result = array(
|
|
'version' => null,
|
|
'method' => 'none',
|
|
'reliability' => 0,
|
|
'attempts' => array(),
|
|
);
|
|
|
|
// Method 1: PHP Imagick extension (most secure).
|
|
if ( extension_loaded( 'imagick' ) && class_exists( 'Imagick' ) ) {
|
|
$result['attempts'][] = 'imagick_extension';
|
|
|
|
try {
|
|
$imagick = new Imagick();
|
|
$version_info = $imagick->getVersion();
|
|
|
|
if ( preg_match( '/ImageMagick(?:\s*\([^)]+\))?\s+(\d+\.\d+(?:\.\d+)?(?:-\d+)?)/i', $version_info['versionString'], $matches ) ) {
|
|
$version = $matches[1];
|
|
if ( preg_match( '/-(\d+)$/', $version, $suffix_matches ) ) {
|
|
$version = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $version );
|
|
}
|
|
$version = wpvulnerability_sanitize_and_validate_version( $version );
|
|
|
|
if ( $version ) {
|
|
$result['version'] = $version;
|
|
$result['method'] = 'imagick_extension';
|
|
$result['reliability'] = 90;
|
|
return $result;
|
|
}
|
|
}
|
|
} catch ( \ImagickException $exception ) {
|
|
wpvulnerability_maybe_log(
|
|
'ImageMagick version detection via the Imagick PHP extension failed.',
|
|
array(
|
|
'exception' => get_class( $exception ),
|
|
'error' => $exception->getMessage(),
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
// Method 2: shell_exec commands (most accurate).
|
|
if ( wpvulnerability_can_shell_exec( 'imagemagick' ) ) {
|
|
$commands = array( 'magick -version', 'convert -version', 'identify -version' );
|
|
|
|
foreach ( $commands as $cmd ) {
|
|
$result['attempts'][] = 'shell_' . explode( ' ', $cmd )[0];
|
|
|
|
$version_output = wpvulnerability_safe_shell_exec( 'imagemagick', $cmd );
|
|
|
|
if ( ! empty( $version_output ) && preg_match( '/ImageMagick(?:\s*\([^)]+\))?\s+(\d+\.\d+(?:\.\d+)?(?:-\d+)?)/i', $version_output, $matches ) ) {
|
|
$version = $matches[1];
|
|
if ( preg_match( '/-(\d+)$/', $version, $suffix_matches ) ) {
|
|
$version = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $version );
|
|
}
|
|
$version = wpvulnerability_sanitize_and_validate_version( $version );
|
|
|
|
if ( $version ) {
|
|
$result['version'] = $version;
|
|
$result['method'] = 'shell_exec';
|
|
$result['reliability'] = 95;
|
|
return $result;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 3: Binary existence check (basic fallback).
|
|
if ( wpvulnerability_can_shell_exec( 'imagemagick' ) ) {
|
|
$binaries = array( 'convert', 'magick', 'identify' );
|
|
|
|
foreach ( $binaries as $binary ) {
|
|
$result['attempts'][] = 'which_' . $binary;
|
|
|
|
$which_output = wpvulnerability_safe_shell_exec( 'imagemagick', 'which ' . $binary );
|
|
|
|
if ( ! empty( $which_output ) ) {
|
|
$result['version'] = 'unknown';
|
|
$result['method'] = 'binary_exists';
|
|
$result['reliability'] = 30;
|
|
return $result;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the Apache HTTP Server version using available PHP APIs.
|
|
*
|
|
* The version is first gathered using the {@see apache_get_version()} function if it exists. The detected
|
|
* version is sanitized and validated to ensure it matches the expected `major.minor.patch` format. The numeric
|
|
* portion is extracted before sanitization so that decorated version strings are normalized. A filter
|
|
* allows overriding the detected version, which is helpful for testing environments where the Apache API is
|
|
* unavailable.
|
|
*
|
|
* @since 4.1.7
|
|
*
|
|
* @return string|null The sanitized Apache version or null when it cannot be determined.
|
|
*/
|
|
function wpvulnerability_get_apache_version() {
|
|
$apache_version = null;
|
|
$normalize_apache_version = static function ( $value ) {
|
|
if ( ! is_string( $value ) ) {
|
|
return null;
|
|
}
|
|
|
|
$value = trim( $value );
|
|
|
|
if ( '' === $value ) {
|
|
return null;
|
|
}
|
|
|
|
if ( preg_match( '/(\d+\.\d+(?:\.\d+){0,2})/', $value, $matches ) ) {
|
|
$value = $matches[1];
|
|
}
|
|
|
|
$value = wpvulnerability_sanitize_and_validate_version( $value );
|
|
|
|
if ( null === $value ) {
|
|
return null;
|
|
}
|
|
|
|
if ( ! preg_match( '/^\d/', $value ) ) {
|
|
return null;
|
|
}
|
|
|
|
return $value;
|
|
};
|
|
|
|
if ( function_exists( 'apache_get_version' ) ) {
|
|
$raw_version = apache_get_version();
|
|
|
|
if ( is_string( $raw_version ) ) {
|
|
$apache_version = $normalize_apache_version( $raw_version );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Filter the detected Apache version.
|
|
*
|
|
* This filter allows overriding the detected Apache HTTP Server version. Returning a falsy value will cause
|
|
* the detection routine to fall back to other discovery mechanisms.
|
|
*
|
|
* @since 4.1.7
|
|
*
|
|
* @param string|null $apache_version The sanitized Apache version, or null if detection failed.
|
|
*/
|
|
$apache_version = apply_filters( 'wpvulnerability_detect_webserver_apache_version', $apache_version );
|
|
|
|
if ( null !== $apache_version ) {
|
|
$apache_version = $normalize_apache_version( $apache_version );
|
|
}
|
|
|
|
return $apache_version;
|
|
}
|
|
|
|
/**
|
|
* Detects the web server software and version from the SERVER_SOFTWARE server variable.
|
|
*
|
|
* This function attempts to identify the web server software (e.g., Apache, nginx) and its version
|
|
* based on the 'SERVER_SOFTWARE' environment variable provided by the server. It uses regular expressions
|
|
* to parse the web server name and version. The function also sanitizes the detected version number
|
|
* to a standard format (major.minor.patch).
|
|
*
|
|
* @since 3.2.0 Introduced.
|
|
*
|
|
* @return array{id: string|null, name: string|null, version: string|null} Web server information.
|
|
*/
|
|
function wpvulnerability_detect_webserver() {
|
|
// Initialize an array to hold the web server information.
|
|
$webserver = array(
|
|
'id' => null,
|
|
'name' => null,
|
|
'version' => null,
|
|
);
|
|
|
|
$apache_version = wpvulnerability_get_apache_version();
|
|
|
|
if ( null !== $apache_version ) {
|
|
$webserver['id'] = 'apache';
|
|
$webserver['name'] = 'Apache HTTPD';
|
|
$webserver['version'] = $apache_version;
|
|
|
|
return $webserver;
|
|
}
|
|
|
|
// Check if the SERVER_SOFTWARE variable is set.
|
|
if ( isset( $_SERVER['SERVER_SOFTWARE'] ) ) {
|
|
// Trim and sanitize the server software string.
|
|
$server_sw_raw = $_SERVER['SERVER_SOFTWARE']; // phpcs:ignore
|
|
$webserver_software = trim( wp_kses( is_string( $server_sw_raw ) ? wp_unslash( $server_sw_raw ) : '', 'strip' ) );
|
|
|
|
// Use regular expressions to extract the web server name and version.
|
|
if ( preg_match( '/^([^\s\/]+)\/?([^\s]*)/', $webserver_software, $matches ) ) {
|
|
$webserver['name'] = trim( (string) $matches[1] );
|
|
$webserver['version'] = trim( (string) $matches[2] );
|
|
|
|
// Replace "-N" at the end of the version with ".N" if present.
|
|
if ( preg_match( '/-(\d+)$/', $webserver['version'], $suffix_matches ) ) {
|
|
$webserver['version'] = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $webserver['version'] );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Normalize and set the web server ID based on the detected name.
|
|
if ( ! empty( $webserver['name'] ) ) {
|
|
$normalized_name = strtolower( (string) $webserver['name'] );
|
|
$webserver['id'] = trim( (string) preg_replace( '/[^a-z0-9]+/', '-', $normalized_name ), '-' );
|
|
switch ( $normalized_name ) {
|
|
case 'httpd':
|
|
case 'apache':
|
|
$webserver['id'] = 'apache';
|
|
$webserver['name'] = 'Apache HTTPD';
|
|
break;
|
|
case 'nginx':
|
|
$webserver['id'] = 'nginx';
|
|
$webserver['name'] = 'nginx';
|
|
break;
|
|
case 'openresty':
|
|
$webserver['id'] = 'nginx';
|
|
$webserver['name'] = 'OpenResty';
|
|
break;
|
|
case 'tengine':
|
|
$webserver['id'] = 'nginx';
|
|
$webserver['name'] = 'Tengine';
|
|
break;
|
|
// Additional web servers can be added here.
|
|
}
|
|
}
|
|
|
|
// If the version is not detected, try to get it from the OS.
|
|
if ( empty( $webserver['version'] ) && wpvulnerability_can_shell_exec() ) {
|
|
if ( 'apache' === $webserver['id'] && wpvulnerability_analyze_filter( 'apache' ) ) {
|
|
$apache_version = wpvulnerability_safe_shell_exec( 'apache', 'apache2 -v' );
|
|
if ( empty( $apache_version ) ) {
|
|
$apache_version = wpvulnerability_safe_shell_exec( 'apache', 'httpd -v' );
|
|
}
|
|
if ( ! empty( $apache_version ) && preg_match( '/Apache\/([\d.]+)/', $apache_version, $version_matches ) ) {
|
|
$webserver['version'] = $version_matches[1];
|
|
// Replace "-N" at the end with ".N" if present.
|
|
if ( preg_match( '/-(\d+)$/', $webserver['version'], $suffix_matches ) ) {
|
|
$webserver['version'] = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $webserver['version'] );
|
|
}
|
|
}
|
|
} elseif ( 'nginx' === $webserver['id'] && wpvulnerability_analyze_filter( 'nginx' ) ) {
|
|
$nginx_version = wpvulnerability_safe_shell_exec( 'nginx', 'nginx -v' );
|
|
if ( ! empty( $nginx_version ) && preg_match( '/nginx\/([\d.]+)/', $nginx_version, $version_matches ) ) {
|
|
$webserver['version'] = $version_matches[1];
|
|
// Replace "-N" at the end with ".N" if present.
|
|
if ( preg_match( '/-(\d+)$/', $webserver['version'], $suffix_matches ) ) {
|
|
$webserver['version'] = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $webserver['version'] );
|
|
}
|
|
} else {
|
|
$angie_version = wpvulnerability_safe_shell_exec( 'nginx', 'angie -v' );
|
|
if ( ! empty( $angie_version ) && preg_match( '/angie\/([\d.]+)/', $angie_version, $version_matches ) ) {
|
|
$webserver['version'] = $version_matches[1];
|
|
// Replace "-N" at the end with ".N" if present.
|
|
if ( preg_match( '/-(\d+)$/', $webserver['version'], $suffix_matches ) ) {
|
|
$webserver['version'] = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $webserver['version'] );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sanitize and validate the web server version format.
|
|
if ( null !== $webserver['version'] && '' !== $webserver['version'] ) {
|
|
// Sanitize the version number to ensure it's in a 'major.minor.patch' format.
|
|
$webserver['version'] = wpvulnerability_sanitize_and_validate_version( $webserver['version'] );
|
|
|
|
if ( null !== $webserver['version'] && ! preg_match( '/^\d+(?:\.\d+)*$/', $webserver['version'] ) ) {
|
|
$webserver['version'] = null;
|
|
}
|
|
}
|
|
|
|
// Return the detected web server information.
|
|
return $webserver;
|
|
}
|
|
|
|
/**
|
|
* Fires plugin and legacy hooks when the database reports an error.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @param string $last_error The database error message.
|
|
*
|
|
* @return void
|
|
*/
|
|
function wpvulnerability_handle_wpdb_last_error( $last_error ) {
|
|
$last_error = trim( (string) $last_error );
|
|
|
|
if ( '' === $last_error ) {
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* Fires when WPVulnerability detects a database error while debugging is enabled.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @param string $last_error The database error message.
|
|
*/
|
|
do_action( 'wpvulnerability_wpdb_last_error', $last_error );
|
|
|
|
// Preserve backward compatibility with the legacy hook name.
|
|
do_action_deprecated( 'wpdb_last_error', array( $last_error ), '4.3.0', 'wpvulnerability_wpdb_last_error' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
|
|
}
|
|
|
|
/**
|
|
* Detects the SQL server software and version from the database server.
|
|
*
|
|
* This function identifies the SQL server software (e.g., MariaDB, MySQL) and its version
|
|
* by querying the database using the 'SHOW VARIABLES' command. It parses the server name
|
|
* and version using the results and sanitizes the detected version number to a standard format (major.minor.patch).
|
|
*
|
|
* @since 3.4.0
|
|
*
|
|
* @return array{id: string|null, name: string|null, version: string|null} SQL server information.
|
|
*/
|
|
function wpvulnerability_detect_sqlserver() {
|
|
// Initialize an array to hold the SQL server information.
|
|
$sqlserver = array(
|
|
'id' => null,
|
|
'name' => null,
|
|
'version' => null,
|
|
);
|
|
|
|
global $wpdb;
|
|
|
|
$version_source = '';
|
|
$server_info_string = '';
|
|
|
|
// Query to get the database server type (version_comment).
|
|
$database_results = $wpdb->get_results( $wpdb->prepare( 'SHOW VARIABLES LIKE %s', 'version_comment' ) ); // phpcs:ignore
|
|
if ( $wpdb->last_error && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
|
wpvulnerability_handle_wpdb_last_error( $wpdb->last_error );
|
|
}
|
|
|
|
// Process the results to determine the database type.
|
|
if ( ! empty( $database_results ) && isset( $database_results[0]->Value ) ) {
|
|
$possible_database = trim( (string) $database_results[0]->Value );
|
|
|
|
if ( false !== stripos( $possible_database, 'mariadb' ) ) {
|
|
$sqlserver['id'] = 'mariadb';
|
|
$sqlserver['name'] = 'MariaDB';
|
|
} elseif ( false !== stripos( $possible_database, 'mysql' ) ) {
|
|
$sqlserver['id'] = 'mysql';
|
|
$sqlserver['name'] = 'MySQL';
|
|
}
|
|
}
|
|
|
|
// Query to get the database server version.
|
|
$version_results = $wpdb->get_results( $wpdb->prepare( 'SHOW VARIABLES LIKE %s', 'version' ) ); // phpcs:ignore
|
|
if ( $wpdb->last_error && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
|
wpvulnerability_handle_wpdb_last_error( $wpdb->last_error );
|
|
}
|
|
|
|
if ( ! empty( $version_results ) && isset( $version_results[0]->Value ) ) {
|
|
$version_source = trim( (string) $version_results[0]->Value );
|
|
}
|
|
|
|
if ( empty( $sqlserver['id'] ) ) {
|
|
$server_info_string = trim( (string) $wpdb->db_server_info() );
|
|
|
|
if ( '' === $server_info_string ) {
|
|
$server_info_string = $wpdb->get_var( 'SELECT VERSION()' ); // phpcs:ignore
|
|
if ( $wpdb->last_error && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
|
wpvulnerability_handle_wpdb_last_error( $wpdb->last_error );
|
|
}
|
|
$server_info_string = is_string( $server_info_string ) ? trim( $server_info_string ) : '';
|
|
}
|
|
|
|
if ( '' !== $server_info_string ) {
|
|
if ( false !== stripos( $server_info_string, 'mariadb' ) ) {
|
|
$sqlserver['id'] = 'mariadb';
|
|
$sqlserver['name'] = 'MariaDB';
|
|
} elseif ( false !== stripos( $server_info_string, 'mysql' ) ) {
|
|
$sqlserver['id'] = 'mysql';
|
|
$sqlserver['name'] = 'MySQL';
|
|
}
|
|
|
|
if ( '' === $version_source ) {
|
|
$version_source = $server_info_string;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( '' !== $version_source ) {
|
|
if ( preg_match( '/(\d+\.\d+\.\d+(?:-\d+)?)/', $version_source, $match ) ||
|
|
preg_match( '/(\d+\.\d+(?:-\d+)?)/', $version_source, $match ) ) {
|
|
$sqlserver['version'] = $match[1];
|
|
} elseif ( 'mysql' === $sqlserver['id'] ) {
|
|
// Fallback to the entire version string if regex doesn't match.
|
|
$sqlserver['version'] = $version_source;
|
|
}
|
|
}
|
|
|
|
// Replace "-N" at the end with ".N" if present.
|
|
if ( ! empty( $sqlserver['version'] ) && preg_match( '/-(\d+)$/', $sqlserver['version'], $suffix_matches ) ) {
|
|
$sqlserver['version'] = preg_replace( '/-(\d+)$/', '.' . $suffix_matches[1], $sqlserver['version'] );
|
|
}
|
|
|
|
// Sanitize and validate the version format.
|
|
if ( ! empty( $sqlserver['version'] ) ) {
|
|
$sqlserver['version'] = wpvulnerability_sanitize_and_validate_version( $sqlserver['version'] );
|
|
}
|
|
|
|
// Return the detected SQL server information.
|
|
return $sqlserver;
|
|
}
|
|
|
|
/**
|
|
* Returns a human-readable HTML entity for the given comparison operator.
|
|
*
|
|
* This function takes a comparison operator in string format and returns
|
|
* its corresponding HTML entity for better readability in web contexts.
|
|
*
|
|
* @since 2.0.0
|
|
*
|
|
* @param string $op The operator string to prettify.
|
|
*
|
|
* @return string The pretty operator HTML string.
|
|
*/
|
|
function wpvulnerability_pretty_operator( $op ) {
|
|
// Normalize the operator string to lowercase and trim whitespace.
|
|
$op = trim( strtolower( (string) $op ) );
|
|
|
|
// Define an associative array mapping operators to their HTML entities.
|
|
$operator_map = array(
|
|
'lt' => '< ', // Less than.
|
|
'le' => '≤ ', // Less than or equal to.
|
|
'gt' => '> ', // Greater than.
|
|
'ge' => '≥ ', // Greater than or equal to.
|
|
'eq' => '= ', // Equal to.
|
|
'ne' => '≠ ', // Not equal to.
|
|
);
|
|
|
|
// Return the corresponding HTML entity, or the original operator if not recognized.
|
|
return isset( $operator_map[ $op ] ) ? $operator_map[ $op ] : $op;
|
|
}
|
|
|
|
/**
|
|
* Returns a human-readable severity level.
|
|
*
|
|
* This function takes a severity string and returns a human-readable
|
|
* severity level, localized for translation.
|
|
*
|
|
* @since 2.0.0
|
|
*
|
|
* @param string $severity The severity string to prettify.
|
|
*
|
|
* @return string The human-readable severity string.
|
|
*/
|
|
function wpvulnerability_severity( $severity ) {
|
|
// Normalize the severity string to lowercase and trim whitespace.
|
|
$severity = trim( strtolower( (string) $severity ) );
|
|
|
|
// Define an associative array mapping severity codes to their human-readable equivalents.
|
|
// Handles both legacy single-char codes (cvss.severity) and full-word values (cvss3.severity).
|
|
$severity_map = array(
|
|
'n' => __( 'None', 'wpvulnerability' ),
|
|
'none' => __( 'None', 'wpvulnerability' ),
|
|
'l' => __( 'Low', 'wpvulnerability' ),
|
|
'low' => __( 'Low', 'wpvulnerability' ),
|
|
'm' => __( 'Medium', 'wpvulnerability' ),
|
|
'medium' => __( 'Medium', 'wpvulnerability' ),
|
|
'h' => __( 'High', 'wpvulnerability' ),
|
|
'high' => __( 'High', 'wpvulnerability' ),
|
|
'c' => __( 'Critical', 'wpvulnerability' ),
|
|
'critical' => __( 'Critical', 'wpvulnerability' ),
|
|
);
|
|
|
|
// Return the corresponding human-readable severity, or the original if not recognized.
|
|
return isset( $severity_map[ $severity ] ) ? $severity_map[ $severity ] : $severity;
|
|
}
|
|
|
|
/**
|
|
* Retrieves vulnerabilities information from the API.
|
|
*
|
|
* This function fetches vulnerability information based on the provided type and slug.
|
|
* It supports caching to minimize API requests and improve performance.
|
|
*
|
|
* @since 2.0.0
|
|
*
|
|
* @param string $type The type of vulnerability. Can be 'core', 'plugin', or 'theme'.
|
|
* @param string $slug The slug of the plugin or theme. For core vulnerabilities, it is the version string.
|
|
* @param int $cache Optional. Whether to use cache. Default is 1 (true).
|
|
*
|
|
* @return array<mixed>|false An array with the vulnerability information or false if there's an error.
|
|
*/
|
|
function wpvulnerability_get( $type, $slug = '', $cache = 1 ) {
|
|
// Validate vulnerability type and normalize.
|
|
$type = strtolower( trim( (string) $type ) );
|
|
$valid_types = array( 'core', 'plugin', 'theme' );
|
|
|
|
if ( ! in_array( $type, $valid_types, true ) ) {
|
|
wp_die( 'Unknown vulnerability type sent.' );
|
|
}
|
|
|
|
// Validate slug for plugin or theme.
|
|
if ( ( 'plugin' === $type || 'theme' === $type ) && empty( sanitize_title( $slug ) ) ) {
|
|
return false;
|
|
}
|
|
|
|
// Validate slug for core.
|
|
if ( 'core' === $type && ! wpvulnerability_sanitize_version( $slug ) ) {
|
|
return false;
|
|
}
|
|
|
|
// Cache key.
|
|
$key = 'wpvulnerability_' . $type . '_' . $slug;
|
|
|
|
// Attempt to retrieve cached data.
|
|
$vulnerability_data = $cache ? ( is_multisite() ? get_site_transient( $key ) : get_transient( $key ) ) : null;
|
|
|
|
// If not cached, fetch updated data.
|
|
if ( empty( $vulnerability_data ) ) {
|
|
$url = WPVULNERABILITY_API_HOST . $type . '/' . $slug . '/';
|
|
$response = wp_remote_get( $url, array( 'timeout' => 2.5 ) );
|
|
wpvulnerability_maybe_log_api_response( $url, $response );
|
|
|
|
if ( ! is_wp_error( $response ) ) {
|
|
$body = wp_remote_retrieve_body( $response );
|
|
|
|
// Cache the response data.
|
|
if ( is_multisite() ) {
|
|
set_site_transient( $key, $body, HOUR_IN_SECONDS * wpvulnerability_cache_hours() );
|
|
} else {
|
|
set_transient( $key, $body, HOUR_IN_SECONDS * wpvulnerability_cache_hours() );
|
|
}
|
|
|
|
$vulnerability_data = $body; // Use the fresh data.
|
|
}
|
|
}
|
|
|
|
if ( ! is_string( $vulnerability_data ) || '' === $vulnerability_data ) {
|
|
return false;
|
|
}
|
|
$decoded = json_decode( $vulnerability_data, true );
|
|
return is_array( $decoded ) ? $decoded : false;
|
|
}
|
|
|
|
/**
|
|
* Retrieve vulnerabilities for a specific version of WordPress Core.
|
|
*
|
|
* This function fetches vulnerability information for a given version of WordPress Core.
|
|
* If no version is provided, it retrieves vulnerabilities for the currently installed version.
|
|
* It supports caching to minimize API requests and improve performance.
|
|
*
|
|
* @since 2.0.0
|
|
*
|
|
* @param string|null $version The version number of WordPress Core. If null, retrieves for the installed version.
|
|
* @param int $cache Optional. Whether to use cache. Default is 1 (true).
|
|
*
|
|
* @return list<array<string, mixed>>|false Array of vulnerabilities, or false on error.
|
|
*/
|
|
function wpvulnerability_get_core( $version = null, $cache = 1 ) {
|
|
// Sanitize the version number.
|
|
if ( ! wpvulnerability_sanitize_version( $version ) ) {
|
|
$version = null; // Reset version if sanitization fails.
|
|
}
|
|
|
|
// If version number is null, retrieve for the installed version.
|
|
if ( is_null( $version ) ) {
|
|
$version = get_bloginfo( 'version' );
|
|
}
|
|
|
|
// Get vulnerabilities from the API.
|
|
$response = wpvulnerability_get( 'core', $version, $cache );
|
|
|
|
// Check for errors in the response.
|
|
if ( ! is_array( $response ) || ( isset( $response['error'] ) && $response['error'] ) ) {
|
|
return false;
|
|
}
|
|
|
|
$data_section = isset( $response['data'] ) && is_array( $response['data'] ) ? $response['data'] : array();
|
|
$vuln_raw = isset( $data_section['vulnerability'] ) && is_array( $data_section['vulnerability'] ) ? $data_section['vulnerability'] : array();
|
|
|
|
if ( empty( $vuln_raw ) ) {
|
|
return false;
|
|
}
|
|
|
|
// Process vulnerabilities and return as an array.
|
|
$vulnerabilities = array();
|
|
foreach ( $vuln_raw as $v ) {
|
|
if ( ! is_array( $v ) ) {
|
|
continue;
|
|
}
|
|
$v_name = $v['name'] ?? null;
|
|
$v_link = $v['link'] ?? null;
|
|
$vulnerabilities[] = array(
|
|
'name' => is_scalar( $v_name ) ? wp_kses( (string) $v_name, 'strip' ) : null,
|
|
'link' => is_scalar( $v_link ) ? esc_url_raw( (string) $v_link ) : null,
|
|
'source' => isset( $v['source'] ) ? $v['source'] : null,
|
|
'impact' => isset( $v['impact'] ) ? $v['impact'] : null,
|
|
'uuid' => is_scalar( $v['uuid'] ?? '' ) ? (string) ( $v['uuid'] ?? '' ) : '',
|
|
);
|
|
}
|
|
|
|
return $vulnerabilities;
|
|
}
|
|
|
|
/**
|
|
* Determines if a vulnerability applies to the specified version of the plugin.
|
|
*
|
|
* @since 3.5.0 Introduced.
|
|
*
|
|
* @param array<mixed> $v The vulnerability data.
|
|
* @param string $version The version of the plugin.
|
|
*
|
|
* @return bool True if the vulnerability applies, false otherwise.
|
|
*/
|
|
function wpvulnerability_is_vulnerability_applicable( $v, $version ) {
|
|
$op = isset( $v['operator'] ) && is_array( $v['operator'] ) ? $v['operator'] : array();
|
|
|
|
$min_op = isset( $op['min_operator'] ) && is_scalar( $op['min_operator'] ) ? (string) $op['min_operator'] : '';
|
|
$max_op = isset( $op['max_operator'] ) && is_scalar( $op['max_operator'] ) ? (string) $op['max_operator'] : '';
|
|
$min_ver = isset( $op['min_version'] ) && is_scalar( $op['min_version'] ) ? (string) $op['min_version'] : '';
|
|
$max_ver = isset( $op['max_version'] ) && is_scalar( $op['max_version'] ) ? (string) $op['max_version'] : '';
|
|
|
|
// Check if the vulnerability has minimum and maximum versions.
|
|
if ( '' !== $min_op && '' !== $max_op ) {
|
|
return version_compare( $version, $min_ver, $min_op ) &&
|
|
version_compare( $version, $max_ver, $max_op );
|
|
}
|
|
|
|
// Check if the vulnerability has only a maximum version.
|
|
if ( '' !== $max_op ) {
|
|
return version_compare( $version, $max_ver, $max_op );
|
|
}
|
|
|
|
// Check if the vulnerability has only a minimum version.
|
|
if ( '' !== $min_op ) {
|
|
return version_compare( $version, $min_ver, $min_op );
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Retrieves vulnerabilities for a specified plugin, optionally returning general plugin data.
|
|
*
|
|
* This function sanitizes the plugin slug and verifies the version number before querying the vulnerability API.
|
|
* If `$data` is set to 1, it returns general information about the plugin instead of vulnerabilities.
|
|
* The function returns an array of vulnerabilities or plugin data based on the `$data` parameter, or `false`
|
|
* if no vulnerabilities are found or the version number is invalid and `$data` is not set.
|
|
*
|
|
* @since 2.0.0 Introduced.
|
|
*
|
|
* @param string $slug The slug of the plugin to check for vulnerabilities.
|
|
* @param string $version The version of the plugin to check. The function may return `false` if this is invalid and `$data` is not set.
|
|
* @param int $data Optional. Set to 1 to return general plugin data instead of vulnerabilities. Default 0 (return vulnerabilities).
|
|
* @param int $cache Optional. Whether to use cache. Default is 1 (true).
|
|
*
|
|
* @return list<array<string, mixed>>|array<string, mixed>|false An array of vulnerabilities or plugin data if `$data` is set to 1, or `false` if no vulnerabilities are found or the version number is invalid and `$data` is not set.
|
|
*/
|
|
function wpvulnerability_get_plugin( $slug, $version, $data = 0, $cache = 1 ) {
|
|
// Sanitize the plugin slug.
|
|
$slug = sanitize_title( $slug );
|
|
|
|
// If the version number is invalid, return false unless $data is set.
|
|
if ( ! wpvulnerability_sanitize_version( $version ) && ! $data ) {
|
|
return false;
|
|
}
|
|
|
|
// Get the response from the vulnerability API.
|
|
$response = wpvulnerability_get( 'plugin', $slug, $cache );
|
|
|
|
// If $data is set to 1, return general plugin data.
|
|
if ( 1 === $data && is_array( $response ) ) {
|
|
$resp_data = isset( $response['data'] ) && is_array( $response['data'] ) ? $response['data'] : array();
|
|
$name_raw = $resp_data['name'] ?? '';
|
|
$link_raw = $resp_data['link'] ?? '';
|
|
$latest_raw = $resp_data['latest'] ?? 0;
|
|
$closed_raw = $resp_data['closed'] ?? 0;
|
|
return array(
|
|
'name' => wp_kses( is_scalar( $name_raw ) ? (string) $name_raw : '', 'strip' ),
|
|
'link' => esc_url( is_scalar( $link_raw ) ? (string) $link_raw : '' ),
|
|
'latest' => number_format( is_scalar( $latest_raw ) ? (int) $latest_raw : 0, 0, '.', '' ),
|
|
'closed' => number_format( is_scalar( $closed_raw ) ? (int) $closed_raw : 0, 0, '.', '' ),
|
|
);
|
|
}
|
|
|
|
// Check for errors in the response.
|
|
if ( ! is_array( $response ) || ( isset( $response['error'] ) && $response['error'] ) ) {
|
|
return false;
|
|
}
|
|
|
|
$resp_data2 = isset( $response['data'] ) && is_array( $response['data'] ) ? $response['data'] : array();
|
|
$vuln_list = isset( $resp_data2['vulnerability'] ) && is_array( $resp_data2['vulnerability'] ) ? $resp_data2['vulnerability'] : array();
|
|
|
|
if ( empty( $vuln_list ) ) {
|
|
return false;
|
|
}
|
|
|
|
// Create an empty array to store vulnerabilities.
|
|
$vulnerabilities = array();
|
|
|
|
// Loop through each vulnerability.
|
|
foreach ( $vuln_list as $v ) {
|
|
if ( ! is_array( $v ) ) {
|
|
continue;
|
|
}
|
|
// Check version constraints and add vulnerabilities accordingly.
|
|
if ( wpvulnerability_is_vulnerability_applicable( $v, $version ) ) {
|
|
$op = isset( $v['operator'] ) && is_array( $v['operator'] ) ? $v['operator'] : array();
|
|
$min_op_raw = $op['min_operator'] ?? '';
|
|
$max_op_raw = $op['max_operator'] ?? '';
|
|
$min_ver_raw = $op['min_version'] ?? '';
|
|
$max_ver_raw = $op['max_version'] ?? '';
|
|
$unfixed_raw = $op['unfixed'] ?? 0;
|
|
$closed_raw2 = $op['closed'] ?? 0;
|
|
$min_op = is_scalar( $min_op_raw ) ? (string) $min_op_raw : '';
|
|
$max_op = is_scalar( $max_op_raw ) ? (string) $max_op_raw : '';
|
|
$min_ver = is_scalar( $min_ver_raw ) ? (string) $min_ver_raw : '';
|
|
$max_ver = is_scalar( $max_ver_raw ) ? (string) $max_ver_raw : '';
|
|
$v_name_raw = $v['name'] ?? '';
|
|
$v_desc_raw = $v['description'] ?? '';
|
|
$vulnerabilities[] = array(
|
|
'name' => wp_kses( is_scalar( $v_name_raw ) ? (string) $v_name_raw : '', 'strip' ),
|
|
'description' => wp_kses_post( is_scalar( $v_desc_raw ) ? (string) $v_desc_raw : '' ),
|
|
'versions' => wp_kses(
|
|
wpvulnerability_pretty_operator( $min_op ) . $min_ver . ' - ' .
|
|
wpvulnerability_pretty_operator( $max_op ) . $max_ver,
|
|
'strip'
|
|
),
|
|
'version' => wp_kses( '' !== $min_ver ? $min_ver : $max_ver, 'strip' ),
|
|
'unfixed' => is_scalar( $unfixed_raw ) ? (int) $unfixed_raw : 0,
|
|
'closed' => is_scalar( $closed_raw2 ) ? (int) $closed_raw2 : 0,
|
|
'source' => isset( $v['source'] ) ? $v['source'] : null,
|
|
'impact' => isset( $v['impact'] ) ? $v['impact'] : null,
|
|
'uuid' => is_scalar( $v['uuid'] ?? '' ) ? (string) ( $v['uuid'] ?? '' ) : '',
|
|
);
|
|
}
|
|
}
|
|
|
|
return $vulnerabilities;
|
|
}
|
|
|
|
/**
|
|
* Get vulnerabilities for a specific theme.
|
|
*
|
|
* This function retrieves and sanitizes the theme slug and version before querying the vulnerability API.
|
|
* It returns an array of vulnerabilities if any are found, or false if there are none.
|
|
*
|
|
* @since 3.5.0
|
|
*
|
|
* @param string $slug Slug of the theme.
|
|
* @param string $version Version of the theme.
|
|
* @param int $cache Optional. Whether to use cache. Default is 1 (true).
|
|
*
|
|
* @return list<array<string, mixed>>|false Returns an array of vulnerabilities, or false if there are none.
|
|
*/
|
|
function wpvulnerability_get_theme( $slug, $version, $cache = 1 ) {
|
|
// Sanitize the theme slug.
|
|
$slug = sanitize_title( $slug );
|
|
|
|
// Validate the version number.
|
|
if ( ! wpvulnerability_sanitize_version( $version ) ) {
|
|
return false; // Return false if the version is invalid.
|
|
}
|
|
|
|
// Get the response from the vulnerability API.
|
|
$response = wpvulnerability_get( 'theme', $slug, $cache );
|
|
|
|
// Check for errors in the response.
|
|
if ( ! is_array( $response ) || ( isset( $response['error'] ) && $response['error'] ) ) {
|
|
return false;
|
|
}
|
|
|
|
$theme_data = isset( $response['data'] ) && is_array( $response['data'] ) ? $response['data'] : array();
|
|
$vuln_list = isset( $theme_data['vulnerability'] ) && is_array( $theme_data['vulnerability'] ) ? $theme_data['vulnerability'] : array();
|
|
|
|
if ( empty( $vuln_list ) ) {
|
|
return false;
|
|
}
|
|
|
|
// Process each vulnerability.
|
|
$vulnerabilities = array();
|
|
foreach ( $vuln_list as $v ) {
|
|
if ( ! is_array( $v ) ) {
|
|
continue;
|
|
}
|
|
// Check if the version falls within the min and max operator range.
|
|
if ( wpvulnerability_is_vulnerability_applicable( $v, $version ) ) {
|
|
$op = isset( $v['operator'] ) && is_array( $v['operator'] ) ? $v['operator'] : array();
|
|
$min_op_raw = $op['min_operator'] ?? '';
|
|
$max_op_raw = $op['max_operator'] ?? '';
|
|
$min_ver_raw = $op['min_version'] ?? '';
|
|
$max_ver_raw = $op['max_version'] ?? '';
|
|
$unfixed_raw = $op['unfixed'] ?? 0;
|
|
$closed_raw = $op['closed'] ?? 0;
|
|
$min_op = is_scalar( $min_op_raw ) ? (string) $min_op_raw : '';
|
|
$max_op = is_scalar( $max_op_raw ) ? (string) $max_op_raw : '';
|
|
$min_ver = is_scalar( $min_ver_raw ) ? (string) $min_ver_raw : '';
|
|
$max_ver = is_scalar( $max_ver_raw ) ? (string) $max_ver_raw : '';
|
|
$v_name_raw = $v['name'] ?? '';
|
|
$v_desc_raw = $v['description'] ?? '';
|
|
$vulnerabilities[] = array(
|
|
'name' => wp_kses( is_scalar( $v_name_raw ) ? (string) $v_name_raw : '', 'strip' ),
|
|
'description' => wp_kses_post( is_scalar( $v_desc_raw ) ? (string) $v_desc_raw : '' ),
|
|
'versions' => wp_kses(
|
|
wpvulnerability_pretty_operator( $min_op ) . $min_ver . ' - ' .
|
|
wpvulnerability_pretty_operator( $max_op ) . $max_ver,
|
|
'strip'
|
|
),
|
|
'version' => wp_kses( '' !== $min_ver ? $min_ver : $max_ver, 'strip' ),
|
|
'unfixed' => is_scalar( $unfixed_raw ) ? (int) $unfixed_raw : 0,
|
|
'closed' => is_scalar( $closed_raw ) ? (int) $closed_raw : 0,
|
|
'source' => isset( $v['source'] ) ? $v['source'] : null,
|
|
'impact' => isset( $v['impact'] ) ? $v['impact'] : null,
|
|
'uuid' => is_scalar( $v['uuid'] ?? '' ) ? (string) ( $v['uuid'] ?? '' ) : '',
|
|
);
|
|
}
|
|
}
|
|
|
|
return $vulnerabilities;
|
|
}
|
|
|
|
/**
|
|
* Get statistics.
|
|
*
|
|
* Returns an array with statistical information about vulnerabilities and their respective products.
|
|
*
|
|
* @since 2.0.0
|
|
*
|
|
* @param int $cache Optional. Whether to use cache. Default is 1 (true).
|
|
*
|
|
* @return array<string, mixed>|false Returns an array with the statistical information if successful, false otherwise.
|
|
*/
|
|
function wpvulnerability_get_statistics( $cache = 1 ) {
|
|
$key = 'wpvulnerability_stats';
|
|
|
|
// Attempt to get cached statistics.
|
|
$vulnerability = $cache ? ( is_multisite() ? get_site_transient( $key ) : get_transient( $key ) ) : null;
|
|
|
|
// If cached statistics are not available, retrieve them from the API.
|
|
if ( empty( $vulnerability ) ) {
|
|
$url = WPVULNERABILITY_API_HOST;
|
|
$response = wp_remote_get( $url, array( 'timeout' => 2.5 ) );
|
|
wpvulnerability_maybe_log_api_response( $url, $response );
|
|
|
|
if ( ! is_wp_error( $response ) ) {
|
|
$body = wp_remote_retrieve_body( $response );
|
|
// Cache the response data.
|
|
if ( is_multisite() ) {
|
|
set_site_transient( $key, $body, HOUR_IN_SECONDS * wpvulnerability_cache_hours() );
|
|
} else {
|
|
set_transient( $key, $body, HOUR_IN_SECONDS * wpvulnerability_cache_hours() );
|
|
}
|
|
$vulnerability = $body; // Use the fresh data.
|
|
}
|
|
}
|
|
|
|
// Decode the JSON response and check for statistics.
|
|
$response = json_decode( is_string( $vulnerability ) ? $vulnerability : '', true );
|
|
if ( ! is_array( $response ) || ! isset( $response['stats'] ) || ! is_array( $response['stats'] ) ) {
|
|
return false;
|
|
}
|
|
|
|
// Extract typed intermediate arrays to avoid mixed-type access errors.
|
|
$stats = $response['stats'];
|
|
$products = isset( $stats['products'] ) && is_array( $stats['products'] ) ? $stats['products'] : array();
|
|
$sources_raw = isset( $stats['vulnerabilities'] ) && is_array( $stats['vulnerabilities'] ) ? $stats['vulnerabilities'] : array();
|
|
$behind = isset( $response['behindtheproject'] ) && is_array( $response['behindtheproject'] ) ? $response['behindtheproject'] : array();
|
|
$updated_raw = $response['updated'] ?? 0;
|
|
$updated = is_scalar( $updated_raw ) ? (int) $updated_raw : 0;
|
|
|
|
$sponsors = array();
|
|
if ( isset( $behind['sponsors'] ) && is_array( $behind['sponsors'] ) && count( $behind['sponsors'] ) ) {
|
|
foreach ( $behind['sponsors'] as $s ) {
|
|
$sponsors[] = $s;
|
|
}
|
|
}
|
|
|
|
$contributors = array();
|
|
if ( isset( $behind['contributors'] ) && is_array( $behind['contributors'] ) && count( $behind['contributors'] ) ) {
|
|
foreach ( $behind['contributors'] as $s ) {
|
|
$contributors[] = $s;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cast a scalar value to int safely.
|
|
*
|
|
* @param mixed $x Value to cast.
|
|
* @return int
|
|
*/
|
|
$to_int = static function ( $x ): int {
|
|
return is_scalar( $x ) ? (int) $x : 0;
|
|
};
|
|
|
|
// Build per-source vulnerability breakdown (stats.vulnerabilities.{source}).
|
|
$sources_data = array();
|
|
$known_sources = array( 'cve', 'euvd', 'jvn', 'patchstack', 'wpscan', 'wordfence' );
|
|
foreach ( $known_sources as $src ) {
|
|
if ( isset( $sources_raw[ $src ] ) && is_array( $sources_raw[ $src ] ) ) {
|
|
$src_data = $sources_raw[ $src ];
|
|
$sources_data[ $src ] = array(
|
|
'core' => $to_int( $src_data['core'] ?? 0 ),
|
|
'plugins' => $to_int( $src_data['plugins'] ?? 0 ),
|
|
'themes' => $to_int( $src_data['themes'] ?? 0 ),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Return an array with statistical information.
|
|
return array(
|
|
'core' => array(
|
|
'versions' => $to_int( $products['core'] ?? 0 ),
|
|
),
|
|
'plugins' => array(
|
|
'products' => $to_int( $products['plugins'] ?? 0 ),
|
|
'vulnerabilities' => $to_int( $stats['plugins'] ?? 0 ),
|
|
),
|
|
'themes' => array(
|
|
'products' => $to_int( $products['themes'] ?? 0 ),
|
|
'vulnerabilities' => $to_int( $stats['themes'] ?? 0 ),
|
|
),
|
|
'php' => array(
|
|
'vulnerabilities' => $to_int( $stats['php'] ?? 0 ),
|
|
),
|
|
'apache' => array(
|
|
'vulnerabilities' => $to_int( $stats['apache'] ?? 0 ),
|
|
),
|
|
'nginx' => array(
|
|
'vulnerabilities' => $to_int( $stats['nginx'] ?? 0 ),
|
|
),
|
|
'mariadb' => array(
|
|
'vulnerabilities' => $to_int( $stats['mariadb'] ?? 0 ),
|
|
),
|
|
'mysql' => array(
|
|
'vulnerabilities' => $to_int( $stats['mysql'] ?? 0 ),
|
|
),
|
|
'imagemagick' => array(
|
|
'vulnerabilities' => $to_int( $stats['imagemagick'] ?? 0 ),
|
|
),
|
|
'curl' => array(
|
|
'vulnerabilities' => $to_int( $stats['curl'] ?? 0 ),
|
|
),
|
|
'memcached' => array(
|
|
'vulnerabilities' => $to_int( $stats['memcached'] ?? 0 ),
|
|
),
|
|
'redis' => array(
|
|
'vulnerabilities' => $to_int( $stats['redis'] ?? 0 ),
|
|
),
|
|
'sqlite' => array(
|
|
'vulnerabilities' => $to_int( $stats['sqlite'] ?? 0 ),
|
|
),
|
|
'sources' => $sources_data,
|
|
'sponsors' => $sponsors,
|
|
'contributors' => $contributors,
|
|
'updated' => array(
|
|
'unixepoch' => $updated,
|
|
'datetime' => gmdate( 'Y-m-d H:i:s', $updated ),
|
|
'iso8601' => gmdate( 'c', $updated ),
|
|
'rfc2822' => gmdate( 'r', $updated ),
|
|
),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Retrieves the latest vulnerability statistics.
|
|
*
|
|
* This function calls the wpvulnerability API to get fresh statistics related to vulnerabilities
|
|
* and returns the updated information.
|
|
*
|
|
* @since 3.4.0
|
|
*
|
|
* @return array<string, mixed>|false The updated vulnerability statistics, or false on error.
|
|
*/
|
|
function wpvulnerability_get_fresh_statistics() {
|
|
// Call the function to get the latest vulnerability statistics.
|
|
$statistics_api_response = wpvulnerability_get_statistics();
|
|
|
|
// Return the response from the API.
|
|
return $statistics_api_response;
|
|
}
|
|
|
|
/**
|
|
* Retrieves and caches the latest vulnerability statistics.
|
|
*
|
|
* This function retrieves the most recent vulnerability statistics, caches the data,
|
|
* and returns the information as a JSON-encoded array. The cache expiration timestamp is also updated.
|
|
*
|
|
* @since 3.4.0
|
|
*
|
|
* @return string JSON-encoded array containing the vulnerability statistics, or empty string on encoding error.
|
|
*/
|
|
function wpvulnerability_statistics_get() {
|
|
// Retrieve fresh statistics.
|
|
$statistics = wpvulnerability_get_fresh_statistics();
|
|
|
|
// Cache the statistics data and the timestamp for cache expiration.
|
|
$encoded_statistics = wp_json_encode( $statistics );
|
|
if ( false === $encoded_statistics ) {
|
|
$encoded_statistics = '';
|
|
}
|
|
$cache_expiration = number_format( time() + ( 3600 * wpvulnerability_cache_hours() ), 0, '.', '' );
|
|
|
|
if ( is_multisite() ) {
|
|
update_site_option( 'wpvulnerability-statistics', $encoded_statistics );
|
|
update_site_option( 'wpvulnerability-statistics-cache', $cache_expiration );
|
|
} else {
|
|
update_option( 'wpvulnerability-statistics', $encoded_statistics );
|
|
update_option( 'wpvulnerability-statistics-cache', $cache_expiration );
|
|
}
|
|
|
|
// Return the JSON-encoded array of statistics data.
|
|
return $encoded_statistics;
|
|
}
|
|
|
|
/**
|
|
* Get vulnerabilities for a specific product version.
|
|
*
|
|
* This function retrieves vulnerability data for a specified product version.
|
|
* It supports caching to minimize API requests and improve performance.
|
|
*
|
|
* @since 3.5.0
|
|
*
|
|
* @param string $type The type of product (e.g., 'php', 'apache', 'nginx', 'mariadb', 'mysql').
|
|
* @param string $version The version of the product to check.
|
|
* @param int $cache Optional. Whether to use cache. Default is 1 (true).
|
|
*
|
|
* @return list<array<string, mixed>>|false Returns an array of vulnerabilities, or false if there are none.
|
|
*/
|
|
function wpvulnerability_get_vulnerabilities( $type, $version, $cache = 1 ) {
|
|
$key = 'wpvulnerability_' . $type;
|
|
$vulnerability_data = null;
|
|
$vulnerability = array();
|
|
|
|
// Get cached statistics if available.
|
|
if ( $cache ) {
|
|
$vulnerability_data = is_multisite() ? get_site_transient( $key ) : get_transient( $key );
|
|
}
|
|
|
|
// If cached statistics are not available, retrieve them from the API and store them in cache.
|
|
if ( empty( $vulnerability_data ) ) {
|
|
$url = WPVULNERABILITY_API_HOST . $type . '/' . $version . '/';
|
|
$response = wp_remote_get( $url, array( 'timeout' => 2.5 ) );
|
|
wpvulnerability_maybe_log_api_response( $url, $response );
|
|
|
|
if ( ! is_wp_error( $response ) ) {
|
|
$body = wp_remote_retrieve_body( $response );
|
|
if ( $cache ) {
|
|
if ( is_multisite() ) {
|
|
set_site_transient( $key, $body, HOUR_IN_SECONDS * wpvulnerability_cache_hours() );
|
|
} else {
|
|
set_transient( $key, $body, HOUR_IN_SECONDS * wpvulnerability_cache_hours() );
|
|
}
|
|
}
|
|
$vulnerability_data = $body; // Use the fresh data.
|
|
}
|
|
}
|
|
|
|
// If the response does not contain vulnerabilities, return false.
|
|
$response = json_decode( is_string( $vulnerability_data ) ? $vulnerability_data : '', true );
|
|
|
|
if ( ! is_array( $response ) || ( isset( $response['error'] ) && $response['error'] ) ) {
|
|
return false;
|
|
}
|
|
|
|
$resp_data2 = isset( $response['data'] ) && is_array( $response['data'] ) ? $response['data'] : array();
|
|
$vuln_list = isset( $resp_data2['vulnerability'] ) && is_array( $resp_data2['vulnerability'] ) ? $resp_data2['vulnerability'] : array();
|
|
|
|
if ( empty( $vuln_list ) ) {
|
|
return false;
|
|
}
|
|
|
|
// Process each vulnerability.
|
|
foreach ( $vuln_list as $v ) {
|
|
if ( ! is_array( $v ) ) {
|
|
continue;
|
|
}
|
|
$v_name_raw = $v['name'] ?? '';
|
|
$name = is_scalar( $v_name_raw ) ? wp_kses( (string) $v_name_raw, 'strip' ) : '';
|
|
$source = isset( $v['source'] ) ? $v['source'] : null;
|
|
$op = isset( $v['operator'] ) && is_array( $v['operator'] ) ? $v['operator'] : array();
|
|
|
|
$min_op_raw = $op['min_operator'] ?? '';
|
|
$max_op_raw = $op['max_operator'] ?? '';
|
|
$min_ver_raw = $op['min_version'] ?? '';
|
|
$max_ver_raw = $op['max_version'] ?? '';
|
|
$unfixed_raw = $op['unfixed'] ?? 0;
|
|
$min_op = is_scalar( $min_op_raw ) ? (string) $min_op_raw : '';
|
|
$max_op = is_scalar( $max_op_raw ) ? (string) $max_op_raw : '';
|
|
$min_ver = is_scalar( $min_ver_raw ) ? (string) $min_ver_raw : '';
|
|
$max_ver = is_scalar( $max_ver_raw ) ? (string) $max_ver_raw : '';
|
|
$unfixed = is_scalar( $unfixed_raw ) ? (int) $unfixed_raw : 0;
|
|
|
|
$impact = isset( $v['impact'] ) ? $v['impact'] : null;
|
|
$uuid = is_scalar( $v['uuid'] ?? '' ) ? (string) ( $v['uuid'] ?? '' ) : '';
|
|
|
|
// Check if the version falls within the specified min and max operator range.
|
|
if ( '' !== $min_op && '' !== $max_op ) {
|
|
if ( version_compare( $version, $min_ver, $min_op ) &&
|
|
version_compare( $version, $max_ver, $max_op ) ) {
|
|
$vulnerability[] = array(
|
|
'name' => $name,
|
|
'versions' => wp_kses( wpvulnerability_pretty_operator( $min_op ) . $min_ver . ' - ' . wpvulnerability_pretty_operator( $max_op ) . $max_ver, 'strip' ),
|
|
'version' => wp_kses( $min_ver, 'strip' ),
|
|
'unfixed' => $unfixed,
|
|
'source' => $source,
|
|
'impact' => $impact,
|
|
'uuid' => $uuid,
|
|
);
|
|
}
|
|
} elseif ( '' !== $max_op ) {
|
|
if ( version_compare( $version, $max_ver, $max_op ) ) {
|
|
$vulnerability[] = array(
|
|
'name' => $name,
|
|
'versions' => wp_kses( wpvulnerability_pretty_operator( $max_op ) . $max_ver, 'strip' ),
|
|
'version' => wp_kses( $max_ver, 'strip' ),
|
|
'unfixed' => $unfixed,
|
|
'source' => $source,
|
|
'impact' => $impact,
|
|
'uuid' => $uuid,
|
|
);
|
|
}
|
|
} elseif ( '' !== $min_op ) {
|
|
if ( version_compare( $version, $min_ver, $min_op ) ) {
|
|
$vulnerability[] = array(
|
|
'name' => $name,
|
|
'versions' => wp_kses( wpvulnerability_pretty_operator( $min_op ) . $min_ver, 'strip' ),
|
|
'version' => wp_kses( $min_ver, 'strip' ),
|
|
'unfixed' => $unfixed,
|
|
'source' => $source,
|
|
'impact' => $impact,
|
|
'uuid' => $uuid,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $vulnerability;
|
|
}
|
|
|
|
/**
|
|
* Get the current security mode configuration.
|
|
*
|
|
* Returns the security mode that controls shell_exec usage.
|
|
* Valid modes: 'standard', 'strict', 'disabled'.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @return string Current security mode.
|
|
*/
|
|
function wpvulnerability_get_security_mode() {
|
|
if ( defined( 'WPVULNERABILITY_SECURITY_MODE' ) ) {
|
|
$mode = strtolower( trim( (string) WPVULNERABILITY_SECURITY_MODE ) );
|
|
$valid_modes = array( 'standard', 'strict', 'disabled' );
|
|
if ( in_array( $mode, $valid_modes, true ) ) {
|
|
return $mode;
|
|
}
|
|
}
|
|
|
|
$settings = is_multisite() ? get_site_option( 'wpvulnerability-config', array() ) : get_option( 'wpvulnerability-config', array() );
|
|
if ( ! is_array( $settings ) ) {
|
|
$settings = array();
|
|
}
|
|
if ( isset( $settings['security_mode'] ) ) {
|
|
$security_mode_raw = $settings['security_mode'];
|
|
$mode = strtolower( trim( is_scalar( $security_mode_raw ) ? (string) $security_mode_raw : '' ) );
|
|
$valid_modes = array( 'standard', 'strict', 'disabled' );
|
|
if ( in_array( $mode, $valid_modes, true ) ) {
|
|
return $mode;
|
|
}
|
|
}
|
|
|
|
return 'standard';
|
|
}
|
|
|
|
/**
|
|
* Validate a shell command before execution.
|
|
*
|
|
* Ensures commands match allowed patterns and don't contain dangerous characters.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @param string $command Command to validate.
|
|
*
|
|
* @return bool True if command is safe, false otherwise.
|
|
*/
|
|
function wpvulnerability_validate_shell_command( $command ) {
|
|
$command = trim( (string) $command );
|
|
|
|
if ( '' === $command ) {
|
|
return false;
|
|
}
|
|
|
|
// Whitelist of allowed command bases.
|
|
$allowed_commands = array(
|
|
'convert',
|
|
'magick',
|
|
'identify',
|
|
'redis-server',
|
|
'memcached',
|
|
'sqlite3',
|
|
'which',
|
|
'apache2',
|
|
'httpd',
|
|
'nginx',
|
|
'angie',
|
|
'caddy',
|
|
'php',
|
|
'curl',
|
|
);
|
|
|
|
// Extract the base command (first word).
|
|
$parts = explode( ' ', $command );
|
|
$base_command = $parts[0];
|
|
|
|
// Check if the base command is in the allowlist (exact match, not substring).
|
|
$command_allowed = in_array( strtolower( $base_command ), $allowed_commands, true );
|
|
|
|
if ( ! $command_allowed ) {
|
|
wpvulnerability_maybe_log(
|
|
'Shell command validation failed: command not in whitelist',
|
|
array(
|
|
'command' => $command,
|
|
'base' => $base_command,
|
|
)
|
|
);
|
|
return false;
|
|
}
|
|
|
|
// Dangerous patterns that should never appear in commands.
|
|
$dangerous_patterns = array(
|
|
';',
|
|
'|',
|
|
'&',
|
|
'`',
|
|
'$(',
|
|
'<',
|
|
'>',
|
|
"\n",
|
|
"\r",
|
|
"\0",
|
|
);
|
|
|
|
foreach ( $dangerous_patterns as $pattern ) {
|
|
if ( false !== strpos( $command, $pattern ) ) {
|
|
wpvulnerability_maybe_log(
|
|
'Shell command validation failed: dangerous pattern detected',
|
|
array(
|
|
'command' => $command,
|
|
'pattern' => $pattern,
|
|
)
|
|
);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Register the custom post type used to store shell_exec logs.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @return void
|
|
*/
|
|
function wpvulnerability_register_shell_log_post_type() {
|
|
register_post_type(
|
|
'wpv_shell_log',
|
|
array(
|
|
'labels' => array(
|
|
'name' => __( 'WPVulnerability Shell Logs', 'wpvulnerability' ),
|
|
'singular_name' => __( 'WPVulnerability Shell Log', 'wpvulnerability' ),
|
|
),
|
|
'public' => false,
|
|
'exclude_from_search' => true,
|
|
'publicly_queryable' => false,
|
|
'show_ui' => false,
|
|
'show_in_menu' => false,
|
|
'supports' => array( 'title', 'editor' ),
|
|
)
|
|
);
|
|
}
|
|
add_action( 'init', 'wpvulnerability_register_shell_log_post_type' );
|
|
|
|
/**
|
|
* Log a shell_exec execution attempt.
|
|
*
|
|
* Stores information about shell command execution for security auditing.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @param string $component Component making the request.
|
|
* @param string $command Command that was executed or attempted.
|
|
* @param string|null $output Command output (null if not executed).
|
|
* @param bool $success Whether execution was successful.
|
|
* @param string $reason Reason for success/failure.
|
|
*
|
|
* @return void
|
|
*/
|
|
function wpvulnerability_log_shell_exec( $component, $command, $output, $success, $reason ) {
|
|
if ( 0 >= wpvulnerability_log_retention_days() ) {
|
|
return;
|
|
}
|
|
|
|
$log_data = array(
|
|
'component' => sanitize_text_field( $component ),
|
|
'command' => sanitize_text_field( $command ),
|
|
'output' => $output ? substr( sanitize_textarea_field( $output ), 0, 1000 ) : null,
|
|
'success' => (bool) $success,
|
|
'reason' => sanitize_text_field( $reason ),
|
|
'timestamp' => current_time( 'mysql' ),
|
|
'user' => is_user_logged_in() ? wp_get_current_user()->user_login : 'system',
|
|
'ip' => ( isset( $_SERVER['REMOTE_ADDR'] ) && is_string( $_SERVER['REMOTE_ADDR'] ) ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'unknown', // phpcs:ignore
|
|
);
|
|
|
|
$log_id = wp_insert_post(
|
|
array(
|
|
'post_type' => 'wpv_shell_log',
|
|
'post_status' => 'publish',
|
|
'post_title' => sprintf( '[%s] %s', $component, $reason ),
|
|
'post_content' => wp_slash( (string) wp_json_encode( $log_data ) ),
|
|
'post_author' => 0,
|
|
),
|
|
true
|
|
);
|
|
|
|
if ( is_wp_error( $log_id ) ) {
|
|
wpvulnerability_maybe_log(
|
|
'Failed to create shell exec log entry',
|
|
array(
|
|
'error' => $log_id->get_error_message(),
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieve shell execution logs for display in admin.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @param int $per_page Optional. Number of logs to return. Default 50.
|
|
* @param int $paged Optional. Page number to retrieve. Default 1.
|
|
*
|
|
* @return WP_Post[] Array of log posts.
|
|
*/
|
|
function wpvulnerability_get_shell_exec_logs( $per_page = 50, $paged = 1 ) {
|
|
$per_page = max( 1, (int) $per_page );
|
|
$paged = max( 1, (int) $paged );
|
|
$offset = ( $paged - 1 ) * $per_page;
|
|
|
|
return get_posts(
|
|
array(
|
|
'post_type' => 'wpv_shell_log',
|
|
'post_status' => 'publish',
|
|
'posts_per_page' => $per_page,
|
|
'orderby' => 'date',
|
|
'order' => 'DESC',
|
|
'offset' => $offset,
|
|
'no_found_rows' => true,
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Count the total amount of stored shell exec log entries.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @return int Number of log posts.
|
|
*/
|
|
function wpvulnerability_count_shell_exec_logs() {
|
|
$counts = wp_count_posts( 'wpv_shell_log' );
|
|
|
|
if ( ! isset( $counts->publish ) ) {
|
|
return 0;
|
|
}
|
|
|
|
return (int) $counts->publish;
|
|
}
|
|
|
|
/**
|
|
* Delete shell exec logs that fall outside of the configured retention window.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @return void
|
|
*/
|
|
function wpvulnerability_delete_expired_shell_logs() {
|
|
$retention = wpvulnerability_log_retention_days();
|
|
if ( $retention <= 0 ) {
|
|
return;
|
|
}
|
|
|
|
$threshold_ts = strtotime( '-' . $retention . ' days', time() );
|
|
$threshold = gmdate( 'Y-m-d H:i:s', false !== $threshold_ts ? $threshold_ts : time() );
|
|
|
|
do {
|
|
$logs = get_posts(
|
|
array(
|
|
'post_type' => 'wpv_shell_log',
|
|
'post_status' => 'publish',
|
|
'fields' => 'ids',
|
|
'posts_per_page' => 100,
|
|
'orderby' => 'date',
|
|
'order' => 'ASC',
|
|
'no_found_rows' => true,
|
|
'cache_results' => false,
|
|
'update_post_term_cache' => false,
|
|
'update_post_meta_cache' => false,
|
|
'date_query' => array(
|
|
array(
|
|
'before' => $threshold,
|
|
'inclusive' => false,
|
|
),
|
|
),
|
|
)
|
|
);
|
|
|
|
if ( empty( $logs ) ) {
|
|
break;
|
|
}
|
|
|
|
foreach ( $logs as $log_id ) {
|
|
wp_delete_post( $log_id, true );
|
|
}
|
|
$logs_count = count( $logs );
|
|
} while ( $logs_count >= 100 );
|
|
}
|
|
|
|
add_action( 'wpvulnerability_cleanup_logs', 'wpvulnerability_delete_expired_shell_logs' );
|
|
|
|
/**
|
|
* Safe wrapper for shell_exec with validation and logging.
|
|
*
|
|
* This function wraps shell_exec calls with security checks and audit logging.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @param string $component Component requesting shell execution.
|
|
* @param string $command Command to execute.
|
|
*
|
|
* @return string|null Command output on success, null on failure.
|
|
*/
|
|
function wpvulnerability_safe_shell_exec( $component, $command ) {
|
|
// Check if shell_exec is allowed for this component.
|
|
if ( ! wpvulnerability_can_shell_exec( $component ) ) {
|
|
wpvulnerability_log_shell_exec( $component, $command, null, false, 'disabled' );
|
|
return null;
|
|
}
|
|
|
|
// Validate command structure.
|
|
if ( ! wpvulnerability_validate_shell_command( $command ) ) {
|
|
wpvulnerability_log_shell_exec( $component, $command, null, false, 'validation_failed' );
|
|
return null;
|
|
}
|
|
|
|
// Execute command.
|
|
$output_raw = @shell_exec( escapeshellcmd( $command ) . ' 2>&1' ); // phpcs:ignore
|
|
$output = ( false === $output_raw ) ? null : $output_raw;
|
|
|
|
// Log execution.
|
|
wpvulnerability_log_shell_exec(
|
|
$component,
|
|
$command,
|
|
$output,
|
|
! empty( $output ),
|
|
'executed'
|
|
);
|
|
|
|
return $output;
|
|
}
|