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

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

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