Issues (4122)

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/libs/MultiHttpClient.php (1 issue)

Severity

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
 * HTTP service client
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 */
22
23
/**
24
 * Class to handle concurrent HTTP requests
25
 *
26
 * HTTP request maps are arrays that use the following format:
27
 *   - method   : GET/HEAD/PUT/POST/DELETE
28
 *   - url      : HTTP/HTTPS URL
29
 *   - query    : <query parameter field/value associative array> (uses RFC 3986)
30
 *   - headers  : <header name/value associative array>
31
 *   - body     : source to get the HTTP request body from;
32
 *                this can simply be a string (always), a resource for
33
 *                PUT requests, and a field/value array for POST request;
34
 *                array bodies are encoded as multipart/form-data and strings
35
 *                use application/x-www-form-urlencoded (headers sent automatically)
36
 *   - stream   : resource to stream the HTTP response body to
37
 *   - proxy    : HTTP proxy to use
38
 *   - flags    : map of boolean flags which supports:
39
 *                  - relayResponseHeaders : write out header via header()
40
 * Request maps can use integer index 0 instead of 'method' and 1 instead of 'url'.
41
 *
42
 * @author Aaron Schulz
43
 * @since 1.23
44
 */
45
class MultiHttpClient {
46
	/** @var resource */
47
	protected $multiHandle = null; // curl_multi handle
48
	/** @var string|null SSL certificates path  */
49
	protected $caBundlePath;
50
	/** @var integer */
51
	protected $connTimeout = 10;
52
	/** @var integer */
53
	protected $reqTimeout = 300;
54
	/** @var bool */
55
	protected $usePipelining = false;
56
	/** @var integer */
57
	protected $maxConnsPerHost = 50;
58
	/** @var string|null proxy */
59
	protected $proxy;
60
	/** @var string */
61
	protected $userAgent = 'wikimedia/multi-http-client v1.0';
62
63
	/**
64
	 * @param array $options
65
	 *   - connTimeout     : default connection timeout (seconds)
66
	 *   - reqTimeout      : default request timeout (seconds)
67
	 *   - proxy           : HTTP proxy to use
68
	 *   - usePipelining   : whether to use HTTP pipelining if possible (for all hosts)
69
	 *   - maxConnsPerHost : maximum number of concurrent connections (per host)
70
	 *   - userAgent       : The User-Agent header value to send
71
	 * @throws Exception
72
	 */
73
	public function __construct( array $options ) {
74
		if ( isset( $options['caBundlePath'] ) ) {
75
			$this->caBundlePath = $options['caBundlePath'];
76
			if ( !file_exists( $this->caBundlePath ) ) {
77
				throw new Exception( "Cannot find CA bundle: " . $this->caBundlePath );
78
			}
79
		}
80
		static $opts = [
81
			'connTimeout', 'reqTimeout', 'usePipelining', 'maxConnsPerHost', 'proxy', 'userAgent'
82
		];
83
		foreach ( $opts as $key ) {
84
			if ( isset( $options[$key] ) ) {
85
				$this->$key = $options[$key];
86
			}
87
		}
88
	}
89
90
	/**
91
	 * Execute an HTTP(S) request
92
	 *
93
	 * This method returns a response map of:
94
	 *   - code    : HTTP response code or 0 if there was a serious cURL error
95
	 *   - reason  : HTTP response reason (empty if there was a serious cURL error)
96
	 *   - headers : <header name/value associative array>
97
	 *   - body    : HTTP response body or resource (if "stream" was set)
98
	 *   - error     : Any cURL error string
99
	 * The map also stores integer-indexed copies of these values. This lets callers do:
100
	 * @code
101
	 *		list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $http->run( $req );
102
	 * @endcode
103
	 * @param array $req HTTP request array
104
	 * @param array $opts
105
	 *   - connTimeout    : connection timeout per request (seconds)
106
	 *   - reqTimeout     : post-connection timeout per request (seconds)
107
	 * @return array Response array for request
108
	 */
109
	public function run( array $req, array $opts = [] ) {
110
		return $this->runMulti( [ $req ], $opts )[0]['response'];
111
	}
112
113
	/**
114
	 * Execute a set of HTTP(S) requests concurrently
115
	 *
116
	 * The maps are returned by this method with the 'response' field set to a map of:
117
	 *   - code    : HTTP response code or 0 if there was a serious cURL error
118
	 *   - reason  : HTTP response reason (empty if there was a serious cURL error)
119
	 *   - headers : <header name/value associative array>
120
	 *   - body    : HTTP response body or resource (if "stream" was set)
121
	 *   - error   : Any cURL error string
122
	 * The map also stores integer-indexed copies of these values. This lets callers do:
123
	 * @code
124
	 *        list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $req['response'];
125
	 * @endcode
126
	 * All headers in the 'headers' field are normalized to use lower case names.
127
	 * This is true for the request headers and the response headers. Integer-indexed
128
	 * method/URL entries will also be changed to use the corresponding string keys.
129
	 *
130
	 * @param array $reqs Map of HTTP request arrays
131
	 * @param array $opts
132
	 *   - connTimeout     : connection timeout per request (seconds)
133
	 *   - reqTimeout      : post-connection timeout per request (seconds)
134
	 *   - usePipelining   : whether to use HTTP pipelining if possible
135
	 *   - maxConnsPerHost : maximum number of concurrent connections (per host)
136
	 * @return array $reqs With response array populated for each
137
	 * @throws Exception
138
	 */
139
	public function runMulti( array $reqs, array $opts = [] ) {
140
		$chm = $this->getCurlMulti();
141
142
		// Normalize $reqs and add all of the required cURL handles...
143
		$handles = [];
144
		foreach ( $reqs as $index => &$req ) {
145
			$req['response'] = [
146
				'code'     => 0,
147
				'reason'   => '',
148
				'headers'  => [],
149
				'body'     => '',
150
				'error'    => ''
151
			];
152
			if ( isset( $req[0] ) ) {
153
				$req['method'] = $req[0]; // short-form
154
				unset( $req[0] );
155
			}
156
			if ( isset( $req[1] ) ) {
157
				$req['url'] = $req[1]; // short-form
158
				unset( $req[1] );
159
			}
160
			if ( !isset( $req['method'] ) ) {
161
				throw new Exception( "Request has no 'method' field set." );
162
			} elseif ( !isset( $req['url'] ) ) {
163
				throw new Exception( "Request has no 'url' field set." );
164
			}
165
			$req['query'] = isset( $req['query'] ) ? $req['query'] : [];
166
			$headers = []; // normalized headers
167
			if ( isset( $req['headers'] ) ) {
168
				foreach ( $req['headers'] as $name => $value ) {
169
					$headers[strtolower( $name )] = $value;
170
				}
171
			}
172
			$req['headers'] = $headers;
173
			if ( !isset( $req['body'] ) ) {
174
				$req['body'] = '';
175
				$req['headers']['content-length'] = 0;
176
			}
177
			$req['flags'] = isset( $req['flags'] ) ? $req['flags'] : [];
178
			$handles[$index] = $this->getCurlHandle( $req, $opts );
179
			if ( count( $reqs ) > 1 ) {
180
				// https://github.com/guzzle/guzzle/issues/349
181
				curl_setopt( $handles[$index], CURLOPT_FORBID_REUSE, true );
182
			}
183
		}
184
		unset( $req ); // don't assign over this by accident
185
186
		$indexes = array_keys( $reqs );
187
		if ( isset( $opts['usePipelining'] ) ) {
188
			curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$opts['usePipelining'] );
189
		}
190
		if ( isset( $opts['maxConnsPerHost'] ) ) {
191
			// Keep these sockets around as they may be needed later in the request
192
			curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$opts['maxConnsPerHost'] );
193
		}
