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 / theme-integrity.php

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

285 lines 8.3 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\Theme;
13 use WP_Error;
14
15 //Deprecated since 2.5.0.
16 class Theme_Integrity extends Behavior {
17 use IO, Theme;
18
19 public const URL_THEME_DOWNLOAD = 'https://downloads.wordpress.org/theme/';
20 public const THEME_SLUGS = 'wd_theme_slugs_changes';
21 public const THEME_PREMIUM_SLUGS = 'wd_theme_premium_slugs';
22 /**
23 * List of premium theme slugs.
24 *
25 * @var array
26 */
27 private $premium_slugs = [];
28
29 private function download_file( $url ) {
30 if ( ! function_exists( 'download_url' ) ) {
31 require_once ABSPATH . 'wp-admin' . DIRECTORY_SEPARATOR . 'includes' . DIRECTORY_SEPARATOR . 'file.php';
32 }
33 $is_file = download_url( $url );
34 if ( is_wp_error( $is_file ) ) {
35 return new WP_Error( 'defender_theme_download', $is_file->get_error_message() );
36 }
37 if ( ! realpath( $is_file ) ) {
38 return new WP_Error( 'defender_file_not_found', __( 'Downloaded file could not be found.', 'wpdef' ) );
39 }
40
41 return $is_file;
42 }
43
44 private function get_tmp_theme_folder() {
45 return $this->get_tmp_folder() . 'zip-themes' . DIRECTORY_SEPARATOR;
46 }
47
48 /**
49 * Get hashes of the uploaded theme.
50 *
51 * @param string $zip_file
52 * @param string $theme_folder
53 *
54 * @return array|WP_Error
55 */
56 private function get_hash_uploaded_archive( $zip_file, $theme_folder ) {
57 $unzip_folder = $this->get_tmp_theme_folder();
58
59 if ( ! file_exists( $unzip_folder . DIRECTORY_SEPARATOR . $theme_folder ) ) {
60 if ( ! function_exists( 'WP_Filesystem' ) ) {
61 include_once 'wp-admin/includes/file.php';
62 }
63 WP_Filesystem();
64
65 $unzip_result = unzip_file( $zip_file, $unzip_folder );
66
67 if ( ! $unzip_result ) {
68 return new WP_Error(
69 'defender_zip',
70 sprintf(
71 /* translators: %s: file path */
72 __( 'Unable to unzip file %s', 'wpdef' ),
73 $zip_file
74 )
75 );
76 }
77 }
78
79 $array_hashes = array();
80 $theme_unzip = new File(
81 $unzip_folder . $theme_folder,
82 true,
83 false,
84 array(),
85 array(),
86 true,
87 true
88 );
89
90 $theme_files = $theme_unzip->get_dir_tree();
91 $theme_files = array_filter( $theme_files );
92
93 $theme_files = new \ArrayIterator( $theme_files );
94 while ( $theme_files->valid() ) {
95 $file = $theme_files->current();
96 $rev_file = str_replace( $unzip_folder, '', $file );
97 $array_hashes[ $rev_file ] = md5_file( $file );
98 $theme_files->next();
99 }
100
101 return $array_hashes;
102 }
103
104 /**
105 * @param string $theme_folder
106 * @param object $theme WP_Theme.
107 *
108 * @return array
109 */
110 public function get_theme_hash( $theme_folder, $theme ) {
111 $url = self::URL_THEME_DOWNLOAD . $theme_folder . '.' . $theme->get( 'Version' ) . '.zip';
112 $tmp_zip_file = $this->download_file( $url );
113 if ( is_wp_error( $tmp_zip_file ) ) {
114 $this->log( 'Scan theme error: ' . $tmp_zip_file->get_error_message(), 'scan.log' );
115 return array();
116 }
117
118 $theme_hashes = $this->get_hash_uploaded_archive( $tmp_zip_file, $theme_folder );
119 unlink( $tmp_zip_file );
120 if ( is_wp_error( $theme_hashes ) ) {
121 $this->log( 'Scan theme error: ' . $theme_hashes->get_error_message(), 'scan.log' );
122 return array();
123 }
124 return $theme_hashes;
125 }
126
127 /**
128 * Fetch the checksums.
129 *
130 * @return array
131 */
132 public function theme_checksum() {
133 $all_theme_hashes = array();
134 foreach ( $this->get_themes() as $theme_folder => $theme ) {
135 if ( is_object( $theme->parent() ) ) {
136 continue;
137 }
138 $theme_hashes = $this->get_theme_hash( $theme_folder, $theme );
139 if ( ! empty( $theme_hashes ) ) {
140 $all_theme_hashes = array_merge( $all_theme_hashes, $theme_hashes );
141 } else {
142 $this->premium_slugs[] = $theme_folder;
143 }
144 }
145 // Remove tmp theme dirs.
146 $this->delete_dir( $this->get_tmp_theme_folder() );
147
148 return $all_theme_hashes;
149 }
150
151 /**
152 * Check if the themes' file is on touch.
153 */
154 public function theme_integrity_check() {
155 $checksums = $this->theme_checksum();
156 $theme_dir = WP_CONTENT_DIR . DIRECTORY_SEPARATOR . 'themes';
157 $exclude = array( 'filename' => array( 'index.php' ) );
158 if ( ! empty( $this->premium_slugs ) ) {
159 // Exclude premium theme files.
160 foreach ( $this->premium_slugs as $premium_slug ) {
161 $exclude['dir'][] = $theme_dir . DIRECTORY_SEPARATOR . $premium_slug;
162 }
163 }
164 // Get theme files.
165 $themes = new File(
166 $theme_dir,
167 true,
168 false,
169 array(),
170 $exclude,
171 true,
172 true
173 );
174
175 $theme_files = $themes->get_dir_tree();
176 $theme_files = array_filter( $theme_files );
177 $theme_files = new \ArrayIterator( $theme_files );
178 $timer = new Timer();
179 $model = $this->owner->scan;
180 $pos = (int) $model->task_checkpoint;
181 $theme_files->seek( $pos );
182 $slugs_of_edited_themes = array();
183 while ( $theme_files->valid() ) {
184 if ( ! $timer->check() ) {
185 $this->log( 'break out cause too long', 'scan.log' );
186 break;
187 }
188
189 if ( $model->is_issue_whitelisted( $theme_files->current() ) ) {
190 // This is whitelisted, so do nothing.
191 $theme_files->next();
192 continue;
193 }
194
195 if ( $model->is_issue_ignored( $theme_files->current() ) ) {
196 // This is ignored, so do nothing.
197 $theme_files->next();
198 continue;
199 }
200
201 // The file will be '\' instead of '/' on Windows, so we need to convert everything to '/'.
202 $file = $theme_files->current();
203 // Get relative so we can compare.
204 $abs_path = $theme_dir;
205 if ( defender_is_windows() ) {
206 // This mean we are on Windows.
207 $abs_path = str_replace( '/', DIRECTORY_SEPARATOR, $abs_path );
208 }
209 $rev_file = str_replace( $abs_path, '', $file );
210 // Remove the first \ on Windows.
211 $rev_file = str_replace( DIRECTORY_SEPARATOR, '/', $rev_file );
212 // Remove the first / on path.
213 $rev_file = ltrim( $rev_file, '/' );
214 if ( isset( $checksums[ $rev_file ] ) ) {
215 if ( ! $this->compare_hashes( $file, $checksums[ $rev_file ] ) ) {
216 $base_slug = explode( '/', $rev_file );
217 $slugs_of_edited_themes[] = array_shift( $base_slug );
218 $this->log( sprintf( 'modified %s', $file ), 'scan.log' );
219 $model->add_item(
220 Scan_Item::TYPE_THEME_CHECK,
221 array(
222 'file' => $file,
223 'type' => 'modified',
224 )
225 );
226 }
227 } else {
228 $base_slug = explode( '/', $rev_file );
229 $theme_slug = array_shift( $base_slug );
230 if ( ! in_array( $theme_slug, $slugs_of_edited_themes, true ) ) {
231 // Check is it an unknown free theme file.
232 $response = wp_remote_head( "https://wordpress.org/themes/$theme_slug/" );
233 if ( 200 === (int) wp_remote_retrieve_response_code( $response ) ) {
234 // No verify from wp.org.
235 $slugs_of_edited_themes[] = $theme_slug;
236 $model->add_item(
237 Scan_Item::TYPE_THEME_CHECK,
238 array(
239 'file' => $file,
240 'type' => is_dir( $file ) ? 'dir' : 'unversion',
241 )
242 );
243 }
244 }
245 }
246 $model->calculate_percent( $theme_files->key() * 100 / $theme_files->count(), 4 );
247 if ( 0 === $theme_files->key() % 100 ) {
248 // We should update the model percent each 100 files so we have some progress on the screen.
249 $model->save();
250 }
251 $theme_files->next();
252 }
253 if ( $theme_files->valid() ) {
254 // Save the current progress and quit.
255 $model->task_checkpoint = $theme_files->key();
256 } else {
257 // We will check if we have any ignore issue from last scan, so we can bring it here.
258 $last = Scan::get_last();
259 if ( is_object( $last ) ) {
260 $ignored_issues = $last->get_issues( Scan_Item::TYPE_THEME_CHECK, Scan_Item::STATUS_IGNORE );
261 foreach ( $ignored_issues as $issue ) {
262 $model->add_item( Scan_Item::TYPE_THEME_CHECK, $issue->raw_data, Scan_Item::STATUS_IGNORE );
263 }
264 }
265 // Done, reset this, so we can use later.
266 $model->task_checkpoint = null;
267 }
268 $model->save();
269 /**
270 * Reduce false positive reports. Check it only if enabled 'Suspicious code' option.
271 * @since 2.4.10
272 */
273 if ( ( new Scan_Settings() )->scan_malware ) {
274 if ( ! empty( $slugs_of_edited_themes ) ) {
275 update_site_option( self::THEME_SLUGS, array_unique( $slugs_of_edited_themes ) );
276 }
277 if ( ! empty( $this->premium_slugs ) ) {
278 update_site_option( self::THEME_PREMIUM_SLUGS, $this->premium_slugs );
279 }
280 }
281 // Todo: add file and time limit improvement.
282 return ! $theme_files->valid();
283 }
284 }
285