PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.0
Defender Security – Malware Scanner, Login Security & Firewall v6.2.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 / class-plugin-integrity.php

class-plugin-integrity.php in Defender Security – Malware Scanner, Login Security & Firewall 6.2.0, at src/behavior/scan/class-plugin-integrity.php

347 lines 10.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles plugin integrity scan.
4 *
5 * @package WP_Defender\Behavior\Scan
6 */
7
8 namespace WP_Defender\Behavior\Scan;
9
10 use ArrayIterator;
11 use Calotes\Base\File;
12 use WP_Defender\Traits\IO;
13 use WP_Defender\Model\Scan;
14 use WP_Defender\Traits\Plugin;
15 use Calotes\Component\Behavior;
16 use WP_Defender\Component\Timer;
17 use WP_Defender\Model\Scan_Item;
18 use WP_Defender\Integrations\Smush;
19 use WP_Defender\Model\Setting\Scan as Scan_Settings;
20 use WP_Defender\Helper\Analytics\Scan as Scan_Analytics;
21 use WP_Defender\Controller\Scan as Scan_Controller;
22 use WP_Defender\Component\Scan as Scan_Component;
23
24 /**
25 * It is responsible for performing integrity checks on plugins.
26 */
27 class Plugin_Integrity extends Behavior {
28
29 use IO;
30 use Plugin;
31
32 public const URL_PLUGIN_VCS = 'https://downloads.wordpress.org/plugin-checksums/';
33 public const PLUGIN_SLUGS = 'wd_plugin_slugs_changes';
34 public const PLUGIN_PREMIUM_SLUGS = 'wd_plugin_premium_slugs';
35 /**
36 * List of premium plugin slugs.
37 *
38 * @var array
39 */
40 private $premium_slugs = array();
41
42 /**
43 * Check if the given slug is a valid WordPress.org slug.
44 *
45 * @param string $slug The slug to check.
46 *
47 * @return bool Returns true if the slug is valid, false otherwise.
48 */
49 private function is_valid_wporg_slug( string $slug ): bool {
50 return '' !== $slug && '.' !== $slug;
51 }
52
53 /**
54 * Plucks a specific field from an array of objects or arrays and returns a new array with the plucked values.
55 *
56 * @param array $collection The array to pluck values from.
57 * @param string $prefix Optional. A prefix to add to each key in the new array. Default is an empty string.
58 *
59 * @return array The new array with the plucked values.
60 */
61 private function pluck( array $collection, $prefix = '' ): array {
62 $new_list = array();
63
64 foreach ( $collection as $key => $value ) {
65 $prefix_key = defender_replace_line( $prefix . $key );
66 if ( is_object( $value ) && property_exists( $value, 'md5' ) ) {
67 $new_list[ $prefix_key ] = $value->md5;
68 } elseif ( is_array( $value ) && isset( $value['md5'] ) ) {
69 $new_list[ $prefix_key ] = $value['md5'];
70 }
71 }
72
73 return $new_list;
74 }
75
76 /**
77 * Check if a plugin is a WPMUDEV pro plugin by checking for WDP ID header.
78 *
79 * @param string $plugin_file Plugin main file path.
80 *
81 * @return bool True if it's a WPMUDEV pro plugin, false otherwise.
82 */
83 private function is_wpmudev_pro_plugin( $plugin_file ): bool {
84 $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin_file );
85 return array_key_exists( 'WDP ID', $plugin_data ) && '' !== $plugin_data['WDP ID'];
86 }
87
88 /**
89 * Retrieve hash for a given plugin from wp.org.
90 * Excludes premium plugins and WPMUDEV pro plugins from hash retrieval.
91 *
92 * @param string $slug Plugin folder.
93 * @param string $version Plugin version.
94 * @param string $plugin_file Plugin main file path.
95 *
96 * @return array
97 */
98 private function get_plugin_hash( string $slug, $version, $plugin_file = '' ): array {
99 if ( ! $this->is_valid_wporg_slug( $slug ) ) {
100 $this->premium_slugs[] = $slug;
101
102 return array();
103 }
104
105 // Check if this is a WPMUDEV pro plugin that upgraded from free version.
106 if ( '' !== $plugin_file && $this->is_wpmudev_pro_plugin( $plugin_file ) ) {
107 $this->premium_slugs[] = $slug;
108
109 return array();
110 }
111
112 // Get original from wp.org e.g. https://downloads.wordpress.org/plugin-checksums/hello-dolly/1.6.json.
113 $response = wp_remote_get( self::URL_PLUGIN_VCS . $slug . '/' . $version . '.json' );
114
115 if ( is_wp_error( $response ) ) {
116 $this->premium_slugs[] = $slug;
117
118 return array();
119 }
120
121 if ( 404 === (int) wp_remote_retrieve_response_code( $response ) ) {
122 // This plugin is not found on WordPress.org.
123 $this->premium_slugs[] = $slug;
124
125 return array();
126 }
127
128 $body = wp_remote_retrieve_body( $response );
129 if ( ! $body ) {
130 $this->premium_slugs[] = $slug;
131
132 return array();
133 }
134
135 $data = json_decode( $body, true );
136
137 if ( ! isset( $data['files'] ) || ! is_array( $data['files'] ) || array() === $data['files'] ) {
138 return array();
139 }
140
141 if ( ! $this->is_likely_wporg_slug( $slug ) ) {
142 $this->premium_slugs[] = $slug;
143
144 return array();
145 }
146
147 return $this->pluck( $data['files'], $slug . DIRECTORY_SEPARATOR );
148 }
149
150 /**
151 * Fetch the checksums.
152 *
153 * @return array
154 */
155 protected function plugin_checksum(): array {
156 $all_plugin_hashes = array();
157 $actioned_plugins = get_site_option( Scan_Component::PLUGINS_ACTIONED );
158
159 if ( Scan_Component::are_actioned_plugins( $actioned_plugins ) ) {
160 foreach ( $actioned_plugins as $slug => $plugin_data ) {
161 $plugin_hashes = $this->get_plugin_hash( $slug, $plugin_data['Version'], $plugin_data['Slug'] );
162
163 if ( array() !== $plugin_hashes ) {
164 $all_plugin_hashes = array_merge( $all_plugin_hashes, $plugin_hashes );
165 }
166 }
167 } else {
168 // An emergency option if for some reason the plugin's quick data is not saved in the first step.
169 foreach ( $this->get_plugins() as $plugin_file => $plugin ) {
170 $slug = $this->get_plugin_slug_by( $plugin_file );
171 $plugin_hashes = $this->get_plugin_hash( $slug, $plugin['Version'], $plugin_file );
172
173 if ( array() !== $plugin_hashes ) {
174 $all_plugin_hashes = array_merge( $all_plugin_hashes, $plugin_hashes );
175 }
176 }
177 }
178
179 return $all_plugin_hashes;
180 }
181
182 /**
183 * Performs integrity check on plugins.
184 *
185 * @return bool
186 */
187 public function plugin_integrity_check(): bool {
188 $abs_path = defender_replace_line( WP_PLUGIN_DIR );
189 $settings = wd_di()->get( Scan_Settings::class );
190 $plugins = new File(
191 $abs_path,
192 true,
193 false,
194 array(),
195 array( 'filename' => array( 'index.php' ) ),
196 true,
197 true,
198 $settings->filesize
199 );
200
201 $plugin_files = $plugins->get_dir_tree();
202 $plugin_files = array_filter(
203 $plugin_files,
204 function ( $value ) {
205 return is_string( $value ) && '' !== trim( $value );
206 }
207 );
208
209 $plugin_files = new ArrayIterator( $plugin_files );
210 $checksums = $this->plugin_checksum();
211 $timer = new Timer();
212 $model = $this->owner->scan;
213 $pos = (int) $model->task_checkpoint;
214 $plugin_files->seek( $pos );
215 $slugs_of_edited_plugins = array();
216 $integration_smush = wd_di()->get( Smush::class );
217 $exist_smush_images = $integration_smush->exist_image_table();
218 while ( $plugin_files->valid() ) {
219 if ( ! $timer->check() ) {
220
221 $reason = 'break out cause too long';
222
223 /**
224 * Retrieves the Scan_Analytics class.
225 *
226 * @var Scan_Analytics $scan_analytics
227 */
228 $scan_analytics = wd_di()->get( Scan_Analytics::class );
229
230 $scan_analytics->track_feature(
231 $scan_analytics::EVENT_SCAN_FAILED,
232 array(
233 $scan_analytics::EVENT_SCAN_FAILED_PROP => $scan_analytics::EVENT_SCAN_FAILED_ERROR,
234 'Error_Reason' => $reason,
235 )
236 );
237
238 $this->log( $reason, Scan_Controller::SCAN_LOG );
239 break;
240 }
241
242 if ( $model->is_issue_whitelisted( $plugin_files->current() ) ) {
243 // This is whitelisted, so do nothing.
244 $plugin_files->next();
245 continue;
246 }
247
248 if ( $model->is_issue_ignored( $plugin_files->current() ) ) {
249 // This is ignored, so do nothing.
250 $plugin_files->next();
251 continue;
252 }
253
254 if (
255 $exist_smush_images
256 && $this->file_is_valid_image( $plugin_files->current() )
257 && $integration_smush->exist_image_path( $plugin_files->current() )
258 ) {
259 $this->log(
260 sprintf( 'skip %s because of Smush optimized file', $plugin_files->current() ),
261 Scan_Controller::SCAN_LOG
262 );
263 $plugin_files->next();
264 continue;
265 }
266
267 $file = $plugin_files->current();
268 $rev_file = str_replace( $abs_path, '', $file );
269 // Remove directory separator on the left.
270 $rev_file = ltrim( $rev_file, DIRECTORY_SEPARATOR );
271 $base_slug = explode( '/', $rev_file );
272 $base_slug = array_shift( $base_slug );
273
274 // Verify files only from wp.org. No Premium-things.
275 if ( isset( $checksums[ $rev_file ] ) ) {
276 if ( ! $this->compare_hashes( $file, $checksums[ $rev_file ] ) ) {
277 $slugs_of_edited_plugins[] = $base_slug;
278 $this->log( sprintf( 'modified %s', $file ), Scan_Controller::SCAN_LOG );
279 $model->add_item(
280 Scan_Item::TYPE_PLUGIN_CHECK,
281 array(
282 'file' => $file,
283 'type' => 'modified',
284 )
285 );
286 }
287 } elseif ( ! in_array( $base_slug, $this->premium_slugs, true ) ) {
288 // Unknown file in a plugin directory.
289 $slugs_of_edited_plugins[] = $base_slug;
290 }
291
292 $model->calculate_percent( $plugin_files->key() * 100 / $plugin_files->count(), 3 );
293 if ( 0 === $plugin_files->key() % 100 ) {
294 // We should update the model percent each 100 files so we have some progress on the screen.
295 $model->save();
296 }
297 $plugin_files->next();
298 }
299 if ( $plugin_files->valid() ) {
300 // Save the current progress and quit.
301 $model->task_checkpoint = $plugin_files->key();
302 } else {
303 // We will check if we have any ignore issue from last scan, so we can bring it here.
304 $last = Scan::get_last();
305 if ( is_object( $last ) ) {
306 $ignored_issues = $last->get_issues( Scan_Item::TYPE_PLUGIN_CHECK, Scan_Item::STATUS_IGNORE );
307 $model->carry_forward_ignored_issues( Scan_Item::TYPE_PLUGIN_CHECK, $ignored_issues, 'file' );
308 }
309 // Done, reset this, so we can use later.
310 $model->task_checkpoint = '';
311 }
312 $model->save();
313
314 /**
315 * Reduce false positive reports. Check it only if enabled 'Suspicious code' option.
316 *
317 * @since 2.4.10
318 */
319 if ( ( new Scan_Settings() )->scan_malware ) {
320 if ( array() !== $slugs_of_edited_plugins ) {
321 update_site_option( self::PLUGIN_SLUGS, array_unique( $slugs_of_edited_plugins ) );
322 }
323 if ( array() !== $this->premium_slugs ) {
324 update_site_option( self::PLUGIN_PREMIUM_SLUGS, $this->premium_slugs );
325 }
326 }
327
328 return ! $plugin_files->valid();
329 }
330
331 /**
332 * Checks if given file is a valid image.
333 *
334 * @param string $file_path Full path to the file.
335 *
336 * @return bool True if file is a valid image, false otherwise.
337 */
338 private function file_is_valid_image( $file_path ): bool {
339 $mime = wp_check_filetype( $file_path );
340 if ( false === $mime['type'] ) {
341 return false;
342 }
343
344 return false !== strpos( $mime['type'], 'image' );
345 }
346 }
347