PluginProbe
Patchstack – WordPress & Plugins Security / 2.1.21
Patchstack – WordPress & Plugins Security v2.1.21
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 +115 -295 trunk2.1.21 View file →
@@ -22,9 +22,9 @@
22 22 * Whether or not the site is a multisite.
23 23 *
24 24 * @var boolean
25 25 */
26 - public $is_multi_site = false;
26 + private $is_multi_site = false;
27 27
28 28 /**
29 29 * Allowed HTML for the wp_kses function used to render certain paragraphs of texts.
30 30 *
@@ -29,70 +29,89 @@
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 = [
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 - ];
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 + );
93 93
94 94 /**
95 + * Some of the IP addresses of Patchstack.
96 + *
97 + * @var array
98 + */
99 + public $ips = array(
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 + );
112 +
113 + /**
95 114 * @param Patchstack $plugin
96 115 * @return void
97 116 */
98 117 public function __construct( $plugin ) {
@@ -109,9 +128,9 @@
109 128 * @return mixed
110 129 */
111 130 public function get_option( $name, $default = false ) {
112 131 // We always want to return the site option on the default settings management page.
113 - if ( isset( $_GET['page'] ) && $_GET['page'] == 'patchstack-multisite-settings' && function_exists( 'wp_get_current_user' ) && is_super_admin() ) {
132 + if ( isset( $_GET['page'] ) && $_GET['page'] == 'patchstack-multisite-settings' && is_super_admin() ) {
114 133 return get_site_option( $name, $default );
115 134 }
116 135
117 136 // Get the setting of the current site.
@@ -116,14 +135,8 @@
116 135
117 136 // Get the setting of the current site.
118 137 $secondary = get_option( $name, $default );
119 138
120 - // On single-site installs there is no network option to reconcile, so
121 - // avoid the extra get_site_option() lookup on every read.
122 - if ( ! is_multisite() ) {
123 - return $secondary;
124 - }
125 -
126 139 // Get the setting of the network and in case there's a difference,
127 140 // return the value of site.
128 141 $main = get_site_option( $name, $default );
129 142 return $main != $secondary ? $secondary : $main;
@@ -173,9 +186,9 @@
173 186 return true;
174 187 }
175 188
176 189 $expiry = get_option( 'patchstack_license_expiry', '' );
177 - if ( $expiry != '' && ( strtotime( $expiry ) > ( time() - ( 3600 * 24 ) ) ) ) {
190 + if ( $expiry != '' && ( strtotime( $expiry ) < ( time() + ( 3600 * 24 ) ) ) ) {
178 191 return true;
179 192 }
180 193
181 194 return false;
@@ -181,262 +194,69 @@
181 194 return false;
182 195 }
183 196
184 197 /**
185 - * Determine if the plugin is connected to the API.
186 - *
187 - * @return boolean
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.
188 202 */
189 - public function is_connected() {
190 - // Determine if the API client id is set.
191 - if ( $this->plugin->client_id == 'PATCHSTACK_CLIENT_ID' && ! get_option( 'patchstack_clientid' ) ) {
203 + public function function_available( $name ) {
204 + $safe_mode = ini_get( 'safe_mode' );
205 + if ( $safe_mode && strtolower( $safe_mode ) != 'off' ) {
192 206 return false;
193 207 }
194 208
195 - // Determine if we have an API token.
196 - if ( get_option( 'patchstack_api_token', '' ) == '' ) {
209 + // Determine if the function is available.
210 + if ( in_array( $name, array_map( 'trim', explode( ',', ini_get( 'disable_functions' ) ) ) ) ) {
197 211 return false;
198 212 }
199 213
200 - // Determine if we have a last license check set.
201 - $last_license_check = get_option( 'patchstack_last_license_check', 0 );
202 - if ( !empty( $last_license_check ) && time() - $last_license_check >= 604800 ) {
203 - return false;
204 - }
205 -
206 214 return true;
207 215 }
208 216
209 217 /**
210 - * Determine if the plugin provides protection.
211 - *
212 - * @return boolean
213 - */
214 - public function is_protected() {
215 - return (int) get_option( 'patchstack_license_free', 0 ) == 0;
216 - }
217 -
218 - /**
219 - * Format a UNIX timestamp as a short relative-time string for the connection card.
220 - * Returns "Never" for empty/zero, otherwise "Just now" / "Xm ago" / "Xh ago" / "Xd ago".
218 + * Attempt to get the client IP by checking all possible IP (proxy) headers.
221 219 *
222 - * The returned string is the raw translated value — escape it at the call site.
223 - *
224 - * @param int $timestamp UNIX timestamp.
225 - * @return string Translated relative-time label (not escaped).
226 - */
227 - public function format_relative_time( $timestamp ) {
228 - $timestamp = (int) $timestamp;
229 - if ( $timestamp <= 0 ) {
230 - return __( 'Never', 'patchstack' );
231 - }
232 -
233 - $diff = time() - $timestamp;
234 - if ( $diff < 60 ) {
235 - return __( 'Just now', 'patchstack' );
236 - }
237 - if ( $diff < 3600 ) {
238 - /* translators: %d: number of minutes since the last sync. */
239 - return sprintf( __( '%dm ago', 'patchstack' ), (int) floor( $diff / 60 ) );
240 - }
241 - if ( $diff < 86400 ) {
242 - /* translators: %d: number of hours since the last sync. */
243 - return sprintf( __( '%dh ago', 'patchstack' ), (int) floor( $diff / 3600 ) );
244 - }
245 - /* translators: %d: number of days since the last sync. */
246 - return sprintf( __( '%dd ago', 'patchstack' ), (int) floor( $diff / 86400 ) );
247 - }
248 -
249 - /**
250 - * Get the timestamp of the last successful API sync.
251 - *
252 - * Prefers patchstack_last_sync, which is stamped on every successful (200 OK)
253 - * API request (log/software uploads, rule pulls, license verify, ping, etc.),
254 - * so it reflects real sync activity rather than only license verification.
255 - * Falls back to patchstack_last_license_check for sites that have not synced
256 - * yet since this option was introduced.
257 - *
258 - * @return int UNIX timestamp, or 0 if never synced.
259 - */
260 - public function get_last_sync_time() {
261 - $last_sync = (int) get_option( 'patchstack_last_sync', 0 );
262 - if ( $last_sync > 0 ) {
263 - return $last_sync;
264 - }
265 -
266 - return (int) get_option( 'patchstack_last_license_check', 0 );
267 - }
268 -
269 - /**
270 - * Grab the IP address of the user. Give the override IP header priority.
271 - * If this does not exist, we should always default to REMOTE_ADDR.
272 - *
273 220 * @return string
274 221 */
275 222 public function get_ip() {
276 - $override = get_option( 'patchstack_firewall_ip_header', '' );
223 + // IP address header override set?
224 + $override = get_site_option( 'patchstack_firewall_ip_header', '' );
277 225 if ( $override != '' && isset( $_SERVER[ $override ] ) ) {
278 226 return $_SERVER[ $override ];
279 227 }
280 228
281 - return isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '';
282 - }
283 -
284 - /**
285 - * Grab the secret key used for API communication.
286 - *
287 - * @param string $custom
288 - * @return string
289 - */
290 - public function get_secret_key( $custom = '' ) {
291 - if ( $custom != '' ) {
292 - return $this->encrypt( $custom );
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 ];
234 + }
293 235 }
294 236
295 - $secret = get_option( 'patchstack_secretkey', '' );
296 - if ( ! $secret ) {
297 - return '';
298 - }
237 + // Special case for hosts that have a weird configuration.
238 + if ( $this->function_available( 'php_uname' ) ) {
239 + $uname = @php_uname();
299 240
300 - if ( strlen( $secret ) === 40 ) {
301 - $enc = $this->encrypt( $secret );
302 -
303 - update_option( 'patchstack_secretkey', $enc['cipher'] );
304 - update_option( 'patchstack_secretkey_nonce', $enc['nonce'] );
305 -
306 - return $secret;
307 - }
308 -
309 - $nonce = get_option( 'patchstack_secretkey_nonce' );
310 - return $this->decrypt( $secret, $nonce );
311 - }
312 -
313 - /**
314 - * Set the secret key used for API communication.
315 - *
316 - * @param string $secret
317 - * @return void
318 - */
319 - public function set_secret_key( $secret ) {
320 - $enc = $this->encrypt( $secret );
321 -
322 - update_option( 'patchstack_secretkey', $enc['cipher'] );
323 - update_option( 'patchstack_secretkey_nonce', $enc['nonce'] );
324 - }
325 -
326 - /**
327 - * Determine which encryption dependency we can use.
328 - *
329 - * @return string
330 - */
331 - public function get_enc_type() {
332 - if ( function_exists('sodium_crypto_generichash') ) {
333 - return 'native';
334 - }
335 -
336 - return 'compat';
337 - }
338 -
339 - /**
340 - * Get the unique nonce that is used for the secretbox.
341 - *
342 - * @return string
343 - */
344 - public function get_enc_nonce() {
345 - if ( function_exists('random_bytes') ) {
346 - return random_bytes( 24 );
347 - }
348 -
349 - require_once dirname( __FILE__ ) . '/2fa/polyfill/lib/random.php';
350 - return random_bytes( 24 );
351 - }
352 -
353 - /**
354 - * Encrypt a string.
355 - *
356 - * @param string $message
357 - * @return array
358 - */
359 - public function encrypt( $message ) {
360 - if ( is_null( $message ) || ! defined( 'AUTH_KEY' ) ) {
361 - return [
362 - 'cipher' => $message,
363 - 'nonce' => ''
364 - ];
365 - }
366 -
367 - $enc_type = $this->get_enc_type();
368 - $nonce = $this->get_enc_nonce();
369 -
370 - try {
371 - // Use the PHP native encryption functions.
372 - if ( $enc_type == 'native' ) {
373 - $key = sodium_crypto_generichash( AUTH_KEY );
374 -
375 - return [
376 - 'cipher' => sodium_bin2hex( sodium_crypto_secretbox( $message, $nonce, $key ) ),
377 - 'nonce' => sodium_bin2hex( $nonce )
378 - ];
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'];
379 244 }
380 245
381 - // Use the Sodium polyfill library part of WordPress core.
382 - if ( ! file_exists( ABSPATH . WPINC . '/sodium_compat/autoload.php' ) ) {
383 - return [
384 - 'cipher' => $message,
385 - 'nonce' => ''
386 - ];
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'];
387 249 }
388 - require_once ABSPATH . WPINC . '/sodium_compat/autoload.php';
389 - $key = \Sodium\crypto_generichash( AUTH_KEY );
390 -
391 - return [
392 - 'cipher' => \Sodium\bin2hex( \Sodium\crypto_secretbox( $message, $nonce, $key ) ),
393 - 'nonce' => \Sodium\bin2hex( $nonce )
394 - ];
395 - } catch ( Exception $e ) {
396 - return [
397 - 'cipher' => $message,
398 - 'nonce' => ''
399 - ];
400 250 }
401 - }
402 251
403 - /**
404 - * Decrypt a cipher to plain-text.
405 - *
406 - * @param string $cipher
407 - * @param string $nonce
408 - * @return string
409 - */
410 - public function decrypt( $cipher, $nonce ) {
411 - $enc_type = $this->get_enc_type();
412 -
413 - // If we received an empty nonce, we assume it was never properly encrypted to begin with.
414 - if ( $nonce == '' || ! defined( 'AUTH_KEY' ) ) {
415 - return $cipher;
416 - }
417 -
418 - try {
419 - // Determine if we should use native or polyfill functions.
420 - if ( $enc_type == 'native' ) {
421 - $key = sodium_crypto_generichash( AUTH_KEY );
422 - $dec = sodium_crypto_secretbox_open( sodium_hex2bin( $cipher ), sodium_hex2bin( $nonce ), $key );
423 - } else {
424 - if ( ! file_exists( ABSPATH . WPINC . '/sodium_compat/autoload.php' ) ) {
425 - return $cipher;
426 - }
427 - require_once ABSPATH . WPINC . '/sodium_compat/autoload.php';
428 - $key = \Sodium\crypto_generichash( AUTH_KEY );
429 - $dec = \Sodium\crypto_secretbox_open( sodium_hex2bin( $cipher ), sodium_hex2bin( $nonce ), $key );
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 ];
430 257 }
431 - } catch ( Exception $e ) {
432 - return $cipher;
433 258 }
434 259
435 - // In case decryption failed, return null.
436 - if ( ! $dec ) {
437 - return null;
438 - }
439 -
440 - return $dec;
260 + return '127.0.0.1';
441 261 }
442 262 }