Issues (1182)

Security Analysis    not enabled

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

  Cross-Site Scripting
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.
  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.
  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.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  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.
  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.
  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.
  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.
  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.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  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.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
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-wc-download-handler.php (5 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
if ( ! defined( 'ABSPATH' ) ) {
4
	exit; // Exit if accessed directly
5
}
6
7
/**
8
 * Download handler.
9
 *
10
 * Handle digital downloads.
11
 *
12
 * @class 		WC_Download_Handler
13
 * @version		2.2.0
14
 * @package		WooCommerce/Classes
15
 * @category	Class
16
 * @author 		WooThemes
17
 */
18
class WC_Download_Handler {
19
20
	/**
21
	 * Hook in methods.
22
	 */
23
	public static function init() {
24
		if ( isset( $_GET['download_file'] ) && isset( $_GET['order'] ) && isset( $_GET['email'] ) ) {
25
			add_action( 'init', array( __CLASS__, 'download_product' ) );
26
		}
27
		add_action( 'woocommerce_download_file_redirect', array( __CLASS__, 'download_file_redirect' ), 10, 2 );
28
		add_action( 'woocommerce_download_file_xsendfile', array( __CLASS__, 'download_file_xsendfile' ), 10, 2 );
29
		add_action( 'woocommerce_download_file_force', array( __CLASS__, 'download_file_force' ), 10, 2 );
30
	}
31
32
	/**
33
	 * Check if we need to download a file and check validity.
34
	 */
35
	public static function download_product() {
36
		$product_id    = absint( $_GET['download_file'] );
37
		$_product      = wc_get_product( $product_id );
38
		$download_data = self::get_download_data( array(
39
			'product_id'  => $product_id,
40
			'order_key'   => wc_clean( $_GET['order'] ),
41
			'email'       => sanitize_email( str_replace( ' ', '+', $_GET['email'] ) ),
42
			'download_id' => wc_clean( isset( $_GET['key'] ) ? preg_replace( '/\s+/', ' ', $_GET['key'] ) : '' )
43
		) );
44
45
		if ( $_product && $download_data ) {
46
			self::check_current_user_can_download( $download_data );
47
48
			do_action( 'woocommerce_download_product', $download_data->user_email, $download_data->order_key, $download_data->product_id, $download_data->user_id, $download_data->download_id, $download_data->order_id );
49
50
			self::count_download( $download_data );
51
			self::download( $_product->get_file_download_path( $download_data->download_id ), $download_data->product_id );
52
		} else {
53
			self::download_error( __( 'Invalid download link.', 'woocommerce' ) );
54
		}
55
	}
56
57
	/**
58
	 * Get a download from the database.
59
	 *
60
	 * @param  array  $args Contains email, order key, product id and download id
61
	 * @return object
62
	 * @access private
63
	 */
64
	private static function get_download_data( $args = array() ) {
65
		global $wpdb;
66
67
		$query = "SELECT * FROM " . $wpdb->prefix . "woocommerce_downloadable_product_permissions ";
68
		$query .= "WHERE user_email = %s ";
69
		$query .= "AND order_key = %s ";
70
		$query .= "AND product_id = %s ";
71
72
		if ( $args['download_id'] ) {
73
			$query .= "AND download_id = %s ";
74
		}
75
76
		$query .= "ORDER BY downloads_remaining DESC";
77
78
		return $wpdb->get_row( $wpdb->prepare( $query, array( $args['email'], $args['order_key'], $args['product_id'], $args['download_id'] ) ) );
79
	}
80
81
	/**
82
	 * Perform checks to see if the current user can download the file.
83
	 * @param  object $download_data
84
	 * @access private
85
	 */
86
	private static function check_current_user_can_download( $download_data ) {
87
		self::check_order_is_valid( $download_data );
0 ignored issues
show
$download_data is of type object, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
88
		self::check_downloads_remaining( $download_data );
0 ignored issues
show
$download_data is of type object, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
89
		self::check_download_expiry( $download_data );
0 ignored issues
show
$download_data is of type object, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
90
		self::check_download_login_required( $download_data );
0 ignored issues
show
$download_data is of type object, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
91
	}
92
93
	/**
94
	 * Check if an order is valid for downloading from.
95
	 * @param  array $download_data
96
	 * @access private
97
	 */
98
	private static function check_order_is_valid( $download_data ) {
99
		if ( $download_data->order_id && ( $order = wc_get_order( $download_data->order_id ) ) && ! $order->is_download_permitted() ) {
100
			self::download_error( __( 'Invalid order.', 'woocommerce' ), '', 403 );
101
		}
102
	}
103
104
	/**
105
	 * Check if there are downloads remaining.
106
	 * @param  array $download_data
107
	 * @access private
108
	 */
109
	private static function check_downloads_remaining( $download_data ) {
110
		if ( '0' == $download_data->downloads_remaining  ) {
111
			self::download_error( __( 'Sorry, you have reached your download limit for this file', 'woocommerce' ), '', 403 );
112
		}
113
	}
114
115
	/**
116
	 * Check if the download has expired.
117
	 * @param  array $download_data
118
	 * @access private
119
	 */
120
	private static function check_download_expiry( $download_data ) {
121
		if ( $download_data->access_expires > 0 && strtotime( $download_data->access_expires ) < strtotime( 'midnight', current_time( 'timestamp' ) ) ) {
122
			self::download_error( __( 'Sorry, this download has expired', 'woocommerce' ), '', 403 );
123
		}
124
	}
125
126
	/**
127
	 * Check if a download requires the user to login first.
128
	 * @param  array $download_data
129
	 * @access private
130
	 */
131
	private static function check_download_login_required( $download_data ) {
132
		if ( $download_data->user_id && 'yes' === get_option( 'woocommerce_downloads_require_login' ) ) {
133
			if ( ! is_user_logged_in() ) {
134
				if ( wc_get_page_id( 'myaccount' ) ) {
135
					wp_safe_redirect( add_query_arg( 'wc_error', urlencode( __( 'You must be logged in to download files.', 'woocommerce' ) ), wc_get_page_permalink( 'myaccount' ) ) );
136
					exit;
137
				} else {
138
					self::download_error( __( 'You must be logged in to download files.', 'woocommerce' ) . ' <a href="' . esc_url( wp_login_url( wc_get_page_permalink( 'myaccount' ) ) ) . '" class="wc-forward">' . __( 'Login', 'woocommerce' ) . '</a>', __( 'Log in to Download Files', 'woocommerce' ), 403 );
139
				}
140
			} elseif ( ! current_user_can( 'download_file', $download_data ) ) {
141
				self::download_error( __( 'This is not your download link.', 'woocommerce' ), '', 403 );
142
			}
143
		}
144
	}
145
146
	/**
147
	 * Log the download + increase counts.
148
	 * @param object $download_data
149
	 */
150
	public static function count_download( $download_data ) {
151
		global $wpdb;
152
153
		$wpdb->update(
154
			$wpdb->prefix . 'woocommerce_downloadable_product_permissions',
155
			array(
156
				'download_count'      => $download_data->download_count + 1,
157
				'downloads_remaining' => $download_data->downloads_remaining > 0 ? $download_data->downloads_remaining - 1 : $download_data->downloads_remaining,
158
			),
159
			array(
160
				'permission_id' => absint( $download_data->permission_id ),
161
			),
162
			array( '%d', '%s' ),
163
			array( '%d' )
164
		);
165
	}
166
167
	/**
168
	 * Download a file - hook into init function.
169
	 * @param string $file_path URL to file
170
	 * @param integer $product_id of the product being downloaded
171
	 */
172
	public static function download( $file_path, $product_id ) {
173
		if ( ! $file_path ) {
174
			self::download_error( __( 'No file defined', 'woocommerce' ) );
175
		}
176
177
		$filename = basename( $file_path );
178
179
		if ( strstr( $filename, '?' ) ) {
180
			$filename = current( explode( '?', $filename ) );
181
		}
182
183
		$filename             = apply_filters( 'woocommerce_file_download_filename', $filename, $product_id );
184
		$file_download_method = apply_filters( 'woocommerce_file_download_method', get_option( 'woocommerce_file_download_method', 'force' ), $product_id );
185
186
		// Add action to prevent issues in IE
187
		add_action( 'nocache_headers', array( __CLASS__, 'ie_nocache_headers_fix' ) );
188
189
		// Trigger download via one of the methods
190
		do_action( 'woocommerce_download_file_' . $file_download_method, $file_path, $filename );
191
	}
192
193
	/**
194
	 * Redirect to a file to start the download.
195
	 * @param  string $file_path
196
	 * @param  string $filename
197
	 */
198
	public static function download_file_redirect( $file_path, $filename = '' ) {
0 ignored issues
show
The parameter $filename is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
199
		header( 'Location: ' . $file_path );
200
		exit;
201
	}
202
203
	/**
204
	 * Parse file path and see if its remote or local.
205
	 * @param  string $file_path
206
	 * @return array
207
	 */
208
	public static function parse_file_path( $file_path ) {
209
		$wp_uploads     = wp_upload_dir();
210
		$wp_uploads_dir = $wp_uploads['basedir'];
211
		$wp_uploads_url = $wp_uploads['baseurl'];
212
213
		// Replace uploads dir, site url etc with absolute counterparts if we can
214
		$replacements = array(
215
			$wp_uploads_url                  => $wp_uploads_dir,
216
			network_site_url( '/', 'https' ) => ABSPATH,
217
			network_site_url( '/', 'http' )  => ABSPATH,
218
			site_url( '/', 'https' )         => ABSPATH,
219
			site_url( '/', 'http' )          => ABSPATH
220
		);
221
222
		$file_path        = str_replace( array_keys( $replacements ), array_values( $replacements ), $file_path );
223
		$parsed_file_path = parse_url( $file_path );
224
		$remote_file      = true;
225
226
		// See if path needs an abspath prepended to work
227
		if ( file_exists( ABSPATH . $file_path ) ) {
228
			$remote_file = false;
229
			$file_path   = ABSPATH . $file_path;
230
231
		// Check if we have an absolute path
232
		} elseif ( ( ! isset( $parsed_file_path['scheme'] ) || ! in_array( $parsed_file_path['scheme'], array( 'http', 'https', 'ftp' ) ) ) && isset( $parsed_file_path['path'] ) && file_exists( $parsed_file_path['path'] ) ) {
233
			$remote_file = false;
234
			$file_path   = $parsed_file_path['path'];
235
		}
236
237
		return array(
238
			'remote_file' => $remote_file,
239
			'file_path'   => $file_path
240
		);
241
	}
242
243
	/**
244
	 * Download a file using X-Sendfile, X-Lighttpd-Sendfile, or X-Accel-Redirect if available.
245
	 * @param  string $file_path
246
	 * @param  string $filename
247
	 */
248
	public static function download_file_xsendfile( $file_path, $filename ) {
249
		$parsed_file_path = self::parse_file_path( $file_path );
250
251
		if ( function_exists( 'apache_get_modules' ) && in_array( 'mod_xsendfile', apache_get_modules() ) ) {
252
			self::download_headers( $parsed_file_path['file_path'], $filename );
253
			header( "X-Sendfile: " . $parsed_file_path['file_path'] );
254
			exit;
255
		} elseif ( stristr( getenv( 'SERVER_SOFTWARE' ), 'lighttpd' ) ) {
256
			self::download_headers( $parsed_file_path['file_path'], $filename );
257
			header( "X-Lighttpd-Sendfile: " . $parsed_file_path['file_path'] );
258
			exit;
259
		} elseif ( stristr( getenv( 'SERVER_SOFTWARE' ), 'nginx' ) || stristr( getenv( 'SERVER_SOFTWARE' ), 'cherokee' ) ) {
260
			self::download_headers( $parsed_file_path['file_path'], $filename );
261
			$xsendfile_path = trim( preg_replace( '`^' . str_replace( '\\', '/', getcwd() ) . '`', '', $parsed_file_path['file_path'] ), '/' );
262
			header( "X-Accel-Redirect: /$xsendfile_path" );
263
			exit;
264
		}
265
266
		// Fallback
267
		self::download_file_force( $file_path, $filename );
268
	}
269
270
	/**
271
	 * Force download - this is the default method.
272
	 * @param  string $file_path
273
	 * @param  string $filename
274
	 */
275
	public static function download_file_force( $file_path, $filename ) {
276
		$parsed_file_path = self::parse_file_path( $file_path );
277
278
		self::download_headers( $parsed_file_path['file_path'], $filename );
279
280
		if ( ! self::readfile_chunked( $parsed_file_path['file_path'] ) ) {
281
			if ( $parsed_file_path['remote_file'] ) {
282
				self::download_file_redirect( $file_path );
283
			} else {
284
				self::download_error( __( 'File not found', 'woocommerce' ) );
285
			}
286
		}
287
288
		exit;
289
	}
290
291
	/**
292
	 * Get content type of a download.
293
	 * @param  string $file_path
294
	 * @return string
295
	 * @access private
296
	 */
297
	private static function get_download_content_type( $file_path ) {
298
		$file_extension  = strtolower( substr( strrchr( $file_path, "." ), 1 ) );
299
		$ctype           = "application/force-download";
300
301
		foreach ( get_allowed_mime_types() as $mime => $type ) {
302
			$mimes = explode( '|', $mime );
303
			if ( in_array( $file_extension, $mimes ) ) {
304
				$ctype = $type;
305
				break;
306
			}
307
		}
308
309
		return $ctype;
310
	}
311
312
	/**
313
	 * Set headers for the download.
314
	 * @param  string $file_path
315
	 * @param  string $filename
316
	 * @access private
317
	 */
318
	private static function download_headers( $file_path, $filename ) {
319
		self::check_server_config();
320
		self::clean_buffers();
321
		nocache_headers();
322
323
		header( "X-Robots-Tag: noindex, nofollow", true );
324
		header( "Content-Type: " . self::get_download_content_type( $file_path ) );
325
		header( "Content-Description: File Transfer" );
326
		header( "Content-Disposition: attachment; filename=\"" . $filename . "\";" );
327
		header( "Content-Transfer-Encoding: binary" );
328
329
        if ( $size = @filesize( $file_path ) ) {
330
        	header( "Content-Length: " . $size );
331
        }
332
	}
333
334
	/**
335
	 * Check and set certain server config variables to ensure downloads work as intended.
336
	 */
337
	private static function check_server_config() {
338
		wc_set_time_limit( 0 );
339
		if ( function_exists( 'get_magic_quotes_runtime' ) && get_magic_quotes_runtime() && version_compare( phpversion(), '5.4', '<' ) ) {
340
			set_magic_quotes_runtime( 0 );
341
		}
342
		if ( function_exists( 'apache_setenv' ) ) {
343
			@apache_setenv( 'no-gzip', 1 );
344
		}
345
		@ini_set( 'zlib.output_compression', 'Off' );
346
		@session_write_close();
347
	}
348
349
	/**
350
	 * Clean all output buffers.
351
	 *
352
	 * Can prevent errors, for example: transfer closed with 3 bytes remaining to read.
353
	 *
354
	 * @access private
355
	 */
356
	private static function clean_buffers() {
357
		if ( ob_get_level() ) {
358
			$levels = ob_get_level();
359
			for ( $i = 0; $i < $levels; $i++ ) {
360
				@ob_end_clean();
361
			}
362
		} else {
363
			@ob_end_clean();
364
		}
365
	}
366
367
	/**
368
	 * readfile_chunked.
369
	 *
370
	 * Reads file in chunks so big downloads are possible without changing PHP.INI - http://codeigniter.com/wiki/Download_helper_for_large_files/.
371
	 *
372
	 * @param   string $file
373
	 * @return 	bool Success or fail
374
	 */
375
	public static function readfile_chunked( $file ) {
376
		$chunksize = 1024 * 1024;
377
		$handle    = @fopen( $file, 'r' );
378
379
		if ( false === $handle ) {
380
			return false;
381
		}
382
383
		while ( ! @feof( $handle ) ) {
384
			echo @fread( $handle, $chunksize );
385
386
			if ( ob_get_length() ) {
387
				ob_flush();
388
				flush();
389
			}
390
		}
391
392
		return @fclose( $handle );
393
	}
394
395
	/**
396
	 * Filter headers for IE to fix issues over SSL.
397
	 *
398
	 * IE bug prevents download via SSL when Cache Control and Pragma no-cache headers set.
399
	 *
400
	 * @param array $headers
401
	 * @return array
402
	 */
403
	public static function ie_nocache_headers_fix( $headers ) {
404
		if ( is_ssl() && ! empty( $GLOBALS['is_IE'] ) ) {
405
			$headers['Cache-Control'] = 'private';
406
			unset( $headers['Pragma'] );
407
		}
408
		return $headers;
409
	}
410
411
	/**
412
	 * Die with an error message if the download fails.
413
	 * @param  string $message
414
	 * @param  string  $title
415
	 * @param  integer $status
416
	 * @access private
417
	 */
418
	private static function download_error( $message, $title = '', $status = 404 ) {
419
		if ( ! strstr( $message, '<a ' ) ) {
420
			$message .= ' <a href="' . esc_url( wc_get_page_permalink( 'shop' ) ) . '" class="wc-forward">' . __( 'Go to shop', 'woocommerce' ) . '</a>';
421
		}
422
		wp_die( $message, $title, array( 'response' => $status ) );
423
	}
424
}
425
426
WC_Download_Handler::init();
427