PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.5.1
Defender Security – Malware Scanner, Login Security & Firewall v5.5.1
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.5.1, at src/behavior/scan/class-plugin-integrity.php

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