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