Issues (850)

Security Analysis    4 potential vulnerabilities

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Code Injection (1)
Code Injection enables an attacker to execute arbitrary code on the server.
  Variable Injection (2)
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Cross-Site Scripting (1)
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  Header Injection
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/geolocation/class-getpaid-geolocation.php (2 issues)

Labels
Severity
1
<?php
2
/**
3
 * Geolocation class
4
 *
5
 * Handles geolocation of IP Addresses.
6
 *
7
 */
8
9
defined( 'ABSPATH' ) || exit;
10
11
/**
12
 * GetPaid_Geolocation Class.
13
 */
14
class GetPaid_Geolocation {
15
16
	/**
17
	 * Holds the current user's IP Address.
18
	 *
19
	 * @var string
20
	 */
21
	public static $current_user_ip;
22
23
	/**
24
	 * API endpoints for looking up a user IP address.
25
	 *
26
	 * For example, in case a user is on localhost.
27
	 *
28
	 * @var array
29
	 */
30
	protected static $ip_lookup_apis = array(
31
		'ipify'             => 'http://api.ipify.org/',
32
		'ipecho'            => 'http://ipecho.net/plain',
33
		'ident'             => 'http://ident.me',
34
		'whatismyipaddress' => 'http://bot.whatismyipaddress.com',
35
	);
36
37
	/**
38
	 * API endpoints for geolocating an IP address
39
	 *
40
	 * @var array
41
	 */
42
	protected static $geoip_apis = array(
43
		'ip-api.com' => 'http://ip-api.com/json/%s',
44
		'ipinfo.io'  => 'https://ipinfo.io/%s/json',
45
	);
46
47
	/**
48
	 * Get current user IP Address.
49
	 *
50
	 * @return string
51
	 */
52
	public static function get_ip_address() {
53
		return wpinv_get_ip();
54
	}
55
56
	/**
57
	 * Get user IP Address using an external service.
58
	 * This can be used as a fallback for users on localhost where
59
	 * get_ip_address() will be a local IP and non-geolocatable.
60
	 *
61
	 * @return string
62
	 */
63
	public static function get_external_ip_address() {
64
65
		$transient_name = 'external_ip_address_0.0.0.0';
66
67
		if ( '' !== self::get_ip_address() ) {
68
			$transient_name      = 'external_ip_address_' . self::get_ip_address();
69
		}
70
71
		// Try retrieving from cache.
72
		$external_ip_address = get_transient( $transient_name );
73
74
		if ( false === $external_ip_address ) {
75
			$external_ip_address     = '0.0.0.0';
76
			$ip_lookup_services      = apply_filters( 'getpaid_geolocation_ip_lookup_apis', self::$ip_lookup_apis );
77
			$ip_lookup_services_keys = array_keys( $ip_lookup_services );
78
			shuffle( $ip_lookup_services_keys );
79
80
			foreach ( $ip_lookup_services_keys as $service_name ) {
81
				$service_endpoint = $ip_lookup_services[ $service_name ];
82
				$response         = wp_safe_remote_get( $service_endpoint, array( 'timeout' => 2 ) );
83
84
				if ( ! is_wp_error( $response ) && rest_is_ip_address( $response['body'] ) ) {
85
					$external_ip_address = apply_filters( 'getpaid_geolocation_ip_lookup_api_response', wpinv_clean( $response['body'] ), $service_name );
86
					break;
87
				}
88
}
89
90
			set_transient( $transient_name, $external_ip_address, WEEK_IN_SECONDS );
91
		}
92
93
		return $external_ip_address;
94
	}
95
96
	/**
97
	 * Geolocate an IP address.
98
	 *
99
	 * @param  string $ip_address   IP Address.
100
	 * @param  bool   $fallback     If true, fallbacks to alternative IP detection (can be slower).
101
	 * @param  bool   $api_fallback If true, uses geolocation APIs if the database file doesn't exist (can be slower).
102
	 * @return array
103
	 */
104
	public static function geolocate_ip( $ip_address = '', $fallback = false, $api_fallback = true ) {
105
106
		if ( empty( $ip_address ) ) {
107
			$ip_address = self::get_ip_address();
108
		}
109
110
		// Update the current user's IP Address.
111
		self::$current_user_ip = $ip_address;
112
113
		// Filter to allow custom geolocation of the IP address.
114
		$country_code = apply_filters( 'getpaid_geolocate_ip', false, $ip_address, $fallback, $api_fallback );
115
116
		if ( false !== $country_code ) {
117
118
			return array(
119
				'country'  => $country_code,
120
				'state'    => '',
121
				'city'     => '',
122
				'postcode' => '',
123
			);
124
125
		}
126
127
		$country_code = self::get_country_code_from_headers();
128
129
		/**
130
		 * Get geolocation filter.
131
		 *
132
		 * @since 1.0.19
133
		 * @param array  $geolocation Geolocation data, including country, state, city, and postcode.
134
		 * @param string $ip_address  IP Address.
135
		 */
136
		$geolocation  = apply_filters(
137
			'getpaid_get_geolocation',
138
			array(
139
				'country'  => $country_code,
140
				'state'    => '',
141
				'city'     => '',
142
				'postcode' => '',
143
			),
144
			$ip_address
145
		);
146
147
		// If we still haven't found a country code, let's consider doing an API lookup.
148
		if ( '' === $geolocation['country'] && $api_fallback ) {
149
			$geolocation['country'] = self::geolocate_via_api( $ip_address );
150
		}
151
152
		// It's possible that we're in a local environment, in which case the geolocation needs to be done from the
153
		// external address.
154
		if ( '' === $geolocation['country'] && $fallback ) {
155
			$external_ip_address = self::get_external_ip_address();
156
157
			// Only bother with this if the external IP differs.
158
			if ( '0.0.0.0' !== $external_ip_address && $external_ip_address !== $ip_address ) {
159
				return self::geolocate_ip( $external_ip_address, false, $api_fallback );
160
			}
161
}
162
163
		return array(
164
			'country'  => $geolocation['country'],
165
			'state'    => $geolocation['state'],
166
			'city'     => $geolocation['city'],
167
			'postcode' => $geolocation['postcode'],
168
		);
169
170
	}
171
172
	/**
173
	 * Fetches the country code from the request headers, if one is available.
174
	 *
175
	 * @since 1.0.19
176
	 * @return string The country code pulled from the headers, or empty string if one was not found.
177
	 */
178
	protected static function get_country_code_from_headers() {
179
		$country_code = '';
180
181
		$headers = array(
182
			'MM_COUNTRY_CODE',
183
			'GEOIP_COUNTRY_CODE',
184
			'HTTP_CF_IPCOUNTRY',
185
			'HTTP_X_COUNTRY_CODE',
186
		);
187
188
		foreach ( $headers as $header ) {
189
			if ( empty( $_SERVER[ $header ] ) ) {
190
				continue;
191
			}
192
193
			$country_code = strtoupper( sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) ) );
0 ignored issues
show
It seems like wp_unslash($_SERVER[$header]) can also be of type array; however, parameter $str of sanitize_text_field() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

193
			$country_code = strtoupper( sanitize_text_field( /** @scrutinizer ignore-type */ wp_unslash( $_SERVER[ $header ] ) ) );
Loading history...
194
			break;
195
		}
