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

330 lines 9.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
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 wordpress.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 $slug => $plugin ) {
146 if ( false === strpos( $slug, '/' ) ) {
147 // Todo: get correct hashes for single-file plugins.
148 // Separate case for 'Hello Dolly'.
149 $base_slug = 'hello.php' === $slug ? 'hello-dolly' : $slug;
150 } else {
151 $base_slug = explode( '/', $slug );
152 $base_slug = array_shift( $base_slug );
153 }
154
155 if ( in_array( $base_slug, $excluded_slugs, true ) ) {
156 continue;
157 }
158
159 $plugin_hashes = $this->get_plugin_hash( $base_slug, $plugin['Version'] );
160
161 if ( ! empty( $plugin_hashes ) ) {
162 $all_plugin_hashes = array_merge( $all_plugin_hashes, $plugin_hashes );
163 }
164 }
165
166 return $all_plugin_hashes;
167 }
168
169 /**
170 * Performs integrity check on plugins.
171 *
172 * @return bool
173 */
174 public function plugin_integrity_check(): bool {
175 $abs_path = defender_replace_line( WP_PLUGIN_DIR );
176 $settings = wd_di()->get( Scan_Settings::class );
177 $plugins = new File(
178 $abs_path,
179 true,
180 false,
181 array(),
182 array( 'filename' => array( 'index.php' ) ),
183 true,
184 true,
185 $settings->filesize
186 );
187
188 $plugin_files = $plugins->get_dir_tree();
189 $plugin_files = array_filter( $plugin_files );
190
191 $plugin_files = new ArrayIterator( $plugin_files );
192 $checksums = $this->plugin_checksum();
193 $timer = new Timer();
194 $model = $this->owner->scan;
195 $pos = (int) $model->task_checkpoint;
196 $plugin_files->seek( $pos );
197 $slugs_of_edited_plugins = array();
198 $integration_smush = wd_di()->get( Smush::class );
199 $exist_smush_images = $integration_smush->exist_image_table();
200 while ( $plugin_files->valid() ) {
201 if ( ! $timer->check() ) {
202
203 $reason = 'break out cause too long';
204
205 /**
206 * Retrieves the Scan_Analytics class.
207 *
208 * @var Scan_Analytics $scan_analytics
209 */
210 $scan_analytics = wd_di()->get( Scan_Analytics::class );
211
212 $scan_analytics->track_feature(
213 $scan_analytics::EVENT_SCAN_FAILED,
214 array(
215 $scan_analytics::EVENT_SCAN_FAILED_PROP => $scan_analytics::EVENT_SCAN_FAILED_ERROR,
216 'Error_Reason' => $reason,
217 )
218 );
219
220 $this->log( $reason, Scan_Controller::SCAN_LOG );
221 break;
222 }
223
224 if ( $model->is_issue_whitelisted( $plugin_files->current() ) ) {
225 // This is whitelisted, so do nothing.
226 $plugin_files->next();
227 continue;
228 }
229
230 if ( $model->is_issue_ignored( $plugin_files->current() ) ) {
231 // This is ignored, so do nothing.
232 $plugin_files->next();
233 continue;
234 }
235
236 if (
237 $exist_smush_images
238 && $this->file_is_valid_image( $plugin_files->current() )
239 && $integration_smush->exist_image_path( $plugin_files->current() )
240 ) {
241 $this->log(
242 sprintf( 'skip %s because of Smush optimized file', $plugin_files->current() ),
243 Scan_Controller::SCAN_LOG
244 );
245 $plugin_files->next();
246 continue;
247 }
248
249 $file = $plugin_files->current();
250 $rev_file = str_replace( $abs_path, '', $file );
251 // Remove directory separator on the left.
252 $rev_file = ltrim( $rev_file, DIRECTORY_SEPARATOR );
253 $base_slug = explode( '/', $rev_file );
254 $base_slug = array_shift( $base_slug );
255
256 // Verify files only from wp.org. No Premium-things.
257 if ( isset( $checksums[ $rev_file ] ) ) {
258 if ( ! $this->compare_hashes( $file, $checksums[ $rev_file ] ) ) {
259 $slugs_of_edited_plugins[] = $base_slug;
260 $this->log( sprintf( 'modified %s', $file ), Scan_Controller::SCAN_LOG );
261 $model->add_item(
262 Scan_Item::TYPE_PLUGIN_CHECK,
263 array(
264 'file' => $file,
265 'type' => 'modified',
266 )
267 );
268 }
269 } elseif ( ! in_array( $base_slug, $this->premium_slugs, true ) ) {
270 // Unknown file in a plugin directory.
271 $slugs_of_edited_plugins[] = $base_slug;
272 }
273
274 $model->calculate_percent( $plugin_files->key() * 100 / $plugin_files->count(), 3 );
275 if ( 0 === $plugin_files->key() % 100 ) {
276 // We should update the model percent each 100 files so we have some progress on the screen.
277 $model->save();
278 }
279 $plugin_files->next();
280 }
281 if ( $plugin_files->valid() ) {
282 // Save the current progress and quit.
283 $model->task_checkpoint = $plugin_files->key();
284 } else {
285 // We will check if we have any ignore issue from last scan, so we can bring it here.
286 $last = Scan::get_last();
287 if ( is_object( $last ) ) {
288 $ignored_issues = $last->get_issues( Scan_Item::TYPE_PLUGIN_CHECK, Scan_Item::STATUS_IGNORE );
289 foreach ( $ignored_issues as $issue ) {
290 $model->add_item( Scan_Item::TYPE_PLUGIN_CHECK, $issue->raw_data, Scan_Item::STATUS_IGNORE );
291 }
292 }
293 // Done, reset this, so we can use later.
294 $model->task_checkpoint = '';
295 }
296 $model->save();
297 /**
298 * Reduce false positive reports. Check it only if enabled 'Suspicious code' option.
299 *
300 * @since 2.4.10
301 */
302 if ( ( new Scan_Settings() )->scan_malware ) {
303 if ( ! empty( $slugs_of_edited_plugins ) ) {
304 update_site_option( self::PLUGIN_SLUGS, array_unique( $slugs_of_edited_plugins ) );
305 }
306 if ( ! empty( $this->premium_slugs ) ) {
307 update_site_option( self::PLUGIN_PREMIUM_SLUGS, $this->premium_slugs );
308 }
309 }
310
311 return ! $plugin_files->valid();
312 }
313
314 /**
315 * Checks if given file is a valid image.
316 *
317 * @param string $file_path Full path to the file.
318 *
319 * @return bool True if file is a valid image, false otherwise.
320 */
321 private function file_is_valid_image( $file_path ): bool {
322 $mime = wp_check_filetype( $file_path );
323 if ( false === $mime['type'] ) {
324 return false;
325 }
326
327 return false !== strpos( $mime['type'], 'image' );
328 }
329 }
330