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/exception/MWExceptionRenderer.php (4 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
 * @author Aaron Schulz
20
 */
21
22
/**
23
 * Class to expose exceptions to the client (API bots, users, admins using CLI scripts)
24
 * @since 1.28
25
 */
26
class MWExceptionRenderer {
27
	const AS_RAW = 1; // show as text
28
	const AS_PRETTY = 2; // show as HTML
29
30
	/**
31
	 * @param Exception|Throwable $e Original exception
32
	 * @param integer $mode MWExceptionExposer::AS_* constant
33
	 * @param Exception|Throwable|null $eNew New exception from attempting to show the first
34
	 */
35
	public static function output( $e, $mode, $eNew = null ) {
36
		global $wgMimeType;
37
38
		if ( defined( 'MW_API' ) ) {
39
			// Unhandled API exception, we can't be sure that format printer is alive
40
			self::header( 'MediaWiki-API-Error: internal_api_error_' . get_class( $e ) );
41
			wfHttpError( 500, 'Internal Server Error', self::getText( $e ) );
42
		} elseif ( self::isCommandLine() ) {
43
			self::printError( self::getText( $e ) );
44
		} elseif ( $mode === self::AS_PRETTY ) {
45
			if ( $e instanceof DBConnectionError ) {
46
				self::reportOutageHTML( $e );
47
			} else {
48
				self::statusHeader( 500 );
49
				self::header( "Content-Type: $wgMimeType; charset=utf-8" );
50
				self::reportHTML( $e );
51
			}
52
		} else {
53
			if ( $eNew ) {
54
				$message = "MediaWiki internal error.\n\n";
55
				if ( self::showBackTrace( $e ) ) {
56
					$message .= 'Original exception: ' .
57
						MWExceptionHandler::getLogMessage( $e ) .
58
						"\nBacktrace:\n" . MWExceptionHandler::getRedactedTraceAsString( $e ) .
59
						"\n\nException caught inside exception handler: " .
60
							MWExceptionHandler::getLogMessage( $eNew ) .
61
						"\nBacktrace:\n" . MWExceptionHandler::getRedactedTraceAsString( $eNew );
62
				} else {
63
					$message .= "Exception caught inside exception handler.\n\n" .
64
						"Set \$wgShowExceptionDetails = true; at the bottom of LocalSettings.php " .
65
						"to show detailed debugging information.";
66
				}
67
				$message .= "\n";
68
			} else {
69
				if ( self::showBackTrace( $e ) ) {
70
					$message = MWExceptionHandler::getLogMessage( $e ) .
71
						"\nBacktrace:\n" .
72
						MWExceptionHandler::getRedactedTraceAsString( $e ) . "\n";
73
				} else {
74
					$message = MWExceptionHandler::getPublicLogMessage( $e );
75
				}
76
			}
77
			if ( self::isCommandLine() ) {
78
				self::printError( $message );
79
			} else {
80
				echo nl2br( htmlspecialchars( $message ) ) . "\n";
81
			}
82
		}
83
	}
84
85
	/**
86
	 * Run hook to allow extensions to modify the text of the exception
87
	 *
88
	 * Called by MWException for b/c
89
	 *
90
	 * @param Exception|Throwable $e
91
	 * @param string $name Class name of the exception
92
	 * @param array $args Arguments to pass to the callback functions
93
	 * @return string|null String to output or null if any hook has been called
94
	 */
95
	public static function runHooks( $e, $name, $args = [] ) {
96
		global $wgExceptionHooks;
97
98
		if ( !isset( $wgExceptionHooks ) || !is_array( $wgExceptionHooks ) ) {
99
			return null; // Just silently ignore
100
		}
101
102
		if ( !array_key_exists( $name, $wgExceptionHooks ) ||
103
			!is_array( $wgExceptionHooks[$name] )
104
		) {
105
			return null;
106
		}
107
108
		$hooks = $wgExceptionHooks[$name];
109
		$callargs = array_merge( [ $e ], $args );
110
111
		foreach ( $hooks as $hook ) {
112
			if (
113
				is_string( $hook ) ||
114
				( is_array( $hook ) && count( $hook ) >= 2 && is_string( $hook[0] ) )
115
			) {
116
				// 'function' or [ 'class', 'hook' ]
117
				$result = call_user_func_array( $hook, $callargs );
118
			} else {
119
				$result = null;
120
			}
121
122
			if ( is_string( $result ) ) {
123
				return $result;
124
			}
125
		}
126
127
		return null;
128
	}
129
130
	/**
131
	 * @param Exception|Throwable $e
132
	 * @return bool Should the exception use $wgOut to output the error?
133
	 */
134
	private static function useOutputPage( $e ) {
0 ignored issues
show
useOutputPage uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
135
		// Can the extension use the Message class/wfMessage to get i18n-ed messages?
136 View Code Duplication
		foreach ( $e->getTrace() as $frame ) {
137
			if ( isset( $frame['class'] ) && $frame['class'] === 'LocalisationCache' ) {
138
				return false;
139
			}
140
		}
141
142
		return (
143
			!empty( $GLOBALS['wgFullyInitialised'] ) &&
144
			!empty( $GLOBALS['wgOut'] ) &&
145
			!defined( 'MEDIAWIKI_INSTALL' )
146
		);
147
	}
148
149
	/**
150
	 * Output the exception report using HTML
151
	 *
152
	 * @param Exception|Throwable $e
153
	 */
154
	private static function reportHTML( $e ) {
155
		global $wgOut, $wgSitename;
156
157
		if ( self::useOutputPage( $e ) ) {
158
			if ( $e instanceof MWException ) {
159
				$wgOut->prepareErrorPage( $e->getPageTitle() );
160
			} elseif ( $e instanceof DBReadOnlyError ) {
161
				$wgOut->prepareErrorPage( self::msg( 'readonly', 'Database is locked' ) );
162
			} elseif ( $e instanceof DBExpectedError ) {
163
				$wgOut->prepareErrorPage( self::msg( 'databaseerror', 'Database error' ) );
164
			} else {
165
				$wgOut->prepareErrorPage( self::msg( 'internalerror', 'Internal error' ) );
166
			}
167
168
			$hookResult = self::runHooks( $e, get_class( $e ) );
169
			if ( $hookResult ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $hookResult of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
170
				$wgOut->addHTML( $hookResult );
171
			} else {
172
				// Show any custom GUI message before the details
173
				if ( $e instanceof MessageSpecifier ) {
174
					$wgOut->addHTML( Message::newFromSpecifier( $e )->escaped() );
175
				}
176
				$wgOut->addHTML( self::getHTML( $e ) );
177
			}
178
179
			$wgOut->output();
180
		} else {
181
			self::header( 'Content-Type: text/html; charset=utf-8' );
182
			$pageTitle = self::msg( 'internalerror', 'Internal error' );
183
			echo "<!DOCTYPE html>\n" .
184
				'<html><head>' .
185
				// Mimick OutputPage::setPageTitle behaviour
186
				'<title>' .
187
				htmlspecialchars( self::msg( 'pagetitle', "$1 - $wgSitename", $pageTitle ) ) .
188
				'</title>' .
189
				'<style>body { font-family: sans-serif; margin: 0; padding: 0.5em 2em; }</style>' .
190
				"</head><body>\n";
191
192
			$hookResult = self::runHooks( $e, get_class( $e ) . 'Raw' );
193
			if ( $hookResult ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $hookResult of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
194
				echo $hookResult;
195
			} else {
196
				echo self::getHTML( $e );
197
			}
198
199
			echo "</body></html>\n";
200
		}
201
	}
202
203
	/**
204
	 * If $wgShowExceptionDetails is true, return a HTML message with a
205
	 * backtrace to the error, otherwise show a message to ask to set it to true
206
	 * to show that information.
207
	 *
208
	 * @param Exception|Throwable $e
209
	 * @return string Html to output
210
	 */
211
	public static function getHTML( $e ) {
212
		if ( self::showBackTrace( $e ) ) {
213
			$html = "<div class=\"errorbox\"><p>" .
214
				nl2br( htmlspecialchars( MWExceptionHandler::getLogMessage( $e ) ) ) .
215
				'</p><p>Backtrace:</p><p>' .
216
				nl2br( htmlspecialchars( MWExceptionHandler::getRedactedTraceAsString( $e ) ) ) .
217
				"</p></div>\n";
218
		} else {
219
			$logId = WebRequest::getRequestId();
220
			$html = "<div class=\"errorbox\">" .
221
				'[' . $logId . '] ' .
222
				gmdate( 'Y-m-d H:i:s' ) . ": " .
223
				self::msg( "internalerror-fatal-exception",
224
					"Fatal exception of type $1",
225
					get_class( $e ),
226
					$logId,
227
					MWExceptionHandler::getURL()
228
				) . "</div>\n" .
229
			"<!-- Set \$wgShowExceptionDetails = true; " .
230
			"at the bottom of LocalSettings.php to show detailed " .
231
			"debugging information. -->";
232
		}
233
234
		return $html;
235
	}
236
237
	/**
238
	 * Get a message from i18n
239
	 *
240
	 * @param string $key Message name
241
	 * @param string $fallback Default message if the message cache can't be
242
	 *                  called by the exception
243
	 * The function also has other parameters that are arguments for the message
244
	 * @return string Message with arguments replaced
245
	 */
246 View Code Duplication
	private static function msg( $key, $fallback /*[, params...] */ ) {
247
		$args = array_slice( func_get_args(), 2 );
248
		try {
249
			return wfMessage( $key, $args )->text();
250
		} catch ( Exception $e ) {
251
			return wfMsgReplaceArgs( $fallback, $args );
252
		}
253
	}
254
255
	/**
256
	 * @param Exception|Throwable $e
257
	 * @return string
258
	 */
259 View Code Duplication
	private static function getText( $e ) {
260
		if ( self::showBackTrace( $e ) ) {
261
			return MWExceptionHandler::getLogMessage( $e ) .
262
				"\nBacktrace:\n" .
263
				MWExceptionHandler::getRedactedTraceAsString( $e ) . "\n";
264
		} else {
265
			return "Set \$wgShowExceptionDetails = true; " .
266
				"in LocalSettings.php to show detailed debugging information.\n";
267
		}
268
	}
269
270
	/**
271
	 * @param Exception|Throwable $e
272
	 * @return bool
273
	 */
274
	private static function showBackTrace( $e ) {
275
		global $wgShowExceptionDetails, $wgShowDBErrorBacktrace;
276
277
		return (
278
			$wgShowExceptionDetails &&
279
			( !( $e instanceof DBError ) || $wgShowDBErrorBacktrace )
280
		);
281
	}
282
283
	/**
284
	 * @return bool
285
	 */
286
	private static function isCommandLine() {
0 ignored issues
show
isCommandLine uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
287
		return !empty( $GLOBALS['wgCommandLineMode'] );
288
	}
289
290
	/**
291
	 * @param string $header
292
	 */
293
	private static function header( $header ) {
294
		if ( !headers_sent() ) {
295
			header( $header );
296
		}
297
	}
298
299
	/**
300
	 * @param integer $code
301
	 */
302
	private static function statusHeader( $code ) {
303
		if ( !headers_sent() ) {
304
			HttpStatus::header( $code );
305
		}
306
	}
307
308
	/**
309
	 * Print a message, if possible to STDERR.
310
	 * Use this in command line mode only (see isCommandLine)
311
	 *
312
	 * @param string $message Failure text
313
	 */
314
	private static function printError( $message ) {
315
		// NOTE: STDERR may not be available, especially if php-cgi is used from the
316
		// command line (bug #15602). Try to produce meaningful output anyway. Using
317
		// echo may corrupt output to STDOUT though.
318
		if ( defined( 'STDERR' ) ) {
319
			fwrite( STDERR, $message );
320
		} else {
321
			echo $message;
322
		}
323
	}
324
325
	/**
326
	 * @param Exception|Throwable $e
327
	 */
328
	private static function reportOutageHTML( $e ) {
329
		global $wgShowDBErrorBacktrace, $wgShowHostnames, $wgShowSQLErrors;
330
331
		$sorry = htmlspecialchars( self::msg(
332
			'dberr-problems',
333
			'Sorry! This site is experiencing technical difficulties.'
334
		) );
335
		$again = htmlspecialchars( self::msg(
336
			'dberr-again',
337
			'Try waiting a few minutes and reloading.'
338
		) );
339
340
		if ( $wgShowHostnames || $wgShowSQLErrors ) {
341
			$info = str_replace(
342
				'$1',
343
				Html::element( 'span', [ 'dir' => 'ltr' ], htmlspecialchars( $e->getMessage() ) ),
344
				htmlspecialchars( self::msg( 'dberr-info', '($1)' ) )
345
			);
346
		} else {
347
			$info = htmlspecialchars( self::msg(
348
				'dberr-info-hidden',
349
				'(Cannot access the database)'
350
			) );
351
		}
352
353
		MessageCache::singleton()->disable(); // no DB access
354
355
		$html = "<h1>$sorry</h1><p>$again</p><p><small>$info</small></p>";
356
357
		if ( $wgShowDBErrorBacktrace ) {
358
			$html .= '<p>Backtrace:</p><pre>' .
359
				htmlspecialchars( $e->getTraceAsString() ) . '</pre>';
360
		}
361
362
		$html .= '<hr />';
363
		$html .= self::googleSearchForm();
364
365
		echo $html;
366
	}
367
368
	/**
369
	 * @return string
370
	 */
371
	private static function googleSearchForm() {
372
		global $wgSitename, $wgCanonicalServer, $wgRequest;
373
374
		$usegoogle = htmlspecialchars( self::msg(
375
			'dberr-usegoogle',
376
			'You can try searching via Google in the meantime.'
377
		) );
378
		$outofdate = htmlspecialchars( self::msg(
379
			'dberr-outofdate',
380
			'Note that their indexes of our content may be out of date.'
381
		) );
382
		$googlesearch = htmlspecialchars( self::msg( 'searchbutton', 'Search' ) );
383
		$search = htmlspecialchars( $wgRequest->getVal( 'search' ) );
384
		$server = htmlspecialchars( $wgCanonicalServer );
385
		$sitename = htmlspecialchars( $wgSitename );
386
		$trygoogle = <<<EOT
387
<div style="margin: 1.5em">$usegoogle<br />
388
<small>$outofdate</small>
389
</div>
390
<form method="get" action="//www.google.com/search" id="googlesearch">
391
	<input type="hidden" name="domains" value="$server" />
392
	<input type="hidden" name="num" value="50" />
393
	<input type="hidden" name="ie" value="UTF-8" />
394
	<input type="hidden" name="oe" value="UTF-8" />
395
	<input type="text" name="q" size="31" maxlength="255" value="$search" />
396
	<input type="submit" name="btnG" value="$googlesearch" />
397
	<p>
398
		<label><input type="radio" name="sitesearch" value="$server" checked="checked" />$sitename</label>
399
		<label><input type="radio" name="sitesearch" value="" />WWW</label>
400
	</p>
401
</form>
402
EOT;
403
		return $trygoogle;
404
	}
405
}
406