PluginProbe
Patchstack – WordPress & Plugins Security / 2.1.10
Patchstack – WordPress & Plugins Security v2.1.10
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 +96 -295 trunk2.1.10 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,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 = [
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 95 * @param Patchstack $plugin
96 96 * @return void
@@ -109,9 +109,9 @@
109 109 * @return mixed
110 110 */
111 111 public function get_option( $name, $default = false ) {
112 112 // 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() ) {
113 + if ( isset( $_GET['page'] ) && $_GET['page'] == 'patchstack-multisite-settings' && is_super_admin() ) {
114 114 return get_site_option( $name, $default );
115 115 }
116 116
117 117 // Get the setting of the current site.
@@ -116,14 +116,8 @@
116 116
117 117 // Get the setting of the current site.
118 118 $secondary = get_option( $name, $default );
119 119
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 120 // Get the setting of the network and in case there's a difference,
127 121 // return the value of site.
128 122 $main = get_site_option( $name, $default );
129 123 return $main != $secondary ? $secondary : $main;
@@ -173,9 +167,9 @@
173 167 return true;
174 168 }
175 169
176 170 $expiry = get_option( 'patchstack_license_expiry', '' );
177 - if ( $expiry != '' && ( strtotime( $expiry ) > ( time() - ( 3600 * 24 ) ) ) ) {
171 + if ( $expiry != '' && ( strtotime( $expiry ) < ( time() + ( 3600 * 24 ) ) ) ) {
178 172 return true;
179 173 }
180 174
181 175 return false;
@@ -181,262 +175,69 @@
181 175 return false;
182 176 }
183 177
184 178 /**
185 - * Determine if the plugin is connected to the API.
186 - *
187 - * @return boolean
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.
188 183 */
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' ) ) {
184 + public function function_available( $name ) {
185 + $safe_mode = ini_get( 'safe_mode' );
186 + if ( $safe_mode && strtolower( $safe_mode ) != 'off' ) {
192 187 return false;
193 188 }
194 189
195 - // Determine if we have an API token.
196 - if ( get_option( 'patchstack_api_token', '' ) == '' ) {
190 + // Determine if the function is available.
191 + if ( in_array( $name, array_map( 'trim', explode( ',', ini_get( 'disable_functions' ) ) ) ) ) {
197 192 return false;
198 193 }
199 194
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 195 return true;
207 196 }
208 197
209 198 /**
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".
199 + * Attempt to get the client IP by checking all possible IP (proxy) headers.
221 200 *
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 201 * @return string
274 202 */
275 203 public function get_ip() {
276 - $override = get_option( 'patchstack_firewall_ip_header', '' );
204 + // IP address header override set?
205 + $override = get_site_option( 'patchstack_firewall_ip_header', '' );
277 206 if ( $override != '' && isset( $_SERVER[ $override ] ) ) {
278 207 return $_SERVER[ $override ];
279 208 }
280 209
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 );
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 ];
215 + }
293 216 }
294 217
295 - $secret = get_option( 'patchstack_secretkey', '' );
296 - if ( ! $secret ) {
297 - return '';
298 - }
218 + // Special case for hosts that have a weird configuration.
219 + if ( $this->function_available( 'php_uname' ) ) {
220 + $uname = @php_uname();
299 221
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 - ];
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'];
379 225 }
380 226
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 - ];
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'];
387 230 }
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 231 }
401 - }
402 232
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 );
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 ];
430 238 }
431 - } catch ( Exception $e ) {
432 - return $cipher;
433 239 }
434 240
435 - // In case decryption failed, return null.
436 - if ( ! $dec ) {
437 - return null;
438 - }
439 -
440 - return $dec;
241 + return '127.0.0.1';
441 242 }
442 243 }