194
195
		// @TODO: use a per-host rolling handle window (e.g. CURLMOPT_MAX_HOST_CONNECTIONS)
196
		$batches = array_chunk( $indexes, $this->maxConnsPerHost );
197
		$infos = [];
198
199
		foreach ( $batches as $batch ) {
200
			// Attach all cURL handles for this batch
201
			foreach ( $batch as $index ) {
202
				curl_multi_add_handle( $chm, $handles[$index] );
203
			}
204
			// Execute the cURL handles concurrently...
205
			$active = null; // handles still being processed
206
			do {
207
				// Do any available work...
208
				do {
209
					$mrc = curl_multi_exec( $chm, $active );
210
					$info = curl_multi_info_read( $chm );
211
					if ( $info !== false ) {
212
						$infos[(int)$info['handle']] = $info;
213
					}
214
				} while ( $mrc == CURLM_CALL_MULTI_PERFORM );
215
				// Wait (if possible) for available work...
216
				if ( $active > 0 && $mrc == CURLM_OK ) {
217
					if ( curl_multi_select( $chm, 10 ) == -1 ) {
218
						// PHP bug 63411; https://curl.haxx.se/libcurl/c/curl_multi_fdset.html
219
						usleep( 5000 ); // 5ms
220
					}
221
				}
222
			} while ( $active > 0 && $mrc == CURLM_OK );
223
		}
