PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.2
Defender Security – Malware Scanner, Login Security & Firewall v6.2.2
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.2.2, at src/behavior/scan-item/class-vuln-result.php

506 lines 14.9 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 $active = $this->is_active_theme( $data['base_slug'] );
377 if ( $active ) {
378 return array(
379 'type_notice' => 'error',
380 'message' => esc_html__( 'This theme cannot be removed because it is active.', 'defender-security' ),
381 );
382 }
383
384 $abs_path = $this->get_path_of_themes_dir() . $data['base_slug'];
385 if ( file_exists( $abs_path ) && ! $this->remove_vulnerability( $abs_path ) ) {
386 return array(
387 'type_notice' => 'error',
388 'message' => esc_html__(
389 'Defender does not have enough permission to remove this theme.',
390 'defender-security'
391 ),
392 );
393 }
394
395 $message = sprintf(
396 /* translators: %s: Plugin theme. */
397 esc_html__( '%s theme', 'defender-security' ),
398 '<b>' . $data['name'] . '</b>'
399 );
400 $this->log( $message . 'is deleted', Scan_Controller::SCAN_LOG );
401 $model = Scan::get_last();
402 $model->remove_issue( $this->owner->id );
403
404 do_action( 'wpdef_fixed_scan_issue', 'vulnerability', 'delete' );
405
406 return array(
407 'collect_type' => true,
408 'message' => $message,
409 );
410 }
411
412 /**
413 * Retrieves the vulnerability body text based on the provided bug data.
414 *
415 * @param array $bug An array containing information about the vulnerability.
416 *
417 * @return string The vulnerability body text.
418 */
419 protected function get_vulnerability_body( array $bug ): string {
420 $text = '#' . $bug['title'] . PHP_EOL;
421 $text .= '-' . esc_html__( 'Vulnerability type:', 'defender-security' ) . ' ' . $bug['vuln_type'] . PHP_EOL;
422 if ( ! isset( $bug['fixed_in'] ) || '' === $bug['fixed_in'] ) {
423 $text .= '-' . esc_html__( 'No Update Available', 'defender-security' ) . PHP_EOL;
424 } else {
425 $text .= '-' . esc_html__(
426 'This bug has been fixed in version:',
427 'defender-security'
428 ) . ' ' . $bug['fixed_in'] . PHP_EOL;
429 }
430
431 return $text;
432 }
433
434 /**
435 * Returns the vulnerability details as a string.
436 *
437 * @param array $data The data containing the vulnerability details.
438 *
439 * @return string The vulnerability details as a string.
440 */
441 public function get_detail_as_string( array $data ): string {
442 $strings = array();
443 foreach ( $data['bugs'] as $bug ) {
444 $strings[] = $this->get_vulnerability_body( $bug );
445 }
446
447 return implode( PHP_EOL, $strings );
448 }
449
450 /**
451 * Returns the details of vulnerabilities as an array.
452 *
453 * @param array $data The data containing information about vulnerabilities.
454 *
455 * @return array An array with vulnerability details including score and detail items.
456 */
457 public function get_details_as_array( array $data ): array {
458 $arr = array();
459 foreach ( $data['bugs'] as $bug ) {
460 $items = array(
461 esc_html__( 'Vulnerability type:', 'defender-security' ) . ' ' . $bug['vuln_type'],
462 );
463
464 if ( ! isset( $bug['fixed_in'] ) || '' === $bug['fixed_in'] ) {
465 $items[] = esc_html__( 'No Update Available', 'defender-security' );
466 } else {
467 $items[] = esc_html__( 'This bug has been fixed in version:', 'defender-security' ) . ' ' . $bug['fixed_in'];
468 }
469
470 $arr[] = array(
471 'score' => $bug['cvss_score'],
472 'title' => '#' . $bug['title'],
473 'items' => $items,
474 );
475 }
476
477 return $arr;
478 }
479
480 /**
481 * Delete inactive plugin or theme.
482 *
483 * @return array|WP_Error
484 */
485 public function delete() {
486 $data = $this->owner->raw_data;
487 // WP core, plugin or theme.
488 if ( 'wp_core' === $data['type'] ) {
489 return array(
490 'type_notice' => 'error',
491 'message' => esc_html__( 'WordPress core cannot be removed.', 'defender-security' ),
492 );
493 } elseif ( 'plugin' === $data['type'] ) {
494 return $this->remove_plugin( $data );
495 } elseif ( 'theme' === $data['type'] ) {
496 return $this->remove_theme( $data );
497 }
498
499 // Sometimes it returns false because of it could not complete the remove process.
500 return new WP_Error(
501 Error_Code::INVALID,
502 esc_html__( "We couldn't remove this item.", 'defender-security' )
503 );
504 }
505 }
506