PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 3.1.0
Defender Security – Malware Scanner, Login Security & Firewall v3.1.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 / plugin-integrity.php

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

297 lines 8.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 declare( strict_types=1 );
3
4 namespace WP_Defender\Behavior\Scan;
5
6 use Calotes\Base\File;
7 use Calotes\Component\Behavior;
8 use WP_Defender\Component\Timer;
9 use WP_Defender\Model\Scan;
10 use WP_Defender\Model\Scan_Item;
11 use WP_Defender\Model\Setting\Scan as Scan_Settings;
12 use WP_Defender\Traits\IO;
13 use WP_Defender\Traits\Plugin;
14
15 class Plugin_Integrity extends Behavior {
16 use IO, Plugin;
17
18 public const URL_PLUGIN_VCS = 'https://downloads.wordpress.org/plugin-checksums/';
19 public const PLUGIN_SLUGS = 'wd_plugin_slugs_changes';
20 public const PLUGIN_PREMIUM_SLUGS = 'wd_plugin_premium_slugs';
21 /**
22 * List of premium plugin slugs.
23 *
24 * @var array
25 */
26 private $premium_slugs = [];
27
28 /**
29 * Check if the slug is a valid WordPress.org slug.
30 *
31 * @param string $slug
32 *
33 * @return bool
34 */
35 private function is_valid_wporg_slug( $slug ): bool {
36 return ! empty( $slug ) && '.' !== $slug;
37 }
38
39 /**
40 * Check the resulting plugin slug against WordPress.org plugin rules.
41 *
42 * @param string $slug
43 *
44 * @return bool
45 */
46 protected function is_likely_wporg_slug( $slug ): bool {
47 // Does file readme.txt exist?
48 $readme_file = trailingslashit( WP_PLUGIN_DIR ) . $slug . '/readme.txt';
49 if ( file_exists( $readme_file ) && is_readable( $readme_file ) ) {
50 $contents = trim( file_get_contents( $readme_file ) );
51
52 if ( false !== strpos( $contents, '===' ) ) {
53 return true;
54 }
55
56 if ( false !== strpos( $contents, '#' ) ) {
57 return true;
58 }
59 }
60
61 return false;
62 }
63
64 /**
65 * Reformat array.
66 *
67 * @param array $array
68 * @param string $field
69 * @param string $prefix Default empty line.
70 *
71 * @return array
72 */
73 private function pluck( $array, $field, $prefix = '' ): array {
74 $new_list = [];
75
76 foreach ( $array as $key => $value ) {
77 if ( is_object( $value ) ) {
78 $new_list[ $prefix . $key ] = $value->$field;
79 } else {
80 $new_list[ $prefix . $key ] = $value[ $field ];
81 }
82 }
83
84 return $new_list;
85 }
86
87 /**
88 * Retrieve hash for a given plugin from wordpress.org.
89 *
90 * @param string $slug Plugin folder.
91 * @param string $version Plugin version.
92 *
93 * @return array
94 */
95 private function get_plugin_hash( $slug, $version ): array {
96 if ( ! $this->is_valid_wporg_slug( $slug ) ) {
97 $this->premium_slugs[] = $slug;
98
99 return [];
100 }
101 // Get original from wp.org e.g. https://downloads.wordpress.org/plugin-checksums/hello-dolly/1.6.json.
102 $response = wp_remote_get( self::URL_PLUGIN_VCS . $slug . '/' . $version . '.json' );
103
104 if ( is_wp_error( $response ) ) {
105 $this->premium_slugs[] = $slug;
106 return [];
107 }
108
109 if ( 404 === (int) wp_remote_retrieve_response_code( $response ) ) {
110 // This plugin is not found on WordPress.org.
111 $this->premium_slugs[] = $slug;
112 return [];
113 }
114
115 $body = wp_remote_retrieve_body( $response );
116 if ( ! $body ) {
117 $this->premium_slugs[] = $slug;
118 return [];
119 }
120
121 $data = json_decode( $body, true );
122
123 if ( ! $data || empty( $data['files'] ) ) {
124 return [];
125 }
126
127 if ( ! $this->is_likely_wporg_slug( $slug ) ) {
128 $this->premium_slugs[] = $slug;
129
130 return [];
131 }
132
133 return $this->pluck( $data['files'], 'md5', $slug . DIRECTORY_SEPARATOR );
134 }
135
136 /**
137 * Fetch the checksums.
138 *
139 * @return array
140 */
141 protected function plugin_checksum(): array {
142 $all_plugin_hashes = [];
143 /**
144 * Exclude plugin slugs.
145 *
146 * @param array $slugs Slugs of excluded plugins.
147 * @since 3.1.0
148 */
149 $excluded_slugs = (array) apply_filters( 'wd_scan_excluded_plugin_slugs', [] );
150
151 foreach ( $this->get_plugins() as $slug => $plugin ) {
152 if ( false === strpos( $slug, '/' ) ) {
153 // Todo: get correct hashes for single-file plugins.
154 // Separate case for 'Hello Dolly'.
155 $base_slug = 'hello.php' === $slug ? 'hello-dolly' : $slug;
156 } else {
157 $base_slug = explode( '/', $slug );
158 $base_slug = array_shift( $base_slug );
159 }
160
161 if ( in_array( $base_slug, $excluded_slugs, true ) ) {
162 continue;
163 }
164
165 $plugin_hashes = $this->get_plugin_hash( $base_slug, $plugin['Version'] );
166
167 if ( ! empty( $plugin_hashes ) ) {
168 $all_plugin_hashes = array_merge( $all_plugin_hashes, $plugin_hashes );
169 }
170 }
171
172 return $all_plugin_hashes;
173 }
174
175 public function plugin_integrity_check(): bool {
176 $plugins = new File(
177 WP_PLUGIN_DIR,
178 true,
179 false,
180 [],
181 [ 'filename' => [ 'index.php' ] ],
182 true,
183 true
184 );
185
186 $plugin_files = $plugins->get_dir_tree();
187 $plugin_files = array_filter( $plugin_files );
188
189 $plugin_files = new \ArrayIterator( $plugin_files );
190 $checksums = $this->plugin_checksum();
191 $timer = new Timer();
192 $model = $this->owner->scan;
193 $pos = (int) $model->task_checkpoint;
194 $plugin_files->seek( $pos );
195 $slugs_of_edited_plugins = [];
196 $integration_smush = wd_di()->get( \WP_Defender\Integrations\Smush::class );
197 $exist_smush_images = $integration_smush->exist_image_table();
198 while ( $plugin_files->valid() ) {
199 if ( ! $timer->check() ) {
200 $this->log( 'break out cause too long', 'scan.log' );
201 break;
202 }
203
204 if ( $model->is_issue_whitelisted( $plugin_files->current() ) ) {
205 // This is whitelisted, so do nothing.
206 $plugin_files->next();
207 continue;
208 }
209
210 if ( $model->is_issue_ignored( $plugin_files->current() ) ) {
211 // This is ignored, so do nothing.
212 $plugin_files->next();
213 continue;
214 }
215
216 require_once( ABSPATH . 'wp-admin/includes/image.php' );
217 if (
218 $exist_smush_images
219 && file_is_valid_image( $plugin_files->current() )
220 && $integration_smush->exist_image_path( $plugin_files->current() )
221 ) {
222 $this->log(
223 sprintf( 'skip %s because of Smush optimized file', $plugin_files->current() ),
224 'scan.log'
225 );
226 $plugin_files->next();
227 continue;
228 }
229
230 // The file will be '\' instead of '/' on Windows, so we need to convert everything to '/'.
231 $file = $plugin_files->current();
232 // Get relative so we can compare.
233 $abs_path = WP_PLUGIN_DIR;
234 if ( defender_is_windows() ) {
235 // This mean we are on Windows.
236 $abs_path = str_replace( '/', DIRECTORY_SEPARATOR, $abs_path );
237 }
238 $rev_file = str_replace( $abs_path, '', $file );
239 // Remove the first \ on Windows.
240 $rev_file = str_replace( DIRECTORY_SEPARATOR, '/', $rev_file );
241 // Remove the first / on path.
242 $rev_file = ltrim( $rev_file, '/' );
243 // Verify files only from wp.org. No Premium-things.
244 if ( isset( $checksums[ $rev_file ] ) ) {
245 if ( ! $this->compare_hashes( $file, $checksums[ $rev_file ] ) ) {
246 $base_slug = explode( '/', $rev_file );
247 $slugs_of_edited_plugins[] = array_shift( $base_slug );
248 $this->log( sprintf( 'modified %s', $file ), 'scan.log' );
249 $model->add_item(
250 Scan_Item::TYPE_PLUGIN_CHECK,
251 [
252 'file' => $file,
253 'type' => 'modified',
254 ]
255 );
256 }
257 }
258 $model->calculate_percent( $plugin_files->key() * 100 / $plugin_files->count(), 3 );
259 if ( 0 === $plugin_files->key() % 100 ) {
260 // We should update the model percent each 100 files so we have some progress on the screen.
261 $model->save();
262 }
263 $plugin_files->next();
264 }
265 if ( $plugin_files->valid() ) {
266 // Save the current progress and quit.
267 $model->task_checkpoint = $plugin_files->key();
268 } else {
269 // We will check if we have any ignore issue from last scan, so we can bring it here.
270 $last = Scan::get_last();
271 if ( is_object( $last ) ) {
272 $ignored_issues = $last->get_issues( Scan_Item::TYPE_PLUGIN_CHECK, Scan_Item::STATUS_IGNORE );
273 foreach ( $ignored_issues as $issue ) {
274 $model->add_item( Scan_Item::TYPE_PLUGIN_CHECK, $issue->raw_data, Scan_Item::STATUS_IGNORE );
275 }
276 }
277 // Done, reset this, so we can use later.
278 $model->task_checkpoint = null;
279 }
280 $model->save();
281 /**
282 * Reduce false positive reports. Check it only if enabled 'Suspicious code' option.
283 * @since 2.4.10
284 */
285 if ( ( new Scan_Settings() )->scan_malware ) {
286 if ( ! empty( $slugs_of_edited_plugins ) ) {
287 update_site_option( self::PLUGIN_SLUGS, array_unique( $slugs_of_edited_plugins ) );
288 }
289 if ( ! empty( $this->premium_slugs ) ) {
290 update_site_option( self::PLUGIN_PREMIUM_SLUGS, $this->premium_slugs );
291 }
292 }
293 // Todo: add file and time limit improvement.
294 return ! $plugin_files->valid();
295 }
296 }
297