PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / behavior / scan-item / class-vuln-result.php

class-vuln-result.php in Defender Security – Malware Scanner, Login Security & Firewall 6.3.0, at src/behavior/scan-item/class-vuln-result.php

532 lines 15.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Represents a vulnerability result.
4 *
5 * @package WP_Defender\Behavior\Scan_Item
6 */
7
8 namespace WP_Defender\Behavior\Scan_Item;
9
10 use WP_Error;
11 use Theme_Upgrader;
12 use Plugin_Upgrader;
13 use WP_Defender\Traits\IO;
14 use WP_Defender\Model\Scan;
15 use WP_Defender\Traits\Theme;
16 use WP_Defender\Traits\Plugin;
17 use Calotes\Component\Behavior;
18 use WP_Defender\Traits\Formats;
19 use WP_Defender\Model\Scan_Item;
20 use WP_Defender\Component\Error_Code;
21 use WP_Defender\Component\Security_Tweaks\WP_Version;
22 use WP_Defender\Controller\Scan as Scan_Controller;
23
24 /**
25 * Represents a vulnerability result.
26 */
27 class Vuln_Result extends Behavior {
28
29 use Formats;
30 use IO;
31 use Plugin;
32 use Theme;
33
34 /**
35 * Checks if upgrade is possible based on the presence of fixed version in vulnerabilities.
36 *
37 * @param array $bugs The array of vulnerabilities to check.
38 *
39 * @return string The upgrade status ('disabled' or 'enabled').
40 */
41 protected function upgrade_possible( array $bugs ): string {
42 $upgrade = 'disabled';
43 foreach ( $bugs as $bug ) {
44 if ( isset( $bug['fixed_in'] ) && '' !== $bug['fixed_in'] ) {
45 $upgrade = 'enabled';
46 break;
47 }
48 }
49
50 return $upgrade;
51 }
52
53 /**
54 * Checks if a plugin upgrade is actually possible.
55 *
56 * First verifies that at least one vulnerability has a known fixed version,
57 * then confirms WordPress's update API has an update available for the plugin.
58 * This guards against showing an upgrade button when the plugin is already at
59 * the latest version on wp.org, or when the plugin has been removed/closed.
60 *
61 * @param array $data The raw scan item data for the plugin.
62 *
63 * @return string The upgrade status ('disabled' or 'enabled').
64 */
65 protected function plugin_upgrade_possible( array $data ): string {
66 // No known fixed version in vulnerability data — nothing to upgrade to.
67 if ( 'disabled' === $this->upgrade_possible( $data['bugs'] ) ) {
68 return 'disabled';
69 }
70
71 // Ensure the update transient is current. wp_update_plugins() detects newly
72 // installed/removed plugins and only fires an HTTP request to wp.org when
73 // the plugin list has changed or the transient has expired; otherwise it
74 // is effectively a no-op.
75 wp_update_plugins();
76 $updates = get_site_transient( 'update_plugins' );
77
78 // If the slug is absent from the response, the plugin is either already
79 // at the latest version on wp.org or is not available there (removed/closed).
80 if ( ! isset( $updates->response[ $data['slug'] ] ) ) {
81 return 'disabled';
82 }
83
84 return 'enabled';
85 }
86
87 /**
88 * Converts the scan item information into an array for output.
89 *
90 * @return array
91 */
92 public function to_array(): array {
93 $data = $this->owner->raw_data;
94 if ( isset( $data['name'], $data['version'], $data['bugs'] ) ) {
95 if ( 'wp_core' === $data['type'] ) {
96 // Check if the current WP version is the latest.
97 $upgrade = ( new WP_Version() )->check() ? 'disabled' : 'enabled';
98 } elseif ( 'plugin' === $data['type'] ) {
99 $upgrade = $this->plugin_upgrade_possible( $data );
100 } elseif ( 'theme' === $data['type'] ) {
101 $upgrade = $this->upgrade_possible( $data['bugs'] );
102 } else {
103 $upgrade = 'disabled';
104 }
105
106 return array(
107 'id' => $this->owner->id,
108 'type' => Scan_Item::TYPE_VULNERABILITY,
109 'file_name' => $data['name'],
110 'short_desc' => sprintf(
111 /* translators: %s: Version of WP core, plugin or theme. */
112 esc_html__( 'Vulnerability found in %s.', 'defender-security' ),
113 $data['version']
114 ),
115 'details' => isset( $data['new_structure'] )
116 ? $this->get_details_as_array( $data )
117 : $this->get_detail_as_string( $data ),
118 // Need for all scan items for WP-CLI command. Full path = base slug for this item.
119 'full_path' => $data['slug'],
120 'new_structure' => isset( $data['new_structure'] ) ? 'yes' : 'no',
121 'upgrade' => $upgrade,
122 );
123 }
124
125 return array();
126 }
127
128 /**
129 * Ignore the suspicious file for the current scan.
130 *
131 * @return array An array with a message indicating successful ignore.
132 */
133 public function ignore(): array {
134 $scan = Scan::get_last();
135 $issue_name = '<b>' . $this->get_issue_name( $this->owner->raw_data ) . '</b>';
136 $res = $scan->ignore_issue( $this->owner->id );
137 if ( ! $res ) {
138 return array(
139 'type_notice' => 'error',
140 'message' => $this->get_failed_ignore_result( $issue_name ),
141 );
142 }
143
144 return array(
145 'message' => sprintf(
146 /* translators: %s: Scan issue name. */
147 esc_html__( 'You’ve successfully ignored the security issue related to %s.', 'defender-security' ),
148 $issue_name
149 ),
150 );
151 }
152
153 /**
154 * Allow the suspicious file for the current scan.
155 *
156 * @return array An array with a message indicating successful restoration.
157 */
158 public function unignore(): array {
159 $scan = Scan::get_last();
160 $issue_name = '<b>' . $this->get_issue_name( $this->owner->raw_data ) . '</b>';
161 $res = $scan->unignore_issue( $this->owner->id );
162 if ( ! $res ) {
163 return array(
164 'type_notice' => 'error',
165 'message' => $this->get_failed_restore_result( $issue_name ),
166 );
167 }
168
169 return array(
170 'message' => sprintf(
171 /* translators: %s: Scan issue name. */
172 esc_html__( 'You’ve successfully restored the security issue related to %s.', 'defender-security' ),
173 $issue_name
174 ),
175 );
176 }
177
178 /**
179 * Resolves the vulnerability issue based on the type of vulnerability.
180 *
181 * @return array|bool|WP_Error The resolved issue or an error if the issue type is not found.
182 */
183 public function resolve() {
184 $data = $this->owner->raw_data;
185 // Redirect for WordPress-type.
186 if ( 'wp_core' === $data['type'] ) {
187 return array( 'url' => network_admin_url( 'update-core.php' ) );
188 } elseif ( 'plugin' === $data['type'] ) {
189 return $this->upgrade_plugin( $data['slug'] );
190 } elseif ( 'theme' === $data['type'] ) {
191 return $this->upgrade_theme( $data['base_slug'] );
192 }
193
194 // If type does not match.
195 return new WP_Error(
196 Error_Code::INVALID,
197 esc_html__( 'Please try again! We could not find the issue type.', 'defender-security' )
198 );
199 }
200
201 /**
202 * Upgrades the theme based on the provided slug.
203 *
204 * @param mixed $slug The slug of the theme to upgrade.
205 *
206 * @return array|WP_Error An array with a message if the upgrade is successful or a WP_Error on failure.
207 */
208 private function upgrade_theme( $slug ) {
209 $skin = new Silent_Skin();
210 $upgrader = new Theme_Upgrader( $skin );
211 $ret = $upgrader->upgrade( $slug );
212
213 if ( true === $ret ) {
214 $model = Scan::get_last();
215 $model->remove_issue( $this->owner->id );
216
217 do_action( 'wpdef_fixed_scan_issue', 'vulnerability', 'resolve' );
218
219 return array( 'message' => esc_html__( 'This item has been resolved.', 'defender-security' ) );
220 }
221
222 // This is WP error.
223 if ( is_wp_error( $ret ) ) {
224 return $ret;
225 }
226
227 // Sometimes it returns false because of it could not complete the update process.
228 return new WP_Error(
229 Error_Code::INVALID,
230 esc_html__( "We couldn't update your theme. Please try updating with another method.", 'defender-security' )
231 );
232 }
233
234 /**
235 * Upgrade a plugin based on the provided slug.
236 *
237 * @param mixed $slug The slug of the plugin to upgrade.
238 *
239 * @return array An array with information about the upgrade status.
240 * @since 2.8.1
241 */
242 private function upgrade_plugin( $slug ): array {
243 if ( null === $this->get_plugin_upgrade_path( $slug ) ) {
244 return array(
245 'type_notice' => 'error',
246 'message' => esc_html__(
247 'The plugin associated with this vulnerability is no longer installed.',
248 'defender-security'
249 ),
250 );
251 }
252
253 $skin = new Plugin_Skin();
254 $upgrader = new Plugin_Upgrader( $skin );
255 $result = $upgrader->bulk_upgrade( array( $slug ) );
256
257 if ( is_wp_error( $skin->result ) ) {
258 return array(
259 'type_notice' => 'error',
260 'message' => $skin->result->get_error_message(),
261 );
262 } elseif ( $skin->get_errors()->has_errors() ) {
263 return array(
264 'type_notice' => 'error',
265 'message' => $skin->get_error_messages(),
266 );
267 } elseif ( is_array( $result ) && isset( $result[ $slug ] ) && array() !== $result[ $slug ] ) {
268 $model = Scan::get_last();
269 $model->remove_issue( $this->owner->id );
270
271 do_action( 'wpdef_fixed_scan_issue', 'vulnerability', 'resolve' );
272
273 return array( 'message' => esc_html__( 'This item has been resolved.', 'defender-security' ) );
274 } elseif ( false === $result ) {
275 return array(
276 'type_notice' => 'error',
277 'message' => esc_html__(
278 'Unable to connect to the filesystem. Please confirm your credentials.',
279 'defender-security'
280 ),
281 );
282 }
283
284 return array(
285 'type_notice' => 'info',
286 'message' => esc_html__( 'There is no update available for this plugin.', 'defender-security' ),
287 );
288 }
289
290 /**
291 * Return the installed plugin path for a safe, existing plugin slug.
292 *
293 * @param mixed $slug Plugin file relative to the plugins directory.
294 *
295 * @return string|null
296 */
297 private function get_plugin_upgrade_path( $slug ): ?string {
298 if ( ! is_string( $slug ) || '' === $slug || 0 !== validate_file( $slug ) ) {
299 return null;
300 }
301
302 $path = trailingslashit( WP_PLUGIN_DIR ) . $slug;
303
304 return is_file( $path ) ? $path : null;
305 }
306
307 /**
308 * Removes a vulnerability at the specified path.
309 *
310 * @param string $path The path of the vulnerability to remove.
311 *
312 * @return bool Returns true on successful removal, false otherwise.
313 */
314 private function remove_vulnerability( string $path ): bool {
315 if ( is_dir( $path ) ) {
316 return $this->delete_dir( $path );
317 } else {
318 // Sometimes a plugin consists of one file.
319 wp_delete_file( $path );
320 return true;
321 }
322 }
323
324 /**
325 * Removes a plugin based on the provided data, checking if it is active and has the necessary permissions.
326 *
327 * @param array $data An array containing information about the plugin to be removed.
328 *
329 * @return array An array with the status of the removal process and any related messages.
330 */
331 private function remove_plugin( array $data ): array {
332 $active = $this->is_active_plugin( $data['slug'] );
333 if ( $active ) {
334 return array(
335 'type_notice' => 'error',
336 'message' => esc_html__( 'This plugin cannot be removed because it is active.', 'defender-security' ),
337 );
338 }
339
340 $abs_path = $this->get_abs_plugin_path_by_slug( $data['base_slug'] );
341 if ( file_exists( $abs_path ) && ! $this->remove_vulnerability( $abs_path ) ) {
342 return array(
343 'type_notice' => 'error',
344 'message' => esc_html__(
345 'Defender does not have enough permission to remove this plugin.',
346 'defender-security'
347 ),
348 );
349 }
350
351 $message = sprintf(
352 /* translators: %s: Plugin name. */
353 esc_html__( '%s plugin', 'defender-security' ),
354 '<b>' . $data['name'] . '</b>'
355 );
356 $this->log( $message . 'is deleted', Scan_Controller::SCAN_LOG );
357 $model = Scan::get_last();
358 $model->remove_issue( $this->owner->id );
359
360 do_action( 'wpdef_fixed_scan_issue', 'vulnerability', 'delete' );
361
362 return array(
363 'collect_type' => true,
364 'message' => $message,
365 );
366 }
367
368 /**
369 * Removes a theme from the system.
370 *
371 * @param array $data The data of the theme to be removed.
372 *
373 * @return array The result of the removal operation.
374 */
375 private function remove_theme( array $data ): array {
376 if ( ! isset( $data['base_slug'] ) || ! is_string( $data['base_slug'] ) || '' === $data['base_slug'] || 0 !== validate_file( $data['base_slug'] ) ) {
377 return array(
378 'type_notice' => 'error',
379 'message' => esc_html__( 'Invalid theme path.', 'defender-security' ),
380 );
381 }
382
383 if ( ! array_key_exists( $data['base_slug'], wp_get_themes() ) ) {
384 return array(
385 'type_notice' => 'error',
386 'message' => esc_html__( 'The requested theme is not installed.', 'defender-security' ),
387 );
388 }
389
390 $active = $this->is_active_theme( $data['base_slug'] );
391 if ( $active ) {
392 return array(
393 'type_notice' => 'error',
394 'message' => esc_html__( 'This theme cannot be removed because it is active.', 'defender-security' ),
395 );
396 }
397
398 $abs_path = $this->get_path_of_themes_dir() . $data['base_slug'];
399 $themes_dir = realpath( $this->get_path_of_themes_dir() );
400 $real_path = realpath( $abs_path );
401 if ( false === $themes_dir || false === $real_path || ! str_starts_with(
402 $real_path,
403 rtrim( $themes_dir, DIRECTORY_SEPARATOR ) . DIRECTORY_SEPARATOR
404 ) ) {
405 return array(
406 'type_notice' => 'error',
407 'message' => esc_html__( 'Invalid theme path.', 'defender-security' ),
408 );
409 }
410
411 if ( file_exists( $real_path ) && ! $this->remove_vulnerability( $real_path ) ) {
412 return array(
413 'type_notice' => 'error',
414 'message' => esc_html__(
415 'Defender does not have enough permission to remove this theme.',
416 'defender-security'
417 ),
418 );
419 }
420
421 $message = sprintf(
422 /* translators: %s: Plugin theme. */
423 esc_html__( '%s theme', 'defender-security' ),
424 '<b>' . $data['name'] . '</b>'
425 );
426 $this->log( $message . 'is deleted', Scan_Controller::SCAN_LOG );
427 $model = Scan::get_last();
428 $model->remove_issue( $this->owner->id );
429
430 do_action( 'wpdef_fixed_scan_issue', 'vulnerability', 'delete' );
431
432 return array(
433 'collect_type' => true,
434 'message' => $message,
435 );
436 }
437
438 /**
439 * Retrieves the vulnerability body text based on the provided bug data.
440 *
441 * @param array $bug An array containing information about the vulnerability.
442 *
443 * @return string The vulnerability body text.
444 */
445 protected function get_vulnerability_body( array $bug ): string {
446 $text = '#' . $bug['title'] . PHP_EOL;
447 $text .= '-' . esc_html__( 'Vulnerability type:', 'defender-security' ) . ' ' . $bug['vuln_type'] . PHP_EOL;
448 if ( ! isset( $bug['fixed_in'] ) || '' === $bug['fixed_in'] ) {
449 $text .= '-' . esc_html__( 'No Update Available', 'defender-security' ) . PHP_EOL;
450 } else {
451 $text .= '-' . esc_html__(
452 'This bug has been fixed in version:',
453 'defender-security'
454 ) . ' ' . $bug['fixed_in'] . PHP_EOL;
455 }
456
457 return $text;
458 }
459
460 /**
461 * Returns the vulnerability details as a string.
462 *
463 * @param array $data The data containing the vulnerability details.
464 *
465 * @return string The vulnerability details as a string.
466 */
467 public function get_detail_as_string( array $data ): string {
468 $strings = array();
469 foreach ( $data['bugs'] as $bug ) {
470 $strings[] = $this->get_vulnerability_body( $bug );
471 }
472
473 return implode( PHP_EOL, $strings );
474 }
475
476 /**
477 * Returns the details of vulnerabilities as an array.
478 *
479 * @param array $data The data containing information about vulnerabilities.
480 *
481 * @return array An array with vulnerability details including score and detail items.
482 */
483 public function get_details_as_array( array $data ): array {
484 $arr = array();
485 foreach ( $data['bugs'] as $bug ) {
486 $items = array(
487 esc_html__( 'Vulnerability type:', 'defender-security' ) . ' ' . $bug['vuln_type'],
488 );
489
490 if ( ! isset( $bug['fixed_in'] ) || '' === $bug['fixed_in'] ) {
491 $items[] = esc_html__( 'No Update Available', 'defender-security' );
492 } else {
493 $items[] = esc_html__( 'This bug has been fixed in version:', 'defender-security' ) . ' ' . $bug['fixed_in'];
494 }
495
496 $arr[] = array(
497 'score' => $bug['cvss_score'],
498 'title' => '#' . $bug['title'],
499 'items' => $items,
500 );
501 }
502
503 return $arr;
504 }
505
506 /**
507 * Delete inactive plugin or theme.
508 *
509 * @return array|WP_Error
510 */
511 public function delete() {
512 $data = $this->owner->raw_data;
513 // WP core, plugin or theme.
514 if ( 'wp_core' === $data['type'] ) {
515 return array(
516 'type_notice' => 'error',
517 'message' => esc_html__( 'WordPress core cannot be removed.', 'defender-security' ),
518 );
519 } elseif ( 'plugin' === $data['type'] ) {
520 return $this->remove_plugin( $data );
521 } elseif ( 'theme' === $data['type'] ) {
522 return $this->remove_theme( $data );
523 }
524
525 // Sometimes it returns false because of it could not complete the remove process.
526 return new WP_Error(
527 Error_Code::INVALID,
528 esc_html__( "We couldn't remove this item.", 'defender-security' )
529 );
530 }
531 }
532