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/http/MWHttpRequest.php (7 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
 * This program is free software; you can redistribute it and/or modify
4
 * it under the terms of the GNU General Public License as published by
5
 * the Free Software Foundation; either version 2 of the License, or
6
 * (at your option) any later version.
7
 *
8
 * This program is distributed in the hope that it will be useful,
9
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
 * GNU General Public License for more details.
12
 *
13
 * You should have received a copy of the GNU General Public License along
14
 * with this program; if not, write to the Free Software Foundation, Inc.,
15
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16
 * http://www.gnu.org/copyleft/gpl.html
17
 *
18
 * @file
19
 */
20
21
use MediaWiki\Logger\LoggerFactory;
22
use Psr\Log\LoggerInterface;
23
use Psr\Log\LoggerAwareInterface;
24
use Psr\Log\NullLogger;
25
26
/**
27
 * This wrapper class will call out to curl (if available) or fallback
28
 * to regular PHP if necessary for handling internal HTTP requests.
29
 *
30
 * Renamed from HttpRequest to MWHttpRequest to avoid conflict with
31
 * PHP's HTTP extension.
32
 */
33
class MWHttpRequest implements LoggerAwareInterface {
34
	const SUPPORTS_FILE_POSTS = false;
35
36
	protected $content;
37
	protected $timeout = 'default';
38
	protected $headersOnly = null;
39
	protected $postData = null;
40
	protected $proxy = null;
41
	protected $noProxy = false;
42
	protected $sslVerifyHost = true;
43
	protected $sslVerifyCert = true;
44
	protected $caInfo = null;
45
	protected $method = "GET";
46
	protected $reqHeaders = [];
47
	protected $url;
48
	protected $parsedUrl;
49
	protected $callback;
50
	protected $maxRedirects = 5;
51
	protected $followRedirects = false;
52
53
	/**
54
	 * @var CookieJar
55
	 */
56
	protected $cookieJar;
57
58
	protected $headerList = [];
59
	protected $respVersion = "0.9";
60
	protected $respStatus = "200 Ok";
61
	protected $respHeaders = [];
62
63
	public $status;
64
65
	/**
66
	 * @var Profiler
67
	 */
68
	protected $profiler;
69
70
	/**
71
	 * @var string
72
	 */
73
	protected $profileName;
74
75
	/**
76
	 * @var LoggerInterface;
77
	 */
78
	protected $logger;
79
80
	/**
81
	 * @param string $url Url to use. If protocol-relative, will be expanded to an http:// URL
82
	 * @param array $options (optional) extra params to pass (see Http::request())
83
	 * @param string $caller The method making this request, for profiling
84
	 * @param Profiler $profiler An instance of the profiler for profiling, or null
85
	 */
86
	protected function __construct(
87
		$url, $options = [], $caller = __METHOD__, $profiler = null
88
	) {
89
		global $wgHTTPTimeout, $wgHTTPConnectTimeout;
90
91
		$this->url = wfExpandUrl( $url, PROTO_HTTP );
92
		$this->parsedUrl = wfParseUrl( $this->url );
0 ignored issues
show
It seems like $this->url can also be of type false; however, wfParseUrl() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
93
94
		if ( isset( $options['logger'] ) ) {
95
			$this->logger = $options['logger'];
96
		} else {
97
			$this->logger = new NullLogger();
98
		}
99
100
		if ( !$this->parsedUrl || !Http::isValidURI( $this->url ) ) {
0 ignored issues
show
It seems like $this->url can also be of type false; however, Http::isValidURI() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
101
			$this->status = Status::newFatal( 'http-invalid-url', $url );
102
		} else {
103
			$this->status = Status::newGood( 100 ); // continue
104
		}
105
106 View Code Duplication
		if ( isset( $options['timeout'] ) && $options['timeout'] != 'default' ) {
107
			$this->timeout = $options['timeout'];
108
		} else {
109
			$this->timeout = $wgHTTPTimeout;
110
		}
111 View Code Duplication
		if ( isset( $options['connectTimeout'] ) && $options['connectTimeout'] != 'default' ) {
112
			$this->connectTimeout = $options['connectTimeout'];
0 ignored issues
show
The property connectTimeout does not seem to exist. Did you mean timeout?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
113
		} else {
114
			$this->connectTimeout = $wgHTTPConnectTimeout;
0 ignored issues
show
The property connectTimeout does not seem to exist. Did you mean timeout?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
115
		}
116
		if ( isset( $options['userAgent'] ) ) {
117
			$this->setUserAgent( $options['userAgent'] );
118
		}
119
120
		$members = [ "postData", "proxy", "noProxy", "sslVerifyHost", "caInfo",
121
				"method", "followRedirects", "maxRedirects", "sslVerifyCert", "callback" ];
122
123
		foreach ( $members as $o ) {
124
			if ( isset( $options[$o] ) ) {
125
				// ensure that MWHttpRequest::method is always
126
				// uppercased. Bug 36137
127
				if ( $o == 'method' ) {
128
					$options[$o] = strtoupper( $options[$o] );
129
				}
130
				$this->$o = $options[$o];
131
			}
132
		}
133
134
		if ( $this->noProxy ) {
135
			$this->proxy = ''; // noProxy takes precedence
136
		}
137
138
		// Profile based on what's calling us
139
		$this->profiler = $profiler;
140
		$this->profileName = $caller;
141
	}
142
143
	/**
144
	 * @param LoggerInterface $logger
145
	 */
146
	public function setLogger( LoggerInterface $logger ) {
147
		$this->logger = $logger;
148
	}
149
150
	/**
151
	 * Simple function to test if we can make any sort of requests at all, using
152
	 * cURL or fopen()
153
	 * @return bool
154
	 */
155
	public static function canMakeRequests() {
156
		return function_exists( 'curl_init' ) || wfIniGetBool( 'allow_url_fopen' );
157
	}
158
159
	/**
160
	 * Generate a new request object
161
	 * @param string $url Url to use
162
	 * @param array $options (optional) extra params to pass (see Http::request())
163
	 * @param string $caller The method making this request, for profiling
164
	 * @throws MWException
165
	 * @return CurlHttpRequest|PhpHttpRequest
166
	 * @see MWHttpRequest::__construct
167
	 */
168
	public static function factory( $url, $options = null, $caller = __METHOD__ ) {
169
		if ( !Http::$httpEngine ) {
170
			Http::$httpEngine = function_exists( 'curl_init' ) ? 'curl' : 'php';
0 ignored issues
show
Documentation Bug introduced by
The property $httpEngine was declared of type boolean, but function_exists('curl_init') ? 'curl' : 'php' is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
171
		} elseif ( Http::$httpEngine == 'curl' && !function_exists( 'curl_init' ) ) {
172
			throw new MWException( __METHOD__ . ': curl (http://php.net/curl) is not installed, but' .
173
				' Http::$httpEngine is set to "curl"' );
174
		}
175
176
		if ( !is_array( $options ) ) {
177
			$options = [];
178
		}
179
180
		if ( !isset( $options['logger'] ) ) {
181
			$options['logger'] = LoggerFactory::getInstance( 'http' );
182
		}
183
184
		switch ( Http::$httpEngine ) {
185
			case 'curl':
186
				return new CurlHttpRequest( $url, $options, $caller, Profiler::instance() );
187
			case 'php':
188
				if ( !wfIniGetBool( 'allow_url_fopen' ) ) {
189
					throw new MWException( __METHOD__ . ': allow_url_fopen ' .
190
						'needs to be enabled for pure PHP http requests to ' .
191
						'work. If possible, curl should be used instead. See ' .
192
						'http://php.net/curl.'
193
					);
194
				}
195
				return new PhpHttpRequest( $url, $options, $caller, Profiler::instance() );
196
			default:
197
				throw new MWException( __METHOD__ . ': The setting of Http::$httpEngine is not valid.' );
198
		}
199
	}
200
201
	/**
202
	 * Get the body, or content, of the response to the request
203
	 *
204
	 * @return string
205
	 */
206
	public function getContent() {
207
		return $this->content;
208
	}
209
210
	/**
211
	 * Set the parameters of the request
212
	 *
213
	 * @param array $args
214
	 * @todo overload the args param
215
	 */
216
	public function setData( $args ) {
217
		$this->postData = $args;
218
	}
219
220
	/**
221
	 * Take care of setting up the proxy (do nothing if "noProxy" is set)
222
	 *
223
	 * @return void
224
	 */
225
	public function proxySetup() {
226
		// If there is an explicit proxy set and proxies are not disabled, then use it
227
		if ( $this->proxy && !$this->noProxy ) {
228
			return;
229
		}
230
231
		// Otherwise, fallback to $wgHTTPProxy if this is not a machine
232
		// local URL and proxies are not disabled
233
		if ( self::isLocalURL( $this->url ) || $this->noProxy ) {
0 ignored issues
show
It seems like $this->url can also be of type false; however, MWHttpRequest::isLocalURL() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
234
			$this->proxy = '';
235
		} else {
236
			$this->proxy = Http::getProxy();
237
		}
238
	}
239
240
	/**
241
	 * Check if the URL can be served by localhost
242
	 *
243
	 * @param string $url Full url to check
244
	 * @return bool
245
	 */
246
	private static function isLocalURL( $url ) {
247
		global $wgCommandLineMode, $wgLocalVirtualHosts;
248
249
		if ( $wgCommandLineMode ) {
250
			return false;
251
		}
252
253
		// Extract host part
254
		$matches = [];
255
		if ( preg_match( '!^https?://([\w.-]+)[/:].*$!', $url, $matches ) ) {
256
			$host = $matches[1];
257
			// Split up dotwise
258
			$domainParts = explode( '.', $host );
259
			// Check if this domain or any superdomain is listed as a local virtual host
260
			$domainParts = array_reverse( $domainParts );
261
262
			$domain = '';
263
			$countParts = count( $domainParts );
264
			for ( $i = 0; $i < $countParts; $i++ ) {
265
				$domainPart = $domainParts[$i];
266
				if ( $i == 0 ) {
267
					$domain = $domainPart;
268
				} else {
269
					$domain = $domainPart . '.' . $domain;
270
				}
271
272
				if ( in_array( $domain, $wgLocalVirtualHosts ) ) {
273
					return true;
274
				}
275
			}
276
		}
277
278
		return false;
279
	}
280
281
	/**
282
	 * Set the user agent
283
	 * @param string $UA
284
	 */
285
	public function setUserAgent( $UA ) {
286
		$this->setHeader( 'User-Agent', $UA );
287
	}
288
289
	/**
290
	 * Set an arbitrary header
291
	 * @param string $name
292
	 * @param string $value
293
	 */
294
	public function setHeader( $name, $value ) {
295
		// I feel like I should normalize the case here...
296
		$this->reqHeaders[$name] = $value;
297
	}
298
299
	/**
300
	 * Get an array of the headers
301
	 * @return array
302
	 */
303
	public function getHeaderList() {
304
		$list = [];
305
306
		if ( $this->cookieJar ) {
307
			$this->reqHeaders['Cookie'] =
308
				$this->cookieJar->serializeToHttpRequest(
309
					$this->parsedUrl['path'],
310
					$this->parsedUrl['host']
311
				);
312
		}
313
314
		foreach ( $this->reqHeaders as $name => $value ) {
315
			$list[] = "$name: $value";
316
		}
317
318
		return $list;
319
	}
320
321
	/**
322
	 * Set a read callback to accept data read from the HTTP request.
323
	 * By default, data is appended to an internal buffer which can be
324
	 * retrieved through $req->getContent().
325
	 *
326
	 * To handle data as it comes in -- especially for large files that
327
	 * would not fit in memory -- you can instead set your own callback,
328
	 * in the form function($resource, $buffer) where the first parameter
329
	 * is the low-level resource being read (implementation specific),
330
	 * and the second parameter is the data buffer.
331
	 *
332
	 * You MUST return the number of bytes handled in the buffer; if fewer
333
	 * bytes are reported handled than were passed to you, the HTTP fetch
334
	 * will be aborted.
335
	 *
336
	 * @param callable $callback
337
	 * @throws MWException
338
	 */
339
	public function setCallback( $callback ) {
340
		if ( !is_callable( $callback ) ) {
341
			throw new MWException( 'Invalid MwHttpRequest callback' );
342
		}
343
		$this->callback = $callback;
344
	}
345
346
	/**
347
	 * A generic callback to read the body of the response from a remote
348
	 * server.
349
	 *
350
	 * @param resource $fh
351
	 * @param string $content
352
	 * @return int
353
	 */
354
	public function read( $fh, $content ) {
355
		$this->content .= $content;
356
		return strlen( $content );
357
	}
358
359
	/**
360
	 * Take care of whatever is necessary to perform the URI request.
361
	 *
362
	 * @return Status
363
	 */
364
	public function execute() {
365
		$this->content = "";
366
367
		if ( strtoupper( $this->method ) == "HEAD" ) {
368
			$this->headersOnly = true;
369
		}
370
371
		$this->proxySetup(); // set up any proxy as needed
372
373
		if ( !$this->callback ) {
374
			$this->setCallback( [ $this, 'read' ] );
375
		}
376
377
		if ( !isset( $this->reqHeaders['User-Agent'] ) ) {
378
			$this->setUserAgent( Http::userAgent() );
379
		}
380
	}
381
382
	/**
383
	 * Parses the headers, including the HTTP status code and any
384
	 * Set-Cookie headers.  This function expects the headers to be
385
	 * found in an array in the member variable headerList.
386
	 */
387
	protected function parseHeader() {
388
		$lastname = "";
389
390
		foreach ( $this->headerList as $header ) {
391
			if ( preg_match( "#^HTTP/([0-9.]+) (.*)#", $header, $match ) ) {
392
				$this->respVersion = $match[1];
393
				$this->respStatus = $match[2];
394
			} elseif ( preg_match( "#^[ \t]#", $header ) ) {
395
				$last = count( $this->respHeaders[$lastname] ) - 1;
396
				$this->respHeaders[$lastname][$last] .= "\r\n$header";
397
			} elseif ( preg_match( "#^([^:]*):[\t ]*(.*)#", $header, $match ) ) {
398
				$this->respHeaders[strtolower( $match[1] )][] = $match[2];
399
				$lastname = strtolower( $match[1] );
400
			}
401
		}
402
403
		$this->parseCookies();
404
	}
405
406
	/**
407
	 * Sets HTTPRequest status member to a fatal value with the error
408
	 * message if the returned integer value of the status code was
409
	 * not successful (< 300) or a redirect (>=300 and < 400).  (see
410
	 * RFC2616, section 10,
411
	 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html for a
412
	 * list of status codes.)
413
	 */
414
	protected function setStatus() {
415
		if ( !$this->respHeaders ) {
416
			$this->parseHeader();
417
		}
418
419
		if ( (int)$this->respStatus > 399 ) {
420
			list( $code, $message ) = explode( " ", $this->respStatus, 2 );
421
			$this->status->fatal( "http-bad-status", $code, $message );
422
		}
423
	}
424
425
	/**
426
	 * Get the integer value of the HTTP status code (e.g. 200 for "200 Ok")
427
	 * (see RFC2616, section 10, http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
428
	 * for a list of status codes.)
429
	 *
430
	 * @return int
431
	 */
432
	public function getStatus() {
433
		if ( !$this->respHeaders ) {
434
			$this->parseHeader();
435
		}
436
437
		return (int)$this->respStatus;
438
	}
439
440
	/**
441
	 * Returns true if the last status code was a redirect.
442
	 *
443
	 * @return bool
444
	 */
445
	public function isRedirect() {
446
		if ( !$this->respHeaders ) {
447
			$this->parseHeader();
448
		}
449
450
		$status = (int)$this->respStatus;
451
452
		if ( $status >= 300 && $status <= 303 ) {
453
			return true;
454
		}
455
456
		return false;
457
	}
458
459
	/**
460
	 * Returns an associative array of response headers after the
461
	 * request has been executed.  Because some headers
462
	 * (e.g. Set-Cookie) can appear more than once the, each value of
463
	 * the associative array is an array of the values given.
464
	 *
465
	 * @return array
466
	 */
467
	public function getResponseHeaders() {
468
		if ( !$this->respHeaders ) {
469
			$this->parseHeader();
470
		}
471
472
		return $this->respHeaders;
473
	}
474
475
	/**
476
	 * Returns the value of the given response header.
477
	 *
478
	 * @param string $header
479
	 * @return string|null
480
	 */
481
	public function getResponseHeader( $header ) {
482
		if ( !$this->respHeaders ) {
483
			$this->parseHeader();
484
		}
485
486
		if ( isset( $this->respHeaders[strtolower( $header )] ) ) {
487
			$v = $this->respHeaders[strtolower( $header )];
488
			return $v[count( $v ) - 1];
489
		}
490
491
		return null;
492
	}
493
494
	/**
495
	 * Tells the MWHttpRequest object to use this pre-loaded CookieJar.
496
	 *
497
	 * @param CookieJar $jar
498
	 */
499
	public function setCookieJar( $jar ) {
500
		$this->cookieJar = $jar;
501
	}
502
503
	/**
504
	 * Returns the cookie jar in use.
505
	 *
506
	 * @return CookieJar
507
	 */
508
	public function getCookieJar() {
509
		if ( !$this->respHeaders ) {
510
			$this->parseHeader();
511
		}
512
513
		return $this->cookieJar;
514
	}
515
516
	/**
517
	 * Sets a cookie. Used before a request to set up any individual
518
	 * cookies. Used internally after a request to parse the
519
	 * Set-Cookie headers.
520
	 * @see Cookie::set
521
	 * @param string $name
522
	 * @param mixed $value
523
	 * @param array $attr
524
	 */
525
	public function setCookie( $name, $value = null, $attr = null ) {
526
		if ( !$this->cookieJar ) {
527
			$this->cookieJar = new CookieJar;
528
		}
529
530
		$this->cookieJar->setCookie( $name, $value, $attr );
0 ignored issues
show
It seems like $attr defined by parameter $attr on line 525 can also be of type null; however, CookieJar::setCookie() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
531
	}
532
533
	/**
534
	 * Parse the cookies in the response headers and store them in the cookie jar.
535
	 */
536
	protected function parseCookies() {
537
		if ( !$this->cookieJar ) {
538
			$this->cookieJar = new CookieJar;
539
		}
540
541
		if ( isset( $this->respHeaders['set-cookie'] ) ) {
542
			$url = parse_url( $this->getFinalUrl() );
543
			foreach ( $this->respHeaders['set-cookie'] as $cookie ) {
544
				$this->cookieJar->parseCookieResponseHeader( $cookie, $url['host'] );
545
			}
546
		}
547
	}
548
549
	/**
550
	 * Returns the final URL after all redirections.
551
	 *
552
	 * Relative values of the "Location" header are incorrect as
553
	 * stated in RFC, however they do happen and modern browsers
554
	 * support them.  This function loops backwards through all
555
	 * locations in order to build the proper absolute URI - Marooned
556
	 * at wikia-inc.com
557
	 *
558
	 * Note that the multiple Location: headers are an artifact of
559
	 * CURL -- they shouldn't actually get returned this way. Rewrite
560
	 * this when bug 29232 is taken care of (high-level redirect
561
	 * handling rewrite).
562
	 *
563
	 * @return string
564
	 */
565
	public function getFinalUrl() {
566
		$headers = $this->getResponseHeaders();
567
568
		// return full url (fix for incorrect but handled relative location)
569
		if ( isset( $headers['location'] ) ) {
570
			$locations = $headers['location'];
571
			$domain = '';
572
			$foundRelativeURI = false;
573
			$countLocations = count( $locations );
574
575
			for ( $i = $countLocations - 1; $i >= 0; $i-- ) {
576
				$url = parse_url( $locations[$i] );
577
578
				if ( isset( $url['host'] ) ) {
579
					$domain = $url['scheme'] . '://' . $url['host'];
580
					break; // found correct URI (with host)
581
				} else {
582
					$foundRelativeURI = true;
583
				}
584
			}
585
586
			if ( $foundRelativeURI ) {
587
				if ( $domain ) {
588
					return $domain . $locations[$countLocations - 1];
589
				} else {
590
					$url = parse_url( $this->url );
591
					if ( isset( $url['host'] ) ) {
592
						return $url['scheme'] . '://' . $url['host'] .
593
							$locations[$countLocations - 1];
594
					}
595
				}
596
			} else {
597
				return $locations[$countLocations - 1];
598
			}
599
		}
600
601
		return $this->url;
602
	}
603
604
	/**
605
	 * Returns true if the backend can follow redirects. Overridden by the
606
	 * child classes.
607
	 * @return bool
608
	 */
609
	public function canFollowRedirects() {
610
		return true;
611
	}
612
}
613