286 lines
11 KiB
PHP
286 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* Core functions
|
|
*
|
|
* @package WPVulnerability
|
|
*
|
|
* @version 2.0.0
|
|
*/
|
|
|
|
defined( 'ABSPATH' ) || die( 'No script kiddies please!' );
|
|
|
|
/**
|
|
* Adds a vulnerability notice under vulnerable core.
|
|
*
|
|
* @since 2.0.0
|
|
*
|
|
* @return void
|
|
*/
|
|
function wpvulnerability_core_info_after() {
|
|
|
|
// Retrieve the vulnerabilities for core from the options table and decode the JSON.
|
|
if ( is_multisite() ) {
|
|
$raw_core = get_site_option( 'wpvulnerability-core' );
|
|
$core_vulnerabilities = json_decode( is_string( $raw_core ) ? $raw_core : '', true );
|
|
} else {
|
|
$raw_core = get_option( 'wpvulnerability-core' );
|
|
$core_vulnerabilities = json_decode( is_string( $raw_core ) ? $raw_core : '', true );
|
|
}
|
|
|
|
// Generate the vulnerability notice message.
|
|
$message = sprintf(
|
|
/* translators: 1: core version */
|
|
__( 'WordPress %1$s has a known vulnerability that may be affecting your installed version.', 'wpvulnerability' ),
|
|
get_bloginfo( 'version' )
|
|
);
|
|
|
|
$information = '<p class="text-red"><img src="' . esc_url( WPVULNERABILITY_PLUGIN_URL ) . 'assets/icon.svg" style="height: 16px; vertical-align: text-top; width: 16px;" alt="" title="WPVulnerability"> <strong>' . esc_html( $message ) . '</strong></p>';
|
|
$information .= '<table class="widefat wpvulnerability">';
|
|
|
|
// Loop through all vulnerabilities for the current version.
|
|
$core_vuln_array = is_array( $core_vulnerabilities ) ? $core_vulnerabilities : array();
|
|
foreach ( $core_vuln_array as $vulnerability ) {
|
|
if ( ! is_array( $vulnerability ) ) {
|
|
continue;
|
|
}
|
|
$vuln_impact_raw = $vulnerability['impact'] ?? null;
|
|
$vuln_impact = is_array( $vuln_impact_raw ) ? $vuln_impact_raw : array();
|
|
$vuln_cvss_raw = $vuln_impact['cvss'] ?? null;
|
|
$vuln_cvss = is_array( $vuln_cvss_raw ) ? $vuln_cvss_raw : array();
|
|
$vuln_cvss2_raw = $vuln_impact['cvss2'] ?? null;
|
|
$vuln_cvss2 = is_array( $vuln_cvss2_raw ) ? $vuln_cvss2_raw : array();
|
|
$vuln_cvss3_raw = $vuln_impact['cvss3'] ?? null;
|
|
$vuln_cvss3 = is_array( $vuln_cvss3_raw ) ? $vuln_cvss3_raw : array();
|
|
$vuln_cvss4_raw = $vuln_impact['cvss4'] ?? null;
|
|
$vuln_cvss4 = is_array( $vuln_cvss4_raw ) ? $vuln_cvss4_raw : array();
|
|
$vuln_ssvc_raw = $vuln_impact['ssvc'] ?? null;
|
|
$vuln_ssvc = is_array( $vuln_ssvc_raw ) ? $vuln_ssvc_raw : array();
|
|
$vuln_cwe_raw = $vuln_impact['cwe'] ?? null;
|
|
$vuln_cwe = is_array( $vuln_cwe_raw ) ? $vuln_cwe_raw : array();
|
|
$vuln_src_raw = $vulnerability['source'] ?? null;
|
|
$vuln_sources = is_array( $vuln_src_raw ) ? $vuln_src_raw : array();
|
|
|
|
$kev = ( isset( $vuln_ssvc['kev'] ) && true === $vuln_ssvc['kev'] );
|
|
$exploitation = isset( $vuln_ssvc['exploitation'] ) && is_string( $vuln_ssvc['exploitation'] ) ? $vuln_ssvc['exploitation'] : '';
|
|
$automatable = isset( $vuln_ssvc['automatable'] ) && is_string( $vuln_ssvc['automatable'] ) ? $vuln_ssvc['automatable'] : '';
|
|
$kev_date_raw = $vuln_ssvc['kev_date'] ?? null;
|
|
$kev_date = is_string( $kev_date_raw ) && '' !== $kev_date_raw ? $kev_date_raw : null;
|
|
$epss_raw = $vuln_impact['epss'] ?? null;
|
|
$epss = is_numeric( $epss_raw ) ? (float) $epss_raw : null;
|
|
$description = wpvulnerability_get_source_description( $vuln_sources );
|
|
|
|
$what = array();
|
|
foreach ( $vuln_cwe as $vulnerability_cwe ) {
|
|
if ( ! is_array( $vulnerability_cwe ) ) {
|
|
continue;
|
|
}
|
|
$cwe_name = is_scalar( $vulnerability_cwe['name'] ?? '' ) ? (string) ( $vulnerability_cwe['name'] ?? '' ) : '';
|
|
$cwe_desc = is_scalar( $vulnerability_cwe['description'] ?? '' ) ? (string) ( $vulnerability_cwe['description'] ?? '' ) : '';
|
|
$what[] = '<div><b>' . wp_kses( $cwe_name, 'strip' ) . '</b></div><div><i>' . esc_html( $cwe_desc ) . '</i></div>';
|
|
}
|
|
|
|
$source_pills = wpvulnerability_render_source_pills( $vuln_sources );
|
|
|
|
// Best available CVSS score and severity: cvss4 > cvss3 > cvss2 > legacy cvss.
|
|
$score = null;
|
|
$sev_raw = null;
|
|
foreach ( array( $vuln_cvss4, $vuln_cvss3, $vuln_cvss2, $vuln_cvss ) as $cvss_c ) {
|
|
if ( empty( $cvss_c ) ) {
|
|
continue;
|
|
}
|
|
$s_raw = $cvss_c['score'] ?? null;
|
|
$v_raw = $cvss_c['severity'] ?? null;
|
|
$s = is_numeric( $s_raw ) ? number_format( (float) $s_raw, 1, '.', '' ) : null;
|
|
$v = is_string( $v_raw ) && '' !== $v_raw ? $v_raw : null;
|
|
if ( null !== $s || null !== $v ) {
|
|
$score = $s;
|
|
$sev_raw = $v;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$vuln_name = is_scalar( $vulnerability['name'] ?? '' ) ? (string) ( $vulnerability['name'] ?? '' ) : '';
|
|
$score_badge = wpvulnerability_render_score_badge( $score, $sev_raw, $epss );
|
|
$show_active = $kev || 'active' === $exploitation;
|
|
$show_poc = 'poc' === $exploitation;
|
|
$show_auto = 'yes' === $automatable;
|
|
|
|
$information .= '<tr>';
|
|
$information .= '<td style="max-width: 256px; min-width: 96px; vertical-align: top; padding-top: 6px;">WordPress <b>' . wp_kses( $vuln_name, 'strip' ) . '</b></td>';
|
|
$information .= '<td>';
|
|
|
|
if ( $show_active || $show_poc || $show_auto || '' !== $score_badge ) {
|
|
$information .= '<div style="display:flex; align-items:center; gap:6px; flex-wrap:wrap; margin-bottom:5px;">';
|
|
if ( $show_active ) {
|
|
$information .= '<span class="wpvuln-kev-label">⚠ ' . esc_html__( 'Actively exploited', 'wpvulnerability' );
|
|
if ( $kev && null !== $kev_date ) {
|
|
$information .= ' · ' . esc_html( $kev_date );
|
|
}
|
|
$information .= '</span>';
|
|
}
|
|
if ( $show_poc ) {
|
|
$information .= '<span class="wpvuln-poc-label">⚡ ' . esc_html__( 'Public exploit', 'wpvulnerability' ) . '</span>';
|
|
}
|
|
if ( $show_auto ) {
|
|
$information .= '<span class="wpvuln-auto-label">⚙ ' . esc_html__( 'Automatable', 'wpvulnerability' ) . '</span>';
|
|
}
|
|
if ( '' !== $score_badge ) {
|
|
$information .= $score_badge;
|
|
}
|
|
$information .= '</div>';
|
|
}
|
|
if ( null !== $description ) {
|
|
$information .= '<div style="padding-bottom: 5px;">' . esc_html( $description ) . '</div>';
|
|
}
|
|
if ( count( $what ) ) {
|
|
$information .= '<div style="padding-bottom: 5px;">' . implode( '', $what ) . '</div>';
|
|
}
|
|
if ( '' !== $source_pills ) {
|
|
$information .= '<div class="wpvuln-refs-row"><span class="wpvuln-refs-label">' . esc_html__( 'References:', 'wpvulnerability' ) . '</span>';
|
|
$information .= $source_pills;
|
|
$information .= '</div>';
|
|
}
|
|
|
|
$information .= '</td>';
|
|
$information .= '</tr>';
|
|
}
|
|
|
|
$information .= '</table>';
|
|
|
|
echo $information; // phpcs:ignore
|
|
}
|
|
|
|
/**
|
|
* Retrieves vulnerabilities for a given WordPress core version and updates its data.
|
|
*
|
|
* @since 2.0.0
|
|
*
|
|
* @return array<int, array<string, mixed>>|false The updated core data array or false if no vulnerabilities are found.
|
|
*/
|
|
function wpvulnerability_get_fresh_core_vulnerabilities() {
|
|
|
|
// Get the core version and sanitize it.
|
|
$version = wpvulnerability_sanitize_version( get_bloginfo( 'version' ) );
|
|
|
|
// Retrieve vulnerabilities for the core version.
|
|
$response = wpvulnerability_get_core( $version, 0 );
|
|
|
|
$core_data = array();
|
|
|
|
// If no vulnerabilities are found, return false.
|
|
if ( empty( $response ) ) {
|
|
return false;
|
|
}
|
|
|
|
// If vulnerabilities are found, update the core data.
|
|
foreach ( $response as $v ) {
|
|
if ( isset( $v['name'], $v['source'], $v['impact'] ) ) { // Ensure expected keys exist.
|
|
$core_data[] = array(
|
|
'name' => wp_kses( is_scalar( $v['name'] ) ? (string) $v['name'] : '', 'strip' ),
|
|
'source' => $v['source'],
|
|
'impact' => $v['impact'],
|
|
'uuid' => is_scalar( $v['uuid'] ?? '' ) ? (string) ( $v['uuid'] ?? '' ) : '',
|
|
);
|
|
}
|
|
}
|
|
|
|
return ! empty( $core_data ) ? $core_data : false; // Return false if core_data is empty.
|
|
}
|
|
|
|
/**
|
|
* Get Vulnerabilities
|
|
*
|
|
* Retrieves and caches the vulnerabilities for the installed WordPress core version.
|
|
*
|
|
* @since 2.0.0
|
|
*
|
|
* @return string JSON-encoded array of core data with vulnerabilities and vulnerable status.
|
|
*/
|
|
function wpvulnerability_core_get_installed() {
|
|
|
|
$wpvulnerability_core_vulnerable = 0;
|
|
$current_version = wpvulnerability_sanitize_version( get_bloginfo( 'version' ) );
|
|
|
|
// Get fresh core vulnerabilities.
|
|
$core = wpvulnerability_get_fresh_core_vulnerabilities();
|
|
|
|
// Check if vulnerabilities were found and count them.
|
|
if ( is_array( $core ) && count( $core ) > 0 ) {
|
|
$wpvulnerability_core_vulnerable = count( $core );
|
|
}
|
|
|
|
// Cache the vulnerability data and the timestamp for cache expiration.
|
|
if ( is_multisite() ) {
|
|
update_site_option( 'wpvulnerability-core', wp_json_encode( $core ) );
|
|
update_site_option( 'wpvulnerability-core-vulnerable', wp_json_encode( number_format( $wpvulnerability_core_vulnerable, 0, '.', '' ) ) );
|
|
update_site_option( 'wpvulnerability-core-cache', wp_json_encode( number_format( time() + ( 3600 * wpvulnerability_cache_hours() ), 0, '.', '' ) ) );
|
|
update_site_option( 'wpvulnerability-core-version', wp_json_encode( $current_version ) );
|
|
} else {
|
|
update_option( 'wpvulnerability-core', wp_json_encode( $core ), false );
|
|
update_option( 'wpvulnerability-core-vulnerable', wp_json_encode( number_format( $wpvulnerability_core_vulnerable, 0, '.', '' ) ), false );
|
|
update_option( 'wpvulnerability-core-cache', wp_json_encode( number_format( time() + ( 3600 * wpvulnerability_cache_hours() ), 0, '.', '' ) ), false );
|
|
update_option( 'wpvulnerability-core-version', wp_json_encode( $current_version ), false );
|
|
}
|
|
|
|
// Return the JSON-encoded array of core vulnerabilities.
|
|
$encoded = wp_json_encode( $core );
|
|
return false !== $encoded ? $encoded : '';
|
|
}
|
|
|
|
/**
|
|
* Get cached core vulnerabilities without refreshing external data.
|
|
*
|
|
* @since 2.0.0
|
|
* @since 4.1.2 Refreshes when the stored WordPress core version differs from the running version.
|
|
*
|
|
* @return array<mixed> Array of core with their vulnerabilities.
|
|
*/
|
|
function wpvulnerability_core_get_vulnerabilities() {
|
|
|
|
if ( is_multisite() ) {
|
|
$raw = get_site_option( 'wpvulnerability-core' );
|
|
$core_data = json_decode( is_string( $raw ) ? $raw : '', true );
|
|
} else {
|
|
$raw = get_option( 'wpvulnerability-core' );
|
|
$core_data = json_decode( is_string( $raw ) ? $raw : '', true );
|
|
}
|
|
|
|
return is_array( $core_data ) ? $core_data : array();
|
|
}
|
|
|
|
/**
|
|
* Update the core cache and remove any old cache data.
|
|
*
|
|
* @since 2.0.0
|
|
*
|
|
* @return void
|
|
*/
|
|
function wpvulnerability_core_get_vulnerabilities_clean() {
|
|
wpvulnerability_clear_cache( 'core' );
|
|
wpvulnerability_core_get_installed();
|
|
}
|
|
|
|
/**
|
|
* Adds vulnerability information after the core version and notices on the update-core.php page.
|
|
*
|
|
* @since 2.0.0
|
|
*
|
|
* @return void
|
|
*/
|
|
function wpvulnerability_core_page() {
|
|
|
|
// Check if the current page is the update-core.php page.
|
|
global $pagenow;
|
|
if ( wpvulnerability_analyze_filter( 'core' ) && 'update-core.php' === $pagenow && wpvulnerability_capabilities() ) {
|
|
|
|
// Get the vulnerabilities for the core.
|
|
$core = wpvulnerability_core_get_vulnerabilities();
|
|
|
|
// If there are vulnerabilities, add an action to display them after the core auto updates settings.
|
|
if ( ! empty( $core ) ) {
|
|
add_action( 'after_core_auto_updates_settings', 'wpvulnerability_core_info_after' );
|
|
}
|
|
}
|
|
}
|
|
// Add notices for vulnerable core on the core page.
|
|
add_action( 'admin_head', 'wpvulnerability_core_page' );
|