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

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

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