224
225
		// Remove all of the added cURL handles and check for errors...
226
		foreach ( $reqs as $index => &$req ) {
227
			$ch = $handles[$index];
228
			curl_multi_remove_handle( $chm, $ch );
229
230
			if ( isset( $infos[(int)$ch] ) ) {
231
				$info = $infos[(int)$ch];
232
				$errno = $info['result'];
233
				if ( $errno !== 0 ) {
234
					$req['response']['error'] = "(curl error: $errno)";
235
					if ( function_exists( 'curl_strerror' ) ) {
236
						$req['response']['error'] .= " " . curl_strerror( $errno );
237
					}
238
				}
239
			} else {
240
				$req['response']['error'] = "(curl error: no status set)";
241
			}
242
243
			// For convenience with the list() operator
244
			$req['response'][0] = $req['response']['code'];
245
			$req['response'][1] = $req['response']['reason'];
246
			$req['response'][2] = $req['response']['headers'];
247
			$req['response'][3] = $req['response']['body'];
248
			$req['response'][4] = $req['response']['error'];
249
			curl_close( $ch );
250
			// Close any string wrapper file handles
251
			if ( isset( $req['_closeHandle'] ) ) {
252
				fclose( $req['_closeHandle'] );
253
				unset( $req['_closeHandle'] );
254
			}
255
		}
256
		unset( $req ); // don't assign over this by accident
257
258
		// Restore the default settings
259
		curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$this->usePipelining );
260
		curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost );
261
262
		return $reqs;
263
	}
264
265
	/**
266
	 * @param array $req HTTP request map
267
	 * @param array $opts
268
	 *   - connTimeout    : default connection timeout
269
	 *   - reqTimeout     : default request timeout
270
	 * @return resource
271
	 * @throws Exception
272
	 */
273
	protected function getCurlHandle( array &$req, array $opts = [] ) {
274
		$ch = curl_init();
275
276
		curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT,
277
			isset( $opts['connTimeout'] ) ? $opts['connTimeout'] : $this->connTimeout );
278
		curl_setopt( $ch, CURLOPT_PROXY, isset( $req['proxy'] ) ? $req['proxy'] : $this->proxy );
279
		curl_setopt( $ch, CURLOPT_TIMEOUT,
280
			isset( $opts['reqTimeout'] ) ? $opts['reqTimeout'] : $this->reqTimeout );
281
		curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 );
282
		curl_setopt( $ch, CURLOPT_MAXREDIRS, 4 );
283
		curl_setopt( $ch, CURLOPT_HEADER, 0 );
284
		if ( !is_null( $this->caBundlePath ) ) {
285
			curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true );
286
			curl_setopt( $ch, CURLOPT_CAINFO, $this->caBundlePath );
287
		}
288
		curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
289
290
		$url = $req['url'];
291
		$query = http_build_query( $req['query'], '', '&', PHP_QUERY_RFC3986 );
292
		if ( $query != '' ) {
293
			$url .= strpos( $req['url'], '?' ) === false ? "?$query" : "&$query";
294
		}
295
		curl_setopt( $ch, CURLOPT_URL, $url );
296
297
		curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, $req['method'] );
298
		if ( $req['method'] === 'HEAD' ) {
299
			curl_setopt( $ch, CURLOPT_NOBODY, 1 );
300
		}
