PluginProbe
Patchstack – WordPress & Plugins Security / 2.2.4
Patchstack – WordPress & Plugins Security v2.2.4
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 +279 -94 2.1.182.2.4 View file →
@@ -29,70 +29,92 @@
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 + * Some of the IP addresses of Patchstack.
96 + *
97 + * @var array
98 + */
99 + public $ips = [
100 + '18.221.197.243',
101 + '52.15.237.250',
102 + '3.19.3.34',
103 + '3.18.238.17',
104 + '13.58.49.77',
105 + '18.222.191.77',
106 + '3.131.108.250',
107 + '3.23.157.140',
108 + '18.220.70.233',
109 + '3.140.84.221',
110 + '185.212.171.100',
111 + '3.133.121.93',
112 + '18.219.61.133',
113 + '3.14.29.150'
114 + ];
115 +
116 + /**
95 117 * @param Patchstack $plugin
96 118 * @return void
97 119 */
98 120 public function __construct( $plugin ) {
@@ -109,9 +131,9 @@
109 131 * @return mixed
110 132 */
111 133 public function get_option( $name, $default = false ) {
112 134 // We always want to return the site option on the default settings management page.
113 - 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() ) {
114 136 return get_site_option( $name, $default );
115 137 }
116 138
117 139 // Get the setting of the current site.
@@ -175,69 +197,232 @@
175 197 return false;
176 198 }
177 199
178 200 /**
179 - * Determine if a given PHP function is disabled or not.
180 - *
181 - * @param string $name Name of the function to check.
182 - * @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
183 205 */
184 - public function function_available( $name ) {
185 - $safe_mode = ini_get( 'safe_mode' );
186 - 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 ) {
187 238 return false;
188 239 }
189 240
190 - // Determine if the function is available.
191 - 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', '' ) == '' ) {
192 243 return false;
193 244 }
194 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 +
195 252 return true;
196 253 }
197 254
198 255 /**
199 - * 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.
200 267 *
201 268 * @return string
202 269 */
203 270 public function get_ip() {
204 - // IP address header override set?
205 271 $override = get_site_option( 'patchstack_firewall_ip_header', '' );
206 272 if ( $override != '' && isset( $_SERVER[ $override ] ) ) {
207 273 return $_SERVER[ $override ];
208 274 }
209 275
210 - // IP address headers which should have priority and be used regardless of other headers.
211 - $priority = array( 'HTTP_CF_CONNECTING_IP', 'HTTP_X_SUCURI_CLIENTIP' );
212 - foreach ( $priority as $header ) {
213 - if ( isset( $_SERVER[ $header ] ) && filter_var( $_SERVER[ $header ], FILTER_VALIDATE_IP ) !== false ) {
214 - 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 + ];
215 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 + ];
216 389 }
390 + }
217 391
218 - // Special case for hosts that have a weird configuration.
219 - if ( $this->function_available( 'php_uname' ) ) {
220 - $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();
221 401
222 - // Bluehos and Hostmonster store the real IP in $_SERVER['REMOTE_ADDR'] but the proxy IP in HTTP_X_FORWARDED_FOR.t
223 - if ( strpos( $uname, 'bluehost' ) !== false || strpos( $uname, 'hostmonster' ) !== false ) {
224 - return $_SERVER['REMOTE_ADDR'];
225 - }
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 + }
226 406
227 - // Hostgator stores the real IP in $_SERVER['REMOTE_ADDR'] but the proxy IP in HTTP_X_FORWARDED_FOR.
228 - if ( ( strpos( $uname, 'websitewelcome' ) || strpos( $uname, 'hostgator' ) ) && isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) && $_SERVER['HTTP_X_FORWARDED_FOR'] != $_SERVER['REMOTE_ADDR'] ) {
229 - 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 );
230 416 }
417 + } catch ( Exception $e ) {
418 + return $cipher;
231 419 }
232 420
233 - // In order of priority, try to get the IP address.
234 - $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' );
235 - foreach ( $allowed as $header ) {
236 - if ( isset( $_SERVER[ $header ] ) && filter_var( $_SERVER[ $header ], FILTER_VALIDATE_IP ) !== false ) {
237 - return $_SERVER[ $header ];
238 - }
421 + // In case decryption failed, return null.
422 + if ( ! $dec ) {
423 + return null;
239 424 }
240 425
241 - return '127.0.0.1';
426 + return $dec;
242 427 }
243 428 }