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/debug/MWDebug.php (2 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
 * Debug toolbar related code.
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
use MediaWiki\Logger\LegacyLogger;
24
25
/**
26
 * New debugger system that outputs a toolbar on page view.
27
 *
28
 * By default, most methods do nothing ( self::$enabled = false ). You have
29
 * to explicitly call MWDebug::init() to enabled them.
30
 *
31
 * @since 1.19
32
 */
33
class MWDebug {
34
	/**
35
	 * Log lines
36
	 *
37
	 * @var array $log
38
	 */
39
	protected static $log = [];
40
41
	/**
42
	 * Debug messages from wfDebug().
43
	 *
44
	 * @var array $debug
45
	 */
46
	protected static $debug = [];
47
48
	/**
49
	 * SQL statements of the database queries.
50
	 *
51
	 * @var array $query
52
	 */
53
	protected static $query = [];
54
55
	/**
56
	 * Is the debugger enabled?
57
	 *
58
	 * @var bool $enabled
59
	 */
60
	protected static $enabled = false;
61
62
	/**
63
	 * Array of functions that have already been warned, formatted
64
	 * function-caller to prevent a buttload of warnings
65
	 *
66
	 * @var array $deprecationWarnings
67
	 */
68
	protected static $deprecationWarnings = [];
69
70
	/**
71
	 * Enabled the debugger and load resource module.
72
	 * This is called by Setup.php when $wgDebugToolbar is true.
73
	 *
74
	 * @since 1.19
75
	 */
76
	public static function init() {
77
		self::$enabled = true;
78
	}
79
80
	/**
81
	 * Disable the debugger.
82
	 *
83
	 * @since 1.28
84
	 */
85
	public static function deinit() {
86
		self::$enabled = false;
87
	}
88
89
	/**
90
	 * Add ResourceLoader modules to the OutputPage object if debugging is
91
	 * enabled.
92
	 *
93
	 * @since 1.19
94
	 * @param OutputPage $out
95
	 */
96
	public static function addModules( OutputPage $out ) {
97
		if ( self::$enabled ) {
98
			$out->addModules( 'mediawiki.debug' );
99
		}
100
	}
101
102
	/**
103
	 * Adds a line to the log
104
	 *
105
	 * @since 1.19
106
	 * @param mixed $str
107
	 */
108
	public static function log( $str ) {
109
		if ( !self::$enabled ) {
110
			return;
111
		}
112
		if ( !is_string( $str ) ) {
113
			$str = print_r( $str, true );
114
		}
115
		self::$log[] = [
116
			'msg' => htmlspecialchars( $str ),
117
			'type' => 'log',
118
			'caller' => wfGetCaller(),
119
		];
120
	}
121
122
	/**
123
	 * Returns internal log array
124
	 * @since 1.19
125
	 * @return array
126
	 */
127
	public static function getLog() {
128
		return self::$log;
129
	}
130
131
	/**
132
	 * Clears internal log array and deprecation tracking
133
	 * @since 1.19
134
	 */
135
	public static function clearLog() {
136
		self::$log = [];
137
		self::$deprecationWarnings = [];
138
	}
139
140
	/**
141
	 * Adds a warning entry to the log
142
	 *
143
	 * @since 1.19
144
	 * @param string $msg
145
	 * @param int $callerOffset
146
	 * @param int $level A PHP error level. See sendMessage()
147
	 * @param string $log 'production' will always trigger a php error, 'auto'
148
	 *    will trigger an error if $wgDevelopmentWarnings is true, and 'debug'
149
	 *    will only write to the debug log(s).
150
	 *
151
	 * @return mixed
152
	 */
153
	public static function warning( $msg, $callerOffset = 1, $level = E_USER_NOTICE, $log = 'auto' ) {
154
		global $wgDevelopmentWarnings;
155
156
		if ( $log === 'auto' && !$wgDevelopmentWarnings ) {
157
			$log = 'debug';
158
		}
159
160
		if ( $log === 'debug' ) {
161
			$level = false;
162
		}
163
164
		$callerDescription = self::getCallerDescription( $callerOffset );
165
166
		self::sendMessage( $msg, $callerDescription, 'warning', $level );
167
168
		if ( self::$enabled ) {
169
			self::$log[] = [
170
				'msg' => htmlspecialchars( $msg ),
171
				'type' => 'warn',
172
				'caller' => $callerDescription['func'],
173
			];
174
		}
175
	}
176
177
	/**
178
	 * Show a warning that $function is deprecated.
179
	 * This will send it to the following locations:
180
	 * - Debug toolbar, with one item per function and caller, if $wgDebugToolbar
181
	 *   is set to true.
182
	 * - PHP's error log, with level E_USER_DEPRECATED, if $wgDevelopmentWarnings
183
	 *   is set to true.
184
	 * - MediaWiki's debug log, if $wgDevelopmentWarnings is set to false.
185
	 *
186
	 * @since 1.19
187
	 * @param string $function Function that is deprecated.
188
	 * @param string|bool $version Version in which the function was deprecated.
189
	 * @param string|bool $component Component to which the function belongs.
190
	 *    If false, it is assumbed the function is in MediaWiki core.
191
	 * @param int $callerOffset How far up the callstack is the original
192
	 *    caller. 2 = function that called the function that called
193
	 *    MWDebug::deprecated() (Added in 1.20).
194
	 */
195
	public static function deprecated( $function, $version = false,
196
		$component = false, $callerOffset = 2
197
	) {
198
		$callerDescription = self::getCallerDescription( $callerOffset );
199
		$callerFunc = $callerDescription['func'];
200
201
		$sendToLog = true;
202
203
		// Check to see if there already was a warning about this function
204
		if ( isset( self::$deprecationWarnings[$function][$callerFunc] ) ) {
205
			return;
206
		} elseif ( isset( self::$deprecationWarnings[$function] ) ) {
207
			if ( self::$enabled ) {
208
				$sendToLog = false;
209
			} else {
210
				return;
211
			}
212
		}
213
214
		self::$deprecationWarnings[$function][$callerFunc] = true;
215
216
		if ( $version ) {
217
			global $wgDeprecationReleaseLimit;
218
			if ( $wgDeprecationReleaseLimit && $component === false ) {
219
				# Strip -* off the end of $version so that branches can use the
220
				# format #.##-branchname to avoid issues if the branch is merged into
221
				# a version of MediaWiki later than what it was branched from
222
				$comparableVersion = preg_replace( '/-.*$/', '', $version );
223
224
				# If the comparableVersion is larger than our release limit then
225
				# skip the warning message for the deprecation
226
				if ( version_compare( $wgDeprecationReleaseLimit, $comparableVersion, '<' ) ) {
227
					$sendToLog = false;
228
				}
229
			}
230
231
			$component = $component === false ? 'MediaWiki' : $component;
232
			$msg = "Use of $function was deprecated in $component $version.";
233
		} else {
234
			$msg = "Use of $function is deprecated.";
235
		}
236
237
		if ( $sendToLog ) {
238
			global $wgDevelopmentWarnings; // we could have a more specific $wgDeprecationWarnings setting.
239
			self::sendMessage(
240
				$msg,
241
				$callerDescription,
242
				'deprecated',
243
				$wgDevelopmentWarnings ? E_USER_DEPRECATED : false
244
			);
245
		}
246
247
		if ( self::$enabled ) {
248
			$logMsg = htmlspecialchars( $msg ) .
249
				Html::rawElement( 'div', [ 'class' => 'mw-debug-backtrace' ],
250
					Html::element( 'span', [], 'Backtrace:' ) . wfBacktrace()
251
				);
252
253
			self::$log[] = [
254
				'msg' => $logMsg,
255
				'type' => 'deprecated',
256
				'caller' => $callerFunc,
257
			];
258
		}
259
	}
260
261
	/**
262
	 * Get an array describing the calling function at a specified offset.
263
	 *
264
	 * @param int $callerOffset How far up the callstack is the original
265
	 *    caller. 0 = function that called getCallerDescription()
266
	 * @return array Array with two keys: 'file' and 'func'
267
	 */
268
	private static function getCallerDescription( $callerOffset ) {
269
		$callers = wfDebugBacktrace();
270
271
		if ( isset( $callers[$callerOffset] ) ) {
272
			$callerfile = $callers[$callerOffset];
273
			if ( isset( $callerfile['file'] ) && isset( $callerfile['line'] ) ) {
274
				$file = $callerfile['file'] . ' at line ' . $callerfile['line'];
275
			} else {
276
				$file = '(internal function)';
277
			}
278
		} else {
279
			$file = '(unknown location)';
280
		}
281
282
		if ( isset( $callers[$callerOffset + 1] ) ) {
283
			$callerfunc = $callers[$callerOffset + 1];
284
			$func = '';
285
			if ( isset( $callerfunc['class'] ) ) {
286
				$func .= $callerfunc['class'] . '::';
287
			}
288
			if ( isset( $callerfunc['function'] ) ) {
289
				$func .= $callerfunc['function'];
290
			}
291
		} else {
292
			$func = 'unknown';
293
		}
294
295
		return [ 'file' => $file, 'func' => $func ];
296
	}
297
298
	/**
299
	 * Send a message to the debug log and optionally also trigger a PHP
300
	 * error, depending on the $level argument.
301
	 *
302
	 * @param string $msg Message to send
303
	 * @param array $caller Caller description get from getCallerDescription()
304
	 * @param string $group Log group on which to send the message
305
	 * @param int|bool $level Error level to use; set to false to not trigger an error
306
	 */
307
	private static function sendMessage( $msg, $caller, $group, $level ) {
308
		$msg .= ' [Called from ' . $caller['func'] . ' in ' . $caller['file'] . ']';
309
310
		if ( $level !== false ) {
311
			trigger_error( $msg, $level );
312
		}
313
314
		wfDebugLog( $group, $msg, 'private' );
315
	}
316
317
	/**
318
	 * This is a method to pass messages from wfDebug to the pretty debugger.
319
	 * Do NOT use this method, use MWDebug::log or wfDebug()
320
	 *
321
	 * @since 1.19
322
	 * @param string $str
323
	 * @param array $context
324
	 */
325
	public static function debugMsg( $str, $context = [] ) {
326
		global $wgDebugComments, $wgShowDebug;
327
328
		if ( self::$enabled || $wgDebugComments || $wgShowDebug ) {
329
			if ( $context ) {
330
				$prefix = '';
331
				if ( isset( $context['prefix'] ) ) {
332
					$prefix = $context['prefix'];
333
				} elseif ( isset( $context['channel'] ) && $context['channel'] !== 'wfDebug' ) {
334
					$prefix = "[{$context['channel']}] ";
335
				}
336
				if ( isset( $context['seconds_elapsed'] ) && isset( $context['memory_used'] ) ) {
337
					$prefix .= "{$context['seconds_elapsed']} {$context['memory_used']}  ";
338
				}
339
				$str = LegacyLogger::interpolate( $str, $context );
340
				$str = $prefix . $str;
341
			}
342
			self::$debug[] = rtrim( UtfNormal\Validator::cleanUp( $str ) );
343
		}
344
	}
345
346
	/**
347
	 * Begins profiling on a database query
348
	 *
349
	 * @since 1.19
350
	 * @param string $sql
351
	 * @param string $function
352
	 * @param bool $isMaster
353
	 * @param float $runTime Query run time
354
	 * @return int ID number of the query to pass to queryTime or -1 if the
355
	 *  debugger is disabled
356
	 */
357
	public static function query( $sql, $function, $isMaster, $runTime ) {
358
		if ( !self::$enabled ) {
359
			return -1;
360
		}
361
362
		// Replace invalid UTF-8 chars with a square UTF-8 character
363
		// This prevents json_encode from erroring out due to binary SQL data
364
		$sql = preg_replace(
365
			'/(
366
				[\xC0-\xC1] # Invalid UTF-8 Bytes
367
				| [\xF5-\xFF] # Invalid UTF-8 Bytes
368
				| \xE0[\x80-\x9F] # Overlong encoding of prior code point
369
				| \xF0[\x80-\x8F] # Overlong encoding of prior code point
370
				| [\xC2-\xDF](?![\x80-\xBF]) # Invalid UTF-8 Sequence Start
371
				| [\xE0-\xEF](?![\x80-\xBF]{2}) # Invalid UTF-8 Sequence Start
372
				| [\xF0-\xF4](?![\x80-\xBF]{3}) # Invalid UTF-8 Sequence Start
373
				| (?<=[\x0-\x7F\xF5-\xFF])[\x80-\xBF] # Invalid UTF-8 Sequence Middle
374
				| (?<![\xC2-\xDF]|[\xE0-\xEF]|[\xE0-\xEF][\x80-\xBF]|[\xF0-\xF4]
375
				   |[\xF0-\xF4][\x80-\xBF]|[\xF0-\xF4][\x80-\xBF]{2})[\x80-\xBF] # Overlong Sequence
376
				| (?<=[\xE0-\xEF])[\x80-\xBF](?![\x80-\xBF]) # Short 3 byte sequence
377
				| (?<=[\xF0-\xF4])[\x80-\xBF](?![\x80-\xBF]{2}) # Short 4 byte sequence
378
				| (?<=[\xF0-\xF4][\x80-\xBF])[\x80-\xBF](?![\x80-\xBF]) # Short 4 byte sequence (2)
379
			)/x',
380
			'â– ',
381
			$sql
382
		);
383
384
		// last check for invalid utf8
385
		$sql = UtfNormal\Validator::cleanUp( $sql );
386
387
		self::$query[] = [
388
			'sql' => $sql,
389
			'function' => $function,
390
			'master' => (bool)$isMaster,
391
			'time' => $runTime,
392
		];
393
394
		return count( self::$query ) - 1;
395
	}
396
397
	/**
398
	 * Returns a list of files included, along with their size
399
	 *
400
	 * @param IContextSource $context
401
	 * @return array
402
	 */
403
	protected static function getFilesIncluded( IContextSource $context ) {
404
		$files = get_included_files();
405
		$fileList = [];
406
		foreach ( $files as $file ) {
407
			$size = filesize( $file );
408
			$fileList[] = [
409
				'name' => $file,
410
				'size' => $context->getLanguage()->formatSize( $size ),
411
			];
412
		}
413
414
		return $fileList;
415
	}
416
417
	/**
418
	 * Returns the HTML to add to the page for the toolbar
419
	 *
420
	 * @since 1.19
421
	 * @param IContextSource $context
422
	 * @return string
423
	 */
424
	public static function getDebugHTML( IContextSource $context ) {
425
		global $wgDebugComments;
426
427
		$html = '';
428
429
		if ( self::$enabled ) {
430
			MWDebug::log( 'MWDebug output complete' );
431
			$debugInfo = self::getDebugInfo( $context );
432
433
			// Cannot use OutputPage::addJsConfigVars because those are already outputted
434
			// by the time this method is called.
435
			$html = ResourceLoader::makeInlineScript(
436
				ResourceLoader::makeConfigSetScript( [ 'debugInfo' => $debugInfo ] )
0 ignored issues
show
It seems like \ResourceLoader::makeCon...ugInfo' => $debugInfo)) targeting ResourceLoader::makeConfigSetScript() can also be of type false; however, ResourceLoader::makeInlineScript() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
437
			);
438
		}
439
440
		if ( $wgDebugComments ) {
441
			$html .= "<!-- Debug output:\n" .
442
				htmlspecialchars( implode( "\n", self::$debug ), ENT_NOQUOTES ) .
443
				"\n\n-->";
444
		}
445
446
		return $html;
447
	}
448
449
	/**
450
	 * Generate debug log in HTML for displaying at the bottom of the main
451
	 * content area.
452
	 * If $wgShowDebug is false, an empty string is always returned.
453
	 *
454
	 * @since 1.20
455
	 * @return string HTML fragment
456
	 */
457
	public static function getHTMLDebugLog() {
458
		global $wgShowDebug;
459
460
		if ( !$wgShowDebug ) {
461
			return '';
462
		}
463
464
		$ret = "\n<hr />\n<strong>Debug data:</strong><ul id=\"mw-debug-html\">\n";
465
466
		foreach ( self::$debug as $line ) {
467
			$display = nl2br( htmlspecialchars( trim( $line ) ) );
468
469
			$ret .= "<li><code>$display</code></li>\n";
470
		}
471
472
		$ret .= '</ul>' . "\n";
473
474
		return $ret;
475
	}
476
477
	/**
478
	 * Append the debug info to given ApiResult
479
	 *
480
	 * @param IContextSource $context
481
	 * @param ApiResult $result
482
	 */
483
	public static function appendDebugInfoToApiResult( IContextSource $context, ApiResult $result ) {
484
		if ( !self::$enabled ) {
485
			return;
486
		}
487
488
		// output errors as debug info, when display_errors is on
489
		// this is necessary for all non html output of the api, because that clears all errors first
490
		$obContents = ob_get_contents();
491
		if ( $obContents ) {
492
			$obContentArray = explode( '<br />', $obContents );
493
			foreach ( $obContentArray as $obContent ) {
494
				if ( trim( $obContent ) ) {
495
					self::debugMsg( Sanitizer::stripAllTags( $obContent ) );
496
				}
497
			}
498
		}
499
500
		MWDebug::log( 'MWDebug output complete' );
501
		$debugInfo = self::getDebugInfo( $context );
502
503
		ApiResult::setIndexedTagName( $debugInfo, 'debuginfo' );
504
		ApiResult::setIndexedTagName( $debugInfo['log'], 'line' );
505
		ApiResult::setIndexedTagName( $debugInfo['debugLog'], 'msg' );
506
		ApiResult::setIndexedTagName( $debugInfo['queries'], 'query' );
507
		ApiResult::setIndexedTagName( $debugInfo['includes'], 'queries' );
508
		$result->addValue( null, 'debuginfo', $debugInfo );
509
	}
510
511
	/**
512
	 * Returns the HTML to add to the page for the toolbar
513
	 *
514
	 * @param IContextSource $context
515
	 * @return array
516
	 */
517
	public static function getDebugInfo( IContextSource $context ) {
518
		if ( !self::$enabled ) {
519
			return [];
520
		}
521
522
		global $wgVersion, $wgRequestTime;
523
		$request = $context->getRequest();
524
525
		// HHVM's reported memory usage from memory_get_peak_usage()
526
		// is not useful when passing false, but we continue passing
527
		// false for consistency of historical data in zend.
528
		// see: https://github.com/facebook/hhvm/issues/2257#issuecomment-39362246
529
		$realMemoryUsage = wfIsHHVM();
530
531
		$branch = GitInfo::currentBranch();
532
		if ( GitInfo::isSHA1( $branch ) ) {
0 ignored issues
show
It seems like $branch defined by \GitInfo::currentBranch() on line 531 can also be of type boolean; however, GitInfo::isSHA1() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
533
			// If it's a detached HEAD, the SHA1 will already be
534
			// included in the MW version, so don't show it.
535
			$branch = false;
536
		}
537
538
		return [
539
			'mwVersion' => $wgVersion,
540
			'phpEngine' => wfIsHHVM() ? 'HHVM' : 'PHP',
541
			'phpVersion' => wfIsHHVM() ? HHVM_VERSION : PHP_VERSION,
542
			'gitRevision' => GitInfo::headSHA1(),
543
			'gitBranch' => $branch,
544
			'gitViewUrl' => GitInfo::headViewUrl(),
545
			'time' => microtime( true ) - $wgRequestTime,
546
			'log' => self::$log,
547
			'debugLog' => self::$debug,
548
			'queries' => self::$query,
549
			'request' => [
550
				'method' => $request->getMethod(),
551
				'url' => $request->getRequestURL(),
552
				'headers' => $request->getAllHeaders(),
553
				'params' => $request->getValues(),
554
			],
555
			'memory' => $context->getLanguage()->formatSize( memory_get_usage( $realMemoryUsage ) ),
556
			'memoryPeak' => $context->getLanguage()->formatSize( memory_get_peak_usage( $realMemoryUsage ) ),
557
			'includes' => self::getFilesIncluded( $context ),
558
		];
559
	}
560
}
561