196
197
		return $country_code;
198
	}
199
200
	/**
201
	 * Use APIs to Geolocate the user.
202
	 *
203
	 * Geolocation APIs can be added through the use of the getpaid_geolocation_geoip_apis filter.
204
	 * Provide a name=>value pair for service-slug=>endpoint.
205
	 *
206
	 * If APIs are defined, one will be chosen at random to fulfil the request. After completing, the result
207
	 * will be cached in a transient.
208
	 *
209
	 * @param  string $ip_address IP address.
210
	 * @return string
211
	 */
212
	protected static function geolocate_via_api( $ip_address ) {
213
214
		// Retrieve from cache...
215
		$country_code = get_transient( 'geoip_' . $ip_address );
216
217
		// If missing, retrieve from the API.
218
		if ( false === $country_code ) {
219
			$geoip_services = apply_filters( 'getpaid_geolocation_geoip_apis', self::$geoip_apis );
220
221
			if ( empty( $geoip_services ) ) {
222
				return '';
223
			}
224
225
			$geoip_services_keys = array_keys( $geoip_services );
226
227
			shuffle( $geoip_services_keys );
228
229
			foreach ( $geoip_services_keys as $service_name ) {
230
231
				$service_endpoint = $geoip_services[ $service_name ];
232
				$response         = wp_safe_remote_get( sprintf( $service_endpoint, $ip_address ), array( 'timeout' => 2 ) );
233
				$country_code     = sanitize_text_field( strtoupper( self::handle_geolocation_response( $response, $service_name ) ) );
0 ignored issues
show
It seems like $response can also be of type array; however, parameter $geolocation_response of GetPaid_Geolocation::handle_geolocation_response() does only seem to accept WP_Error|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

233
				$country_code     = sanitize_text_field( strtoupper( self::handle_geolocation_response( /** @scrutinizer ignore-type */ $response, $service_name ) ) );
Loading history...
234
235
				if ( ! empty( $country_code ) ) {
236
					break;
237
				}
238
}
239
240
			set_transient( 'geoip_' . $ip_address, $country_code, WEEK_IN_SECONDS );
241
		}
242
243
		return $country_code;
244
	}
245
246
	/**
247
	 * Handles geolocation response
248
	 *
249
	 * @param  WP_Error|String $geolocation_response
250
	 * @param  String $geolocation_service
251
	 * @return string Country code
252
	 */
253
	protected static function handle_geolocation_response( $geolocation_response, $geolocation_service ) {
254
255
		if ( is_wp_error( $geolocation_response ) || empty( $geolocation_response['body'] ) ) {
256
			return '';
257
		}
258
259
		if ( $geolocation_service === 'ipinfo.io' ) {
260
			$data = json_decode( $geolocation_response['body'] );
261
			return empty( $data ) || empty( $data->country ) ? '' : $data->country;
262
		}
263
264
		if ( $geolocation_service === 'ip-api.com' ) {
265
			$data = json_decode( $geolocation_response['body'] );
266
			return empty( $data ) || empty( $data->countryCode ) ? '' : $data->countryCode;
267
		}
268
269
		return apply_filters( 'getpaid_geolocation_geoip_response_' . $geolocation_service, '', $geolocation_response['body'] );
270
271
	}
272
273
}
274