301
302
		if ( $req['method'] === 'PUT' ) {
303
			curl_setopt( $ch, CURLOPT_PUT, 1 );
304
			if ( is_resource( $req['body'] ) ) {
305
				curl_setopt( $ch, CURLOPT_INFILE, $req['body'] );
306
				if ( isset( $req['headers']['content-length'] ) ) {
307
					curl_setopt( $ch, CURLOPT_INFILESIZE, $req['headers']['content-length'] );
308
				} elseif ( isset( $req['headers']['transfer-encoding'] ) &&
309
					$req['headers']['transfer-encoding'] === 'chunks'
310
				) {
311
					curl_setopt( $ch, CURLOPT_UPLOAD, true );
312
				} else {
313
					throw new Exception( "Missing 'Content-Length' or 'Transfer-Encoding' header." );
314
				}
315
			} elseif ( $req['body'] !== '' ) {
316
				$fp = fopen( "php://temp", "wb+" );
317
				fwrite( $fp, $req['body'], strlen( $req['body'] ) );
318
				rewind( $fp );
319
				curl_setopt( $ch, CURLOPT_INFILE, $fp );
320
				curl_setopt( $ch, CURLOPT_INFILESIZE, strlen( $req['body'] ) );
321
				$req['_closeHandle'] = $fp; // remember to close this later
322
			} else {
323
				curl_setopt( $ch, CURLOPT_INFILESIZE, 0 );
324
			}
325
			curl_setopt( $ch, CURLOPT_READFUNCTION,
326
				function ( $ch, $fd, $length ) {
327
					$data = fread( $fd, $length );
328
					$len = strlen( $data );
0 ignored issues
show
$len is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
329
					return $data;
330
				}
331
			);
332
		} elseif ( $req['method'] === 'POST' ) {
333
			curl_setopt( $ch, CURLOPT_POST, 1 );
334
			// Don't interpret POST parameters starting with '@' as file uploads, because this
335
			// makes it impossible to POST plain values starting with '@' (and causes security
336
			// issues potentially exposing the contents of local files).
337
			// The PHP manual says this option was introduced in PHP 5.5 defaults to true in PHP 5.6,
338
			// but we support lower versions, and the option doesn't exist in HHVM 5.6.99.
339
			if ( defined( 'CURLOPT_SAFE_UPLOAD' ) ) {
340
				curl_setopt( $ch, CURLOPT_SAFE_UPLOAD, true );
341
			} elseif ( is_array( $req['body'] ) ) {
342
				// In PHP 5.2 and later, '@' is interpreted as a file upload if POSTFIELDS
343
				// is an array, but not if it's a string. So convert $req['body'] to a string
344
				// for safety.
345
				$req['body'] = http_build_query( $req['body'] );
346
			}
347
			curl_setopt( $ch, CURLOPT_POSTFIELDS, $req['body'] );
348
		} else {
349
			if ( is_resource( $req['body'] ) || $req['body'] !== '' ) {
350
				throw new Exception( "HTTP body specified for a non PUT/POST request." );
351
			}
352
			$req['headers']['content-length'] = 0;
353
		}
354
355
		if ( !isset( $req['headers']['user-agent'] ) ) {
356
			$req['headers']['user-agent'] = $this->userAgent;
357
		}
358
359
		$headers = [];
360
		foreach ( $req['headers'] as $name => $value ) {
361
			if ( strpos( $name, ': ' ) ) {
362
				throw new Exception( "Headers cannot have ':' in the name." );
363
			}
364
			$headers[] = $name . ': ' . trim( $value );
365
		}
366
		curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
367
368
		curl_setopt( $ch, CURLOPT_HEADERFUNCTION,
369
			function ( $ch, $header ) use ( &$req ) {
370
				if ( !empty( $req['flags']['relayResponseHeaders'] ) ) {
371
					header( $header );
372
				}
373
				$length = strlen( $header );
374
				$matches = [];
375
				if ( preg_match( "/^(HTTP\/1\.[01]) (\d{3}) (.*)/", $header, $matches ) ) {
376
					$req['response']['code'] = (int)$matches[2];
377
					$req['response']['reason'] = trim( $matches[3] );
378
					return $length;
379
				}
380
				if ( strpos( $header, ":" ) === false ) {
381
					return $length;
382
				}
383
				list( $name, $value ) = explode( ":", $header, 2 );
384
				$req['response']['headers'][strtolower( $name )] = trim( $value );
385
				return $length;
386
			}
387
		);
388
389
		if ( isset( $req['stream'] ) ) {
390
			// Don't just use CURLOPT_FILE as that might give:
391
			// curl_setopt(): cannot represent a stream of type Output as a STDIO FILE*
392
			// The callback here handles both normal files and php://temp handles.
393
			curl_setopt( $ch, CURLOPT_WRITEFUNCTION,
394
				function ( $ch, $data ) use ( &$req ) {
395
					return fwrite( $req['stream'], $data );
396
				}
397
			);
398
		} else {
399
			curl_setopt( $ch, CURLOPT_WRITEFUNCTION,
400
				function ( $ch, $data ) use ( &$req ) {
401
					$req['response']['body'] .= $data;
402
					return strlen( $data );
403
				}
404
			);
405
		}
406
407
		return $ch;
408
	}
409
410
	/**
411
	 * @return resource
412
	 */
413
	protected function getCurlMulti() {
414
		if ( !$this->multiHandle ) {
415
			$cmh = curl_multi_init();
416
			curl_multi_setopt( $cmh, CURLMOPT_PIPELINING, (int)$this->usePipelining );
417
			curl_multi_setopt( $cmh, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost );
418
			$this->multiHandle = $cmh;
419
		}
420
		return $this->multiHandle;
421
	}
422
423
	function __destruct() {
424
		if ( $this->multiHandle ) {
425
			curl_multi_close( $this->multiHandle );
426
		}
427
	}
428
}
429