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/class-getpaid-customers-query.php (2 issues)

1
<?php
2
/**
3
 * GetPaid_Customers_Query class
4
 *
5
 * Contains core class used to query for customers.
6
 *
7
 * @since 1.0.19
8
 */
9
10
/**
11
 * Main class used for querying customers.
12
 *
13
 * @since 1.0.19
14
 *
15
 * @see GetPaid_Subscriptions_Query::prepare_query() for information on accepted arguments.
16
 */
17
class GetPaid_Customers_Query {
18
19
	/**
20
	 * Query vars, after parsing
21
	 *
22
	 * @since 1.0.19
23
	 * @var array
24
	 */
25
	public $query_vars = array();
26
27
	/**
28
	 * List of found customers.
29
	 *
30
	 * @since 1.0.19
31
	 * @var array
32
	 */
33
	private $results;
34
35
	/**
36
	 * Total number of found customers for the current query
37
	 *
38
	 * @since 1.0.19
39
	 * @var int
40
	 */
41
	private $total_customers = 0;
42
43
	/**
44
	 * The SQL query used to fetch matching customers.
45
	 *
46
	 * @since 1.0.19
47
	 * @var string
48
	 */
49
	public $request;
50
51
	// SQL clauses
52
53
	/**
54
	 * Contains the 'FIELDS' sql clause
55
	 *
56
	 * @since 1.0.19
57
	 * @var string
58
	 */
59
	public $query_fields;
60
61
	/**
62
	 * Contains the 'FROM' sql clause
63
	 *
64
	 * @since 1.0.19
65
	 * @var string
66
	 */
67
	public $query_from;
68
69
	/**
70
	 * Contains the 'WHERE' sql clause
71
	 *
72
	 * @since 1.0.19
73
	 * @var string
74
	 */
75
	public $query_where;
76
77
	/**
78
	 * Contains the 'ORDER BY' sql clause
79
	 *
80
	 * @since 1.0.19
81
	 * @var string
82
	 */
83
	public $query_orderby;
84
85
	/**
86
	 * Contains the 'LIMIT' sql clause
87
	 *
88
	 * @since 1.0.19
89
	 * @var string
90
	 */
91
	public $query_limit;
92
93
	/**
94
	 * Class constructor.
95
	 *
96
	 * @since 1.0.19
97
	 *
98
	 * @param null|string|array $query Optional. The query variables.
99
	 */
100
	public function __construct( $query = null ) {
101
		if ( ! is_null( $query ) ) {
102
			$this->prepare_query( $query );
103
			$this->query();
104
		}
105
	}
106
107
	/**
108
	 * Fills in missing query variables with default values.
109
	 *
110
	 * @since 1.0.19
111
	 *
112
	 * @param  string|array $args Query vars, as passed to `GetPaid_Subscriptions_Query`.
113
	 * @return array Complete query variables with undefined ones filled in with defaults.
114
	 */
115
	public static function fill_query_vars( $args ) {
116
		$defaults = array(
117
			'include'     => array(),
118
			'exclude'     => array(),
119
			'orderby'     => 'id',
120
			'order'       => 'DESC',
121
			'offset'      => '',
122
			'number'      => 10,
123
			'paged'       => 1,
124
			'count_total' => true,
125
			'fields'      => 'all',
126
			's'           => '',
127
		);
128
129
		foreach ( GetPaid_Customer_Data_Store::get_database_fields() as $field => $type ) {
130
			$defaults[ $field ] = 'any';
131
132
			if ( '%f' === $type || '%d' === $type ) {
133
				$defaults[ $field . '_min' ] = '';
134
				$defaults[ $field . '_max' ] = '';
135
			}
136
		}
137
138
		return wp_parse_args( $args, $defaults );
0 ignored issues
show
Security Variable Injection introduced by
$args can contain request data and is used in variable name context(s) leading to a potential security vulnerability.

2 paths for user data to reach this point

  1. Path: Read from $_GET, and Data is passed through rawurlencode_deep(), and Data is passed through wpinv_clean(), and wpinv_clean(rawurlencode_deep($_GET[$field])) is assigned to $query in includes/admin/class-wpinv-customers-table.php on line 239
  1. Read from $_GET, and Data is passed through rawurlencode_deep(), and Data is passed through wpinv_clean(), and wpinv_clean(rawurlencode_deep($_GET[$field])) is assigned to $query
    in includes/admin/class-wpinv-customers-table.php on line 239
  2. getpaid_get_customers() is called
    in includes/admin/class-wpinv-customers-table.php on line 256
  3. Enters via parameter $args
    in includes/user-functions.php on line 20
  4. GetPaid_Customers_Query::__construct() is called
    in includes/user-functions.php on line 33
  5. Enters via parameter $query
    in includes/class-getpaid-customers-query.php on line 100
  6. GetPaid_Customers_Query::prepare_query() is called
    in includes/class-getpaid-customers-query.php on line 102
  7. Enters via parameter $query
    in includes/class-getpaid-customers-query.php on line 148
  8. GetPaid_Customers_Query::fill_query_vars() is called
    in includes/class-getpaid-customers-query.php on line 153
  9. Enters via parameter $args
    in includes/class-getpaid-customers-query.php on line 115
  2. Path: Read from $_GET, and Data is passed through rawurlencode_deep(), and Data is passed through wpinv_clean(), and wpinv_clean(rawurlencode_deep($_GET[$field])) is assigned to $query in includes/admin/class-wpinv-customers-table.php on line 232
  1. Read from $_GET, and Data is passed through rawurlencode_deep(), and Data is passed through wpinv_clean(), and wpinv_clean(rawurlencode_deep($_GET[$field])) is assigned to $query
    in includes/admin/class-wpinv-customers-table.php on line 232
  2. getpaid_get_customers() is called
    in includes/admin/class-wpinv-customers-table.php on line 256
  3. Enters via parameter $args
    in includes/user-functions.php on line 20
  4. GetPaid_Customers_Query::__construct() is called
    in includes/user-functions.php on line 33
  5. Enters via parameter $query
    in includes/class-getpaid-customers-query.php on line 100
  6. GetPaid_Customers_Query::prepare_query() is called
    in includes/class-getpaid-customers-query.php on line 102
  7. Enters via parameter $query
    in includes/class-getpaid-customers-query.php on line 148
  8. GetPaid_Customers_Query::fill_query_vars() is called
    in includes/class-getpaid-customers-query.php on line 153
  9. Enters via parameter $args
    in includes/class-getpaid-customers-query.php on line 115

Used in variable context

  1. wp_parse_args() is called
    in includes/class-getpaid-customers-query.php on line 137
  2. Enters via parameter $args
    in wordpress/wp-includes/functions.php on line 4821
  3. wp_parse_str() is called
    in wordpress/wp-includes/functions.php on line 4827
  4. Enters via parameter $input_string
    in wordpress/wp-includes/formatting.php on line 5148
  5. parse_str() is called
    in wordpress/wp-includes/formatting.php on line 5149

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
139
	}
140
141
	/**
142
	 * Prepare the query variables.
143
	 *
144
	 * @since 1.0.19
145
	 *
146
	 * @see self::fill_query_vars() For allowede args and their defaults.
147
	 */
148
	public function prepare_query( $query = array() ) {
149
		global $wpdb;
150
151
		if ( empty( $this->query_vars ) || ! empty( $query ) ) {
152
			$this->query_limit = null;
153
			$this->query_vars  = $this->fill_query_vars( $query );
154
		}
155
156
		if ( ! empty( $this->query_vars['fields'] ) && 'all' !== $this->query_vars['fields'] ) {
157
			$this->query_vars['fields'] = wpinv_parse_list( $this->query_vars['fields'] );
158
		}
159
160
		do_action( 'getpaid_pre_get_customers', array( &$this ) );
161
162
		// Ensure that query vars are filled after 'getpaid_pre_get_customers'.
163
		$qv                = & $this->query_vars;
164
		$qv                = $this->fill_query_vars( $qv );
165
		$table             = $wpdb->prefix . 'getpaid_customers';
166
		$this->query_from  = "FROM $table";
167
168
		// Prepare query fields.
169
		$this->prepare_query_fields( $qv, $table );
170
171
		// Prepare query where.
172
		$this->prepare_query_where( $qv, $table );
173
174
		// Prepare query order.
175
		$this->prepare_query_order( $qv, $table );
176
177
		// limit
178
		if ( isset( $qv['number'] ) && $qv['number'] > 0 ) {
179
			if ( $qv['offset'] ) {
180
				$this->query_limit = $wpdb->prepare( 'LIMIT %d, %d', $qv['offset'], $qv['number'] );
181
			} else {
182
				$this->query_limit = $wpdb->prepare( 'LIMIT %d, %d', $qv['number'] * ( $qv['paged'] - 1 ), $qv['number'] );
183
			}
184
		}
185
186
		do_action_ref_array( 'getpaid_after_customers_query', array( &$this ) );
187
	}
188
189
	/**
190
	 * Prepares the query fields.
191
	 *
192
	 * @since 1.0.19
193
	 *
194
	 * @param array $qv Query vars.
195
	 * @param string $table Table name.
196
	 */
197
	protected function prepare_query_fields( &$qv, $table ) {
198
199
		if ( is_array( $qv['fields'] ) ) {
200
			$qv['fields']   = array_unique( $qv['fields'] );
201
			$allowed_fields = array_keys( GetPaid_Customer_Data_Store::get_database_fields() );
202
203
			$query_fields = array();
204
			foreach ( $qv['fields'] as $field ) {
205
				if ( ! in_array( $field, $allowed_fields ) ) {
206
					continue;
207
				}
208
209
				$field          = sanitize_key( $field );
210
				$query_fields[] = "$table.`$field`";
211
			}
212
			$this->query_fields = implode( ',', $query_fields );
213
		} else {
214
			$this->query_fields = "$table.*";
215
		}
216
217
		if ( isset( $qv['count_total'] ) && $qv['count_total'] ) {
218
			$this->query_fields = 'SQL_CALC_FOUND_ROWS ' . $this->query_fields;
219
		}
220
221
	}
222
223
	/**
224
	 * Prepares the query where.
225
	 *
226
	 * @since 1.0.19
227
	 *
228
	 * @param array $qv Query vars.
229
	 * @param string $table Table name.
230
	 */
231
	protected function prepare_query_where( &$qv, $table ) {
232
		global $wpdb;
233
		$this->query_where = 'WHERE 1=1';
234
235
		// Fields.
236
		foreach ( GetPaid_Customer_Data_Store::get_database_fields() as $field => $type ) {
237
			if ( 'any' !== $qv[ $field ] ) {
238
239
				// In.
240
				if ( is_array( $qv[ $field ] ) ) {
241
					$in                 = join( ',', array_fill( 0, count( $qv[ $field ] ), $type ) );
242
					$this->query_where .= $wpdb->prepare( " AND $table.`status` IN ( $in )", $qv[ $field ] );
243
				} elseif ( ! empty( $qv[ $field ] ) ) {
244
					$this->query_where .= $wpdb->prepare( " AND $table.`$field` = $type", $qv[ $field ] );
245
				}
246
			}
247
248
			// Min/Max.
249
			if ( '%f' === $type || '%d' === $type ) {
250
251
				// Min.
252
				if ( is_numeric( $qv[ $field . '_min' ] ) ) {
253
					$this->query_where .= $wpdb->prepare( " AND $table.`$field` >= $type", $qv[ $field . '_min' ] );
254
				}
255
256
				// Max.
257
				if ( is_numeric( $qv[ $field . '_max' ] ) ) {
258
					$this->query_where .= $wpdb->prepare( " AND $table.`$field` <= $type", $qv[ $field . '_max' ] );
259
				}
260
			}
261
		}
262
263
		if ( ! empty( $qv['include'] ) ) {
264
			$include            = implode( ',', wp_parse_id_list( $qv['include'] ) );
265
			$this->query_where .= " AND $table.`id` IN ($include)";
266
		} elseif ( ! empty( $qv['exclude'] ) ) {
267
			$exclude            = implode( ',', wp_parse_id_list( $qv['exclude'] ) );
268
			$this->query_where .= " AND $table.`id` NOT IN ($exclude)";
269
		}
270
271
		// Date queries are allowed for the customer creation date.
272
		if ( ! empty( $qv['date_created_query'] ) && is_array( $qv['date_created_query'] ) ) {
273
			$date_created_query = new WP_Date_Query( $qv['date_created_query'], "$table.date_created" );
274
			$this->query_where .= $date_created_query->get_sql();
275
		}
276
277
		// Search.
278
		if ( ! empty( $qv['s'] ) ) {
279
			$this->query_where .= $this->get_search_sql( $qv['s'] );
280
		}
281
	}
282
283
	/**
284
	 * Used internally to generate an SQL string for searching across multiple columns
285
	 *
286
	 * @since 1.2.7
287
	 *
288
	 * @global wpdb $wpdb WordPress database abstraction object.
289
	 *
290
	 * @param string $string The string to search for.
291
	 * @return string
292
	 */
293
	protected function get_search_sql( $string ) {
294
		global $wpdb;
295
296
		$searches = array();
297
		$string   = trim( $string, '%' );
298
		$like     = '%' . $wpdb->esc_like( $string ) . '%';
299
300
		foreach ( array_keys( GetPaid_Customer_Data_Store::get_database_fields() ) as $col ) {
301
			if ( 'id' === $col || 'user_id' === $col ) {
302
				$searches[] = $wpdb->prepare( "$col = %s", $string );  // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
303
			} else {
304
				$searches[] = $wpdb->prepare( "$col LIKE %s", $like );  // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
305
			}
306
		}
307
308
		return ' AND (' . implode( ' OR ', $searches ) . ')';
309
	}
310
311
	/**
312
	 * Prepares the query order.
313
	 *
314
	 * @since 1.0.19
315
	 *
316
	 * @param array $qv Query vars.
317
	 * @param string $table Table name.
318
	 */
319
	protected function prepare_query_order( &$qv, $table ) {
320
321
		// sorting.
322
		$qv['order'] = isset( $qv['order'] ) ? strtoupper( $qv['order'] ) : '';
323
		$order       = $this->parse_order( $qv['order'] );
324
325
		// Default order is by 'id' (latest customers).
326
		if ( empty( $qv['orderby'] ) ) {
327
			$qv['orderby'] = array( 'id' );
328
		}
329
330
		// 'orderby' values may be an array, comma- or space-separated list.
331
		$ordersby      = array_filter( wpinv_parse_list( $qv['orderby'] ) );
332
333
		$orderby_array = array();
334
		foreach ( $ordersby as $_key => $_value ) {
335
336
			if ( is_int( $_key ) ) {
337
				// Integer key means this is a flat array of 'orderby' fields.
338
				$_orderby = $_value;
339
				$_order   = $order;
340
			} else {
341
				// Non-integer key means that the key is the field and the value is ASC/DESC.
342
				$_orderby = $_key;
343
				$_order   = $_value;
344
			}
345
346
			$parsed = $this->parse_orderby( $_orderby, $table );
347
348
			if ( $parsed ) {
349
				$orderby_array[] = $parsed . ' ' . $this->parse_order( $_order );
350
			}
351
		}
352
353
		// If no valid clauses were found, order by id.
354
		if ( empty( $orderby_array ) ) {
355
			$orderby_array[] = "id $order";
356
		}
357
358
		$this->query_orderby = 'ORDER BY ' . implode( ', ', $orderby_array );
359
360
	}
361
362
	/**
363
	 * Execute the query, with the current variables.
364
	 *
365
	 * @since 1.0.19
366
	 *
367
	 * @global wpdb $wpdb WordPress database abstraction object.
368
	 */
369
	public function query() {
370
		global $wpdb;
371
372
		$qv =& $this->query_vars;
373
374
		// Return a non-null value to bypass the default GetPaid customers query and remember to set the
375
		// total_customers property.
376
		$this->results = apply_filters_ref_array( 'getpaid_customers_pre_query', array( null, &$this ) );
377
378
		if ( null === $this->results ) {
379
			$this->request = "SELECT $this->query_fields $this->query_from $this->query_where $this->query_orderby $this->query_limit";
380
381
			if ( ( is_array( $qv['fields'] ) && 1 !== count( $qv['fields'] ) ) || 'all' === $qv['fields'] ) {
382
				$this->results = $wpdb->get_results( $this->request );
383
			} else {
384
				$this->results = $wpdb->get_col( $this->request );
385
			}
386
387
			if ( isset( $qv['count_total'] ) && $qv['count_total'] ) {
388
				$found_customers_query = apply_filters( 'getpaid_found_customers_query', 'SELECT FOUND_ROWS()', $this );
389
				$this->total_customers = (int) $wpdb->get_var( $found_customers_query );
390
			}
391
		}
392
393
		if ( 'all' === $qv['fields'] ) {
394
			foreach ( $this->results as $key => $customer ) {
395
				$this->set_cache( $customer->id, $customer, 'getpaid_customers' );
396
				$this->set_cache( $customer->user_id, $customer->id, 'getpaid_customer_ids_by_user_id' );
397
				$this->set_cache( $customer->email, $customer->id, 'getpaid_customer_ids_by_email' );
398
				$this->results[ $key ] = new GetPaid_Customer( $customer );
399
			}
400
		}
401
402
	}
403
404
	/**
405
	 * Set cache
406
	 *
407
	 * @param string  $id
408
	 * @param mixed   $data
409
	 * @param string  $group
410
	 * @param integer $expire
411
	 * @return boolean
412
	 */
413
	public function set_cache( $key, $data, $group = '', $expire = 0 ) {
414
415
		if ( empty( $key ) ) {
416
			return false;
417
		}
418
419
		wp_cache_set( $key, $data, $group, $expire );
420
	}
421
422
	/**
423
	 * Retrieve query variable.
424
	 *
425
	 * @since 1.0.19
426
	 *
427
	 * @param string $query_var Query variable key.
428
	 * @return mixed
429
	 */
430
	public function get( $query_var ) {
431
		if ( isset( $this->query_vars[ $query_var ] ) ) {
432
			return $this->query_vars[ $query_var ];
433
		}
434
435
		return null;
436
	}
437
438
	/**
439
	 * Set query variable.
440
	 *
441
	 * @since 1.0.19
442
	 *
443
	 * @param string $query_var Query variable key.
444
	 * @param mixed $value Query variable value.
445
	 */
446
	public function set( $query_var, $value ) {
447
		$this->query_vars[ $query_var ] = $value;
448
	}
449
450
	/**
451
	 * Return the list of customers.
452
	 *
453
	 * @since 1.0.19
454
	 *
455
	 * @return GetPaid_Customer[]|array Found customers.
456
	 */
457
	public function get_results() {
458
		return $this->results;
459
	}
460
461
	/**
462
	 * Return the total number of customers for the current query.
463
	 *
464
	 * @since 1.0.19
465
	 *
466
	 * @return int Number of total customers.
467
	 */
468
	public function get_total() {
469
		return $this->total_customers;
470
	}
471
472
	/**
473
	 * Parse and sanitize 'orderby' keys passed to the customers query.
474
	 *
475
	 * @since 1.0.19
476
	 *
477
	 * @param string $orderby Alias for the field to order by.
478
	 *  @param string $table The current table.
479
	 * @return string Value to use in the ORDER clause, if `$orderby` is valid.
480
	 */
481
	protected function parse_orderby( $orderby, $table ) {
482
483
		$_orderby = '';
484
		if ( in_array( $orderby, array_keys( GetPaid_Customer_Data_Store::get_database_fields() ), true ) ) {
485
			$_orderby = "$table.`$orderby`";
486
		} elseif ( 'id' === strtolower( $orderby ) ) {
487
			$_orderby = "$table.id";
488
		} elseif ( 'include' === $orderby && ! empty( $this->query_vars['include'] ) ) {
489
			$include     = wp_parse_id_list( $this->query_vars['include'] );
490
			$include_sql = implode( ',', $include );
491
			$_orderby    = "FIELD( $table.id, $include_sql )";
492
		}
493
494
		return $_orderby;
495
	}
496
497
	/**
498
	 * Parse an 'order' query variable and cast it to ASC or DESC as necessary.
499
	 *
500
	 * @since 1.0.19
501
	 *
502
	 * @param string $order The 'order' query variable.
503
	 * @return string The sanitized 'order' query variable.
504
	 */
505
	protected function parse_order( $order ) {
506
		if ( ! is_string( $order ) || empty( $order ) ) {
0 ignored issues
show
The condition is_string($order) is always true.
Loading history...
507
			return 'DESC';
508
		}
509
510
		if ( 'ASC' === strtoupper( $order ) ) {
511
			return 'ASC';
512
		} else {
513
			return 'DESC';
514
		}
515
	}
516
517
}
518