| 1 |
<?php |
| 2 |
/** |
| 3 |
* Core functions |
| 4 |
* |
| 5 |
* @package WPVulnerability |
| 6 |
* |
| 7 |
* @version 2.0.0 |
| 8 |
*/ |
| 9 |
|
| 10 |
defined( 'ABSPATH' ) || die( 'No script kiddies please!' ); |
| 11 |
|
| 12 |
/** |
| 13 |
* Adds a vulnerability notice under vulnerable core. |
| 14 |
* |
| 15 |
* @since 2.0.0 |
| 16 |
* |
| 17 |
* @return void |
| 18 |
*/ |
| 19 |
function wpvulnerability_core_info_after() { |
| 20 |
|
| 21 |
// Retrieve the vulnerabilities for core from the options table and decode the JSON. |
| 22 |
if ( is_multisite() ) { |
| 23 |
$raw_core = get_site_option( 'wpvulnerability-core' ); |
| 24 |
$core_vulnerabilities = json_decode( is_string( $raw_core ) ? $raw_core : '', true ); |
| 25 |
} else { |
| 26 |
$raw_core = get_option( 'wpvulnerability-core' ); |
| 27 |
$core_vulnerabilities = json_decode( is_string( $raw_core ) ? $raw_core : '', true ); |
| 28 |
} |
| 29 |
|
| 30 |
// Generate the vulnerability notice message. |
| 31 |
$message = sprintf( |
| 32 |
/* translators: 1: core version */ |
| 33 |
__( 'WordPress %1$s has a known vulnerability that may be affecting your installed version.', 'wpvulnerability' ), |
| 34 |
get_bloginfo( 'version' ) |
| 35 |
); |
| 36 |
|
| 37 |
$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>'; |
| 38 |
$information .= '<table class="widefat wpvulnerability">'; |
| 39 |
|
| 40 |
// Loop through all vulnerabilities for the current version. |
| 41 |
$core_vuln_array = is_array( $core_vulnerabilities ) ? $core_vulnerabilities : array(); |
| 42 |
foreach ( $core_vuln_array as $vulnerability ) { |
| 43 |
if ( ! is_array( $vulnerability ) ) { |
| 44 |
continue; |
| 45 |
} |
| 46 |
$vuln_impact_raw = $vulnerability['impact'] ?? null; |
| 47 |
$vuln_impact = is_array( $vuln_impact_raw ) ? $vuln_impact_raw : array(); |
| 48 |
$vuln_cvss_raw = $vuln_impact['cvss'] ?? null; |
| 49 |
$vuln_cvss = is_array( $vuln_cvss_raw ) ? $vuln_cvss_raw : array(); |
| 50 |
$vuln_cvss2_raw = $vuln_impact['cvss2'] ?? null; |
| 51 |
$vuln_cvss2 = is_array( $vuln_cvss2_raw ) ? $vuln_cvss2_raw : array(); |
| 52 |
$vuln_cvss3_raw = $vuln_impact['cvss3'] ?? null; |
| 53 |
$vuln_cvss3 = is_array( $vuln_cvss3_raw ) ? $vuln_cvss3_raw : array(); |
| 54 |
$vuln_cvss4_raw = $vuln_impact['cvss4'] ?? null; |
| 55 |
$vuln_cvss4 = is_array( $vuln_cvss4_raw ) ? $vuln_cvss4_raw : array(); |
| 56 |
$vuln_ssvc_raw = $vuln_impact['ssvc'] ?? null; |
| 57 |
$vuln_ssvc = is_array( $vuln_ssvc_raw ) ? $vuln_ssvc_raw : array(); |
| 58 |
$vuln_cwe_raw = $vuln_impact['cwe'] ?? null; |
| 59 |
$vuln_cwe = is_array( $vuln_cwe_raw ) ? $vuln_cwe_raw : array(); |
| 60 |
$vuln_src_raw = $vulnerability['source'] ?? null; |
| 61 |
$vuln_sources = is_array( $vuln_src_raw ) ? $vuln_src_raw : array(); |
| 62 |
|
| 63 |
$kev = ( isset( $vuln_ssvc['kev'] ) && true === $vuln_ssvc['kev'] ); |
| 64 |
$exploitation = isset( $vuln_ssvc['exploitation'] ) && is_string( $vuln_ssvc['exploitation'] ) ? $vuln_ssvc['exploitation'] : ''; |
| 65 |
$automatable = isset( $vuln_ssvc['automatable'] ) && is_string( $vuln_ssvc['automatable'] ) ? $vuln_ssvc['automatable'] : ''; |
| 66 |
$kev_date_raw = $vuln_ssvc['kev_date'] ?? null; |
| 67 |
$kev_date = is_string( $kev_date_raw ) && '' !== $kev_date_raw ? $kev_date_raw : null; |
| 68 |
$epss_raw = $vuln_impact['epss'] ?? null; |
| 69 |
$epss = is_numeric( $epss_raw ) ? (float) $epss_raw : null; |
| 70 |
$description = wpvulnerability_get_source_description( $vuln_sources ); |
| 71 |
|
| 72 |
$what = array(); |
| 73 |
foreach ( $vuln_cwe as $vulnerability_cwe ) { |
| 74 |
if ( ! is_array( $vulnerability_cwe ) ) { |
| 75 |
continue; |
| 76 |
} |
| 77 |
$cwe_name = is_scalar( $vulnerability_cwe['name'] ?? '' ) ? (string) ( $vulnerability_cwe['name'] ?? '' ) : ''; |
| 78 |
$cwe_desc = is_scalar( $vulnerability_cwe['description'] ?? '' ) ? (string) ( $vulnerability_cwe['description'] ?? '' ) : ''; |
| 79 |
$what[] = '<div><b>' . wp_kses( $cwe_name, 'strip' ) . '</b></div><div><i>' . esc_html( $cwe_desc ) . '</i></div>'; |
| 80 |
} |
| 81 |
|
| 82 |
$source_pills = wpvulnerability_render_source_pills( $vuln_sources ); |
| 83 |
|
| 84 |
// Best available CVSS score and severity: cvss4 > cvss3 > cvss2 > legacy cvss. |
| 85 |
$score = null; |
| 86 |
$sev_raw = null; |
| 87 |
foreach ( array( $vuln_cvss4, $vuln_cvss3, $vuln_cvss2, $vuln_cvss ) as $cvss_c ) { |
| 88 |
if ( empty( $cvss_c ) ) { |
| 89 |
continue; |
| 90 |
} |
| 91 |
$s_raw = $cvss_c['score'] ?? null; |
| 92 |
$v_raw = $cvss_c['severity'] ?? null; |
| 93 |
$s = is_numeric( $s_raw ) ? number_format( (float) $s_raw, 1, '.', '' ) : null; |
| 94 |
$v = is_string( $v_raw ) && '' !== $v_raw ? $v_raw : null; |
| 95 |
if ( null !== $s || null !== $v ) { |
| 96 |
$score = $s; |
| 97 |
$sev_raw = $v; |
| 98 |
break; |
| 99 |
} |
| 100 |
} |
| 101 |
|
| 102 |
$vuln_name = is_scalar( $vulnerability['name'] ?? '' ) ? (string) ( $vulnerability['name'] ?? '' ) : ''; |
| 103 |
$score_badge = wpvulnerability_render_score_badge( $score, $sev_raw, $epss ); |
| 104 |
$show_active = $kev || 'active' === $exploitation; |
| 105 |
$show_poc = 'poc' === $exploitation; |
| 106 |
$show_auto = 'yes' === $automatable; |
| 107 |
|
| 108 |
$information .= '<tr>'; |
| 109 |
$information .= '<td style="max-width: 256px; min-width: 96px; vertical-align: top; padding-top: 6px;">WordPress <b>' . wp_kses( $vuln_name, 'strip' ) . '</b></td>'; |
| 110 |
$information .= '<td>'; |
| 111 |
|
| 112 |
if ( $show_active || $show_poc || $show_auto || '' !== $score_badge ) { |
| 113 |
$information .= '<div style="display:flex; align-items:center; gap:6px; flex-wrap:wrap; margin-bottom:5px;">'; |
| 114 |
if ( $show_active ) { |
| 115 |
$information .= '<span class="wpvuln-kev-label">⚠ ' . esc_html__( 'Actively exploited', 'wpvulnerability' ); |
| 116 |
if ( $kev && null !== $kev_date ) { |
| 117 |
$information .= ' · ' . esc_html( $kev_date ); |
| 118 |
} |
| 119 |
$information .= '</span>'; |
| 120 |
} |
| 121 |
if ( $show_poc ) { |
| 122 |
$information .= '<span class="wpvuln-poc-label">⚡ ' . esc_html__( 'Public exploit', 'wpvulnerability' ) . '</span>'; |
| 123 |
} |
| 124 |
if ( $show_auto ) { |
| 125 |
$information .= '<span class="wpvuln-auto-label">⚙ ' . esc_html__( 'Automatable', 'wpvulnerability' ) . '</span>'; |
| 126 |
} |
| 127 |
if ( '' !== $score_badge ) { |
| 128 |
$information .= $score_badge; |
| 129 |
} |
| 130 |
$information .= '</div>'; |
| 131 |
} |
| 132 |
if ( null !== $description ) { |
| 133 |
$information .= '<div style="padding-bottom: 5px;">' . esc_html( $description ) . '</div>'; |
| 134 |
} |
| 135 |
if ( count( $what ) ) { |
| 136 |
$information .= '<div style="padding-bottom: 5px;">' . implode( '', $what ) . '</div>'; |
| 137 |
} |
| 138 |
if ( '' !== $source_pills ) { |
| 139 |
$information .= '<div class="wpvuln-refs-row"><span class="wpvuln-refs-label">' . esc_html__( 'References:', 'wpvulnerability' ) . '</span>'; |
| 140 |
$information .= $source_pills; |
| 141 |
$information .= '</div>'; |
| 142 |
} |
| 143 |
|
| 144 |
$information .= '</td>'; |
| 145 |
$information .= '</tr>'; |
| 146 |
} |
| 147 |
|
| 148 |
$information .= '</table>'; |
| 149 |
|
| 150 |
echo $information; // phpcs:ignore |
| 151 |
} |
| 152 |
|
| 153 |
/** |
| 154 |
* Retrieves vulnerabilities for a given WordPress core version and updates its data. |
| 155 |
* |
| 156 |
* @since 2.0.0 |
| 157 |
* |
| 158 |
* @return array<int, array<string, mixed>>|false The updated core data array or false if no vulnerabilities are found. |
| 159 |
*/ |
| 160 |
function wpvulnerability_get_fresh_core_vulnerabilities() { |
| 161 |
|
| 162 |
// Get the core version and sanitize it. |
| 163 |
$version = wpvulnerability_sanitize_version( get_bloginfo( 'version' ) ); |
| 164 |
|
| 165 |
// Retrieve vulnerabilities for the core version. |
| 166 |
$response = wpvulnerability_get_core( $version, 0 ); |
| 167 |
|
| 168 |
$core_data = array(); |
| 169 |
|
| 170 |
// If no vulnerabilities are found, return false. |
| 171 |
if ( empty( $response ) ) { |
| 172 |
return false; |
| 173 |
} |
| 174 |
|
| 175 |
// If vulnerabilities are found, update the core data. |
| 176 |
foreach ( $response as $v ) { |
| 177 |
if ( isset( $v['name'], $v['source'], $v['impact'] ) ) { // Ensure expected keys exist. |
| 178 |
$core_data[] = array( |
| 179 |
'name' => wp_kses( is_scalar( $v['name'] ) ? (string) $v['name'] : '', 'strip' ), |
| 180 |
'source' => $v['source'], |
| 181 |
'impact' => $v['impact'], |
| 182 |
'uuid' => is_scalar( $v['uuid'] ?? '' ) ? (string) ( $v['uuid'] ?? '' ) : '', |
| 183 |
); |
| 184 |
} |
| 185 |
} |
| 186 |
|
| 187 |
return ! empty( $core_data ) ? $core_data : false; // Return false if core_data is empty. |
| 188 |
} |
| 189 |
|
| 190 |
/** |
| 191 |
* Get Vulnerabilities |
| 192 |
* |
| 193 |
* Retrieves and caches the vulnerabilities for the installed WordPress core version. |
| 194 |
* |
| 195 |
* @since 2.0.0 |
| 196 |
* |
| 197 |
* @return string JSON-encoded array of core data with vulnerabilities and vulnerable status. |
| 198 |
*/ |
| 199 |
function wpvulnerability_core_get_installed() { |
| 200 |
|
| 201 |
$wpvulnerability_core_vulnerable = 0; |
| 202 |
$current_version = wpvulnerability_sanitize_version( get_bloginfo( 'version' ) ); |
| 203 |
|
| 204 |
// Get fresh core vulnerabilities. |
| 205 |
$core = wpvulnerability_get_fresh_core_vulnerabilities(); |
| 206 |
|
| 207 |
// Check if vulnerabilities were found and count them. |
| 208 |
if ( is_array( $core ) && count( $core ) > 0 ) { |
| 209 |
$wpvulnerability_core_vulnerable = count( $core ); |
| 210 |
} |
| 211 |
|
| 212 |
// Cache the vulnerability data and the timestamp for cache expiration. |
| 213 |
if ( is_multisite() ) { |
| 214 |
update_site_option( 'wpvulnerability-core', wp_json_encode( $core ) ); |
| 215 |
update_site_option( 'wpvulnerability-core-vulnerable', wp_json_encode( number_format( $wpvulnerability_core_vulnerable, 0, '.', '' ) ) ); |
| 216 |
update_site_option( 'wpvulnerability-core-cache', wp_json_encode( number_format( time() + ( 3600 * wpvulnerability_cache_hours() ), 0, '.', '' ) ) ); |
| 217 |
update_site_option( 'wpvulnerability-core-version', wp_json_encode( $current_version ) ); |
| 218 |
} else { |
| 219 |
update_option( 'wpvulnerability-core', wp_json_encode( $core ), false ); |
| 220 |
update_option( 'wpvulnerability-core-vulnerable', wp_json_encode( number_format( $wpvulnerability_core_vulnerable, 0, '.', '' ) ), false ); |
| 221 |
update_option( 'wpvulnerability-core-cache', wp_json_encode( number_format( time() + ( 3600 * wpvulnerability_cache_hours() ), 0, '.', '' ) ), false ); |
| 222 |
update_option( 'wpvulnerability-core-version', wp_json_encode( $current_version ), false ); |
| 223 |
} |
| 224 |
|
| 225 |
// Return the JSON-encoded array of core vulnerabilities. |
| 226 |
$encoded = wp_json_encode( $core ); |
| 227 |
return false !== $encoded ? $encoded : ''; |
| 228 |
} |
| 229 |
|
| 230 |
/** |
| 231 |
* Get cached core vulnerabilities without refreshing external data. |
| 232 |
* |
| 233 |
* @since 2.0.0 |
| 234 |
* @since 4.1.2 Refreshes when the stored WordPress core version differs from the running version. |
| 235 |
* |
| 236 |
* @return array<mixed> Array of core with their vulnerabilities. |
| 237 |
*/ |
| 238 |
function wpvulnerability_core_get_vulnerabilities() { |
| 239 |
|
| 240 |
if ( is_multisite() ) { |
| 241 |
$raw = get_site_option( 'wpvulnerability-core' ); |
| 242 |
$core_data = json_decode( is_string( $raw ) ? $raw : '', true ); |
| 243 |
} else { |
| 244 |
$raw = get_option( 'wpvulnerability-core' ); |
| 245 |
$core_data = json_decode( is_string( $raw ) ? $raw : '', true ); |
| 246 |
} |
| 247 |
|
| 248 |
return is_array( $core_data ) ? $core_data : array(); |
| 249 |
} |
| 250 |
|
| 251 |
/** |
| 252 |
* Update the core cache and remove any old cache data. |
| 253 |
* |
| 254 |
* @since 2.0.0 |
| 255 |
* |
| 256 |
* @return void |
| 257 |
*/ |
| 258 |
function wpvulnerability_core_get_vulnerabilities_clean() { |
| 259 |
wpvulnerability_clear_cache( 'core' ); |
| 260 |
wpvulnerability_core_get_installed(); |
| 261 |
} |
| 262 |
|
| 263 |
/** |
| 264 |
* Adds vulnerability information after the core version and notices on the update-core.php page. |
| 265 |
* |
| 266 |
* @since 2.0.0 |
| 267 |
* |
| 268 |
* @return void |
| 269 |
*/ |
| 270 |
function wpvulnerability_core_page() { |
| 271 |
|
| 272 |
// Check if the current page is the update-core.php page. |
| 273 |
global $pagenow; |
| 274 |
if ( wpvulnerability_analyze_filter( 'core' ) && 'update-core.php' === $pagenow && wpvulnerability_capabilities() ) { |
| 275 |
|
| 276 |
// Get the vulnerabilities for the core. |
| 277 |
$core = wpvulnerability_core_get_vulnerabilities(); |
| 278 |
|
| 279 |
// If there are vulnerabilities, add an action to display them after the core auto updates settings. |
| 280 |
if ( ! empty( $core ) ) { |
| 281 |
add_action( 'after_core_auto_updates_settings', 'wpvulnerability_core_info_after' ); |
| 282 |
} |
| 283 |
} |
| 284 |
} |
| 285 |
// Add notices for vulnerable core on the core page. |
| 286 |
add_action( 'admin_head', 'wpvulnerability_core_page' ); |
| 287 |
|