PluginProbe
Patchstack – WordPress & Plugins Security / 2.2.9
Patchstack – WordPress & Plugins Security v2.2.9
2.3.7 trunk 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.17 2.1.18 2.1.19 2.1.2 2.1.20 2.1.21 2.1.22 2.1.23 2.1.24 2.1.25 2.1.3 2.1.4 2.1.5 2.1.6 All 49 releases
← All changes | includes/core.php +263 -97 2.1.212.2.9 View file →
@@ -29,68 +29,68 @@
29 29 * Allowed HTML for the wp_kses function used to render certain paragraphs of texts.
30 30 *
31 31 * @var array
32 32 */
33 - public $allowed_html = array(
34 - 'a' => array(
35 - 'href' => array(),
36 - 'title' => array(),
37 - 'target' => array()
38 - ),
39 - 'p' => array(
40 - 'style' => array()
41 - ),
42 - 'span' => array(
43 - 'style' => array()
44 - ),
45 - 'br' => array(),
46 - 'strong' => array(),
47 - 'b' => array(),
48 - 'i' => array(
49 - 'style' => array()
50 - ),
51 - 'label' => array(
52 - 'for' => array(),
53 - 'style' => array()
54 - ),
55 - 'input' => array(
56 - 'type' => array(),
57 - 'class' => array(),
58 - 'name' => array(),
59 - 'id' => array(),
60 - 'value' => array(),
61 - 'checked' => array(),
62 - 'style' => array()
63 - ),
64 - 'textarea' => array(
65 - 'rows' => array(),
66 - 'id' => array(),
67 - 'name' => array()
68 - ),
69 - 'select' => array(
70 - 'name' => array(),
71 - 'id' => array(),
72 - 'data-selected' => array()
73 - ),
74 - 'option' => array(
75 - 'value' => array(),
76 - 'selected' => array()
77 - ),
78 - 'table' => array(
79 - 'class' => array(),
80 - 'style' => array()
81 - ),
82 - 'thead' => array(),
83 - 'th' => array(
84 - 'style' => array()
85 - ),
86 - 'tr' => array(),
87 - 'td' => array(),
88 - 'div' => array(
89 - 'class' => array(),
90 - 'style' => array()
91 - )
92 - );
33 + public $allowed_html = [
34 + 'a' => [
35 + 'href' => [],
36 + 'title' => [],
37 + 'target' => []
38 + ],
39 + 'p' => [
40 + 'style' => []
41 + ],
42 + 'span' => [
43 + 'style' => []
44 + ],
45 + 'br' => [],
46 + 'strong' => [],
47 + 'b' => [],
48 + 'i' => [
49 + 'style' => []
50 + ],
51 + 'label' => [
52 + 'for' => [],
53 + 'style' => []
54 + ],
55 + 'input' => [
56 + 'type' => [],
57 + 'class' => [],
58 + 'name' => [],
59 + 'id' => [],
60 + 'value' => [],
61 + 'checked' => [],
62 + 'style' => []
63 + ],
64 + 'textarea' => [
65 + 'rows' => [],
66 + 'id' => [],
67 + 'name' => []
68 + ],
69 + 'select' => [
70 + 'name' => [],
71 + 'id' => [],
72 + 'data-selected' => []
73 + ],
74 + 'option' => [
75 + 'value' => [],
76 + 'selected' => []
77 + ],
78 + 'table' => [
79 + 'class' => [],
80 + 'style' => []
81 + ],
82 + 'thead' => [],
83 + 'th' => [
84 + 'style' => []
85 + ],
86 + 'tr' => [],
87 + 'td' => [],
88 + 'div' => [
89 + 'class' => [],
90 + 'style' => []
91 + ]
92 + ];
93 93
94 94 /**
95 95 * Some of the IP addresses of Patchstack.
96 96 *
@@ -95,9 +95,9 @@
95 95 * Some of the IP addresses of Patchstack.
96 96 *
97 97 * @var array
98 98 */
99 - public $ips = array(
99 + public $ips = [
100 100 '18.221.197.243',
101 101 '52.15.237.250',
102 102 '3.19.3.34',
103 103 '3.18.238.17',
@@ -106,10 +106,13 @@
106 106 '3.131.108.250',
107 107 '3.23.157.140',
108 108 '18.220.70.233',
109 109 '3.140.84.221',
110 - '185.212.171.100'
111 - );
110 + '185.212.171.100',
111 + '3.133.121.93',
112 + '18.219.61.133',
113 + '3.14.29.150'
114 + ];
112 115
113 116 /**
114 117 * @param Patchstack $plugin
115 118 * @return void
@@ -128,9 +131,9 @@
128 131 * @return mixed
129 132 */
130 133 public function get_option( $name, $default = false ) {
131 134 // We always want to return the site option on the default settings management page.
132 - if ( isset( $_GET['page'] ) && $_GET['page'] == 'patchstack-multisite-settings' && is_super_admin() ) {
135 + if ( isset( $_GET['page'] ) && $_GET['page'] == 'patchstack-multisite-settings' && function_exists( 'wp_get_current_user' ) && is_super_admin() ) {
133 136 return get_site_option( $name, $default );
134 137 }
135 138
136 139 // Get the setting of the current site.
@@ -194,69 +197,232 @@
194 197 return false;
195 198 }
196 199
197 200 /**
198 - * Determine if a given PHP function is disabled or not.
199 - *
200 - * @param string $name Name of the function to check.
201 - * @return boolean Whether or not the function is available to call.
201 + * Convert the subscription class name to its text variant.
202 + *
203 + * @param int $class
204 + * @return string
202 205 */
203 - public function function_available( $name ) {
204 - $safe_mode = ini_get( 'safe_mode' );
205 - if ( $safe_mode && strtolower( $safe_mode ) != 'off' ) {
206 + public function get_subscription_name( $class ) {
207 + switch ( $class ) {
208 + case 0:
209 + return 'Community';
210 + case 1:
211 + case 6:
212 + return 'Developer';
213 + case 7:
214 + return 'Business';
215 + default:
216 + return 'Unknown';
217 + }
218 + }
219 +
220 + /**
221 + * Determine if the user is a community user.
222 + *
223 + * @return boolean
224 + */
225 + public function is_community() {
226 + $class = get_option( 'patchstack_subscription_class', '');
227 + return $class != '' && (int) $class === 0;
228 + }
229 +
230 + /**
231 + * Determine if the plugin is connected to the API.
232 + *
233 + * @return boolean
234 + */
235 + public function is_connected() {
236 + // Determine if the API client id is set.
237 + if ( $this->plugin->client_id == 'PATCHSTACK_CLIENT_ID' && get_option( 'patchstack_clientid', false ) === false ) {
206 238 return false;
207 239 }
208 240
209 - // Determine if the function is available.
210 - if ( in_array( $name, array_map( 'trim', explode( ',', ini_get( 'disable_functions' ) ) ) ) ) {
241 + // Determine if we have an API token.
242 + if ( get_option( 'patchstack_api_token', '' ) == '' ) {
211 243 return false;
212 244 }
213 245
246 + // Determine if we have a last license check set.
247 + $last_license_check = get_option( 'patchstack_last_license_check', 0 );
248 + if ( !empty( $last_license_check ) && time() - $last_license_check >= 604800 ) {
249 + return false;
250 + }
251 +
214 252 return true;
215 253 }
216 254
217 255 /**
218 - * Attempt to get the client IP by checking all possible IP (proxy) headers.
256 + * Determine if the plugin provides protection.
257 + *
258 + * @return boolean
259 + */
260 + public function is_protected() {
261 + return get_option( 'patchstack_license_free', false) == 0;
262 + }
263 +
264 + /**
265 + * Grab the IP address of the user. Give the override IP header priority.
266 + * If this does not exist, we should always default to REMOTE_ADDR.
219 267 *
220 268 * @return string
221 269 */
222 270 public function get_ip() {
223 - // IP address header override set?
224 271 $override = get_site_option( 'patchstack_firewall_ip_header', '' );
225 272 if ( $override != '' && isset( $_SERVER[ $override ] ) ) {
226 273 return $_SERVER[ $override ];
227 274 }
228 275
229 - // IP address headers which should have priority and be used regardless of other headers.
230 - $priority = array( 'HTTP_CF_CONNECTING_IP', 'HTTP_X_SUCURI_CLIENTIP' );
231 - foreach ( $priority as $header ) {
232 - if ( isset( $_SERVER[ $header ] ) && filter_var( $_SERVER[ $header ], FILTER_VALIDATE_IP ) !== false ) {
233 - return $_SERVER[ $header ];
276 + return isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '';
277 + }
278 +
279 + /**
280 + * Grab the secret key used for API communication.
281 + *
282 + * @param string $custom
283 + * @return string
284 + */
285 + public function get_secret_key( $custom = '' ) {
286 + if ( $custom != '' ) {
287 + return $this->encrypt( $custom );
288 + }
289 +
290 + $secret = get_option( 'patchstack_secretkey', '' );
291 + if ( ! $secret ) {
292 + return '';
293 + }
294 +
295 + if ( strlen( $secret ) === 40 ) {
296 + $enc = $this->encrypt( $secret );
297 +
298 + update_option( 'patchstack_secretkey', $enc['cipher'] );
299 + update_option( 'patchstack_secretkey_nonce', $enc['nonce'] );
300 +
301 + return $secret;
302 + }
303 +
304 + $nonce = get_option( 'patchstack_secretkey_nonce' );
305 + return $this->decrypt( $secret, $nonce );
306 + }
307 +
308 + /**
309 + * Set the secret key used for API communication.
310 + *
311 + * @param string $secret
312 + * @return void
313 + */
314 + public function set_secret_key( $secret ) {
315 + $enc = $this->encrypt( $secret );
316 +
317 + update_option( 'patchstack_secretkey', $enc['cipher'] );
318 + update_option( 'patchstack_secretkey_nonce', $enc['nonce'] );
319 + }
320 +
321 + /**
322 + * Determine which encryption dependency we can use.
323 + *
324 + * @return string
325 + */
326 + public function get_enc_type() {
327 + if ( function_exists('sodium_crypto_generichash') ) {
328 + return 'native';
329 + }
330 +
331 + return 'compat';
332 + }
333 +
334 + /**
335 + * Get the unique nonce that is used for the secretbox.
336 + *
337 + * @return string
338 + */
339 + public function get_enc_nonce() {
340 + if ( function_exists('random_bytes') ) {
341 + return random_bytes( 24 );
342 + }
343 +
344 + require_once dirname( __FILE__ ) . '/2fa/polyfill/lib/random.php';
345 + return random_bytes( 24 );
346 + }
347 +
348 + /**
349 + * Encrypt a string.
350 + *
351 + * @param string $message
352 + * @return array
353 + */
354 + public function encrypt( $message ) {
355 + if ( is_null( $message ) || ! defined( 'AUTH_KEY' ) ) {
356 + return [
357 + 'cipher' => $message,
358 + 'nonce' => ''
359 + ];
360 + }
361 +
362 + $enc_type = $this->get_enc_type();
363 + $nonce = $this->get_enc_nonce();
364 +
365 + try {
366 + // Use the PHP native encryption functions.
367 + if ( $enc_type == 'native' ) {
368 + $key = sodium_crypto_generichash( AUTH_KEY );
369 +
370 + return [
371 + 'cipher' => sodium_bin2hex( sodium_crypto_secretbox( $message, $nonce, $key ) ),
372 + 'nonce' => sodium_bin2hex( $nonce )
373 + ];
234 374 }
375 +
376 + // Use the Sodium polyfill library part of WordPress core.
377 + require_once ABSPATH . WPINC . '/sodium_compat/autoload.php';
378 + $key = \Sodium\crypto_generichash( AUTH_KEY );
379 +
380 + return [
381 + 'cipher' => \Sodium\bin2hex( \Sodium\crypto_secretbox( $message, $nonce, $key ) ),
382 + 'nonce' => \Sodium\bin2hex( $nonce )
383 + ];
384 + } catch ( Exception $e ) {
385 + return [
386 + 'cipher' => $message,
387 + 'nonce' => ''
388 + ];
235 389 }
390 + }
236 391
237 - // Special case for hosts that have a weird configuration.
238 - if ( $this->function_available( 'php_uname' ) ) {
239 - $uname = @php_uname();
392 + /**
393 + * Decrypt a cipher to plain-text.
394 + *
395 + * @param string $cipher
396 + * @param string $nonce
397 + * @return string
398 + */
399 + public function decrypt( $cipher, $nonce ) {
400 + $enc_type = $this->get_enc_type();
240 401
241 - // Bluehos and Hostmonster store the real IP in $_SERVER['REMOTE_ADDR'] but the proxy IP in HTTP_X_FORWARDED_FOR.t
242 - if ( strpos( $uname, 'bluehost' ) !== false || strpos( $uname, 'hostmonster' ) !== false ) {
243 - return $_SERVER['REMOTE_ADDR'];
244 - }
402 + // If we received an empty nonce, we assume it was never properly encrypted to begin with.
403 + if ( $nonce == '' || ! defined( 'AUTH_KEY' ) ) {
404 + return $cipher;
405 + }
245 406
246 - // Hostgator stores the real IP in $_SERVER['REMOTE_ADDR'] but the proxy IP in HTTP_X_FORWARDED_FOR.
247 - if ( ( strpos( $uname, 'websitewelcome' ) || strpos( $uname, 'hostgator' ) ) && isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) && $_SERVER['HTTP_X_FORWARDED_FOR'] != $_SERVER['REMOTE_ADDR'] ) {
248 - return $_SERVER['REMOTE_ADDR'];
407 + try {
408 + // Determine if we should use native or polyfill functions.
409 + if ( $enc_type == 'native' ) {
410 + $key = sodium_crypto_generichash( AUTH_KEY );
411 + $dec = sodium_crypto_secretbox_open( sodium_hex2bin( $cipher ), sodium_hex2bin( $nonce ), $key );
412 + } else {
413 + require_once ABSPATH . WPINC . '/sodium_compat/autoload.php';
414 + $key = \Sodium\crypto_generichash( AUTH_KEY );
415 + $dec = \Sodium\crypto_secretbox_open( sodium_hex2bin( $cipher ), sodium_hex2bin( $nonce ), $key );
249 416 }
417 + } catch ( Exception $e ) {
418 + return $cipher;
250 419 }
251 420
252 - // In order of priority, try to get the IP address.
253 - $allowed = array( 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'SUCURI_RIP', 'REMOTE_ADDR' );
254 - foreach ( $allowed as $header ) {
255 - if ( isset( $_SERVER[ $header ] ) && filter_var( $_SERVER[ $header ], FILTER_VALIDATE_IP ) !== false ) {
256 - return $_SERVER[ $header ];
257 - }
421 + // In case decryption failed, return null.
422 + if ( ! $dec ) {
423 + return null;
258 424 }
259 425
260 - return '127.0.0.1';
426 + return $dec;
261 427 }
262